summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorViacheslav Hletenko <v.gletenko@vyos.io>2026-04-10 12:47:55 +0300
committerGitHub <noreply@github.com>2026-04-10 12:47:55 +0300
commit4d26859088b1463965403c14472fbbff245864b5 (patch)
tree46fdf93514b38f4826705c385b2233ba8a32360a /scripts
parent1b4f4f5cf23cdc75864eaccbbfcf6fcfd9ebacba (diff)
parent08f179acb1e9959d24f55f43b29c1e5dcc8d88cf (diff)
downloadvyos-build-4d26859088b1463965403c14472fbbff245864b5.tar.gz
vyos-build-4d26859088b1463965403c14472fbbff245864b5.zip
Merge pull request #1154 from hedrok/T8457-bgp_link_state
T8457: FRR: backport BGP-LS
Diffstat (limited to 'scripts')
-rw-r--r--scripts/package-build/frr/patches/frr/0020-BGP-LS-support.patch17959
1 files changed, 17959 insertions, 0 deletions
diff --git a/scripts/package-build/frr/patches/frr/0020-BGP-LS-support.patch b/scripts/package-build/frr/patches/frr/0020-BGP-LS-support.patch
new file mode 100644
index 00000000..44d474f0
--- /dev/null
+++ b/scripts/package-build/frr/patches/frr/0020-BGP-LS-support.patch
@@ -0,0 +1,17959 @@
+From 5fb319d193444bcb3b92b55190922993a933d4b2 Mon Sep 17 00:00:00 2001
+From: Carmine Scarpitta <cscarpit@cisco.com>
+Date: Fri, 23 Jan 2026 10:27:38 +0000
+Subject: [PATCH] *: BGP-LS support
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Squash commits from PRs:
+
+* https://github.com/FRRouting/frr/pull/20470
+* https://github.com/FRRouting/frr/pull/21077
+* https://github.com/FRRouting/frr/pull/21078
+* https://github.com/FRRouting/frr/pull/21092
+* https://github.com/FRRouting/frr/pull/21102
+* https://github.com/FRRouting/frr/pull/21108
+* https://github.com/FRRouting/frr/pull/21109
+* https://github.com/FRRouting/frr/pull/21285
+* https://github.com/FRRouting/frr/pull/21286
+* https://github.com/FRRouting/frr/pull/21076
+* https://github.com/FRRouting/frr/pull/21376
+
+Plus e62419b8940 that is also needed
+
+*: Add BGP-LS AFI/SAFI constants
+
+Add BGP Link-State (BGP-LS) Address Family infrastructure to prepare
+for full protocol implementation in subsequent commits (RFC 9552).
+
+Introduce AFI_BGP_LS (16388) and SAFI_BGP_LS (71) and update all
+AFI/SAFI-aware code paths to recognize the new address family.
+
+Protocol-specific logic is marked with TODO placeholders to be
+implemented in subsequent commits.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 314b3eb5e6f3a6688d726661dfb8fb8985c36f5a)
+
+bgpd: Add debugging support for BGP-LS
+
+Add debugging support for BGP Link-State operations to aid development
+and troubleshooting.
+
+- Add `debug bgp link-state` command
+- Add `no debug bgp link-state` command
+- Extend `no debug bgp` to also disable link-state debugging
+- Show link-state debugging status in `show debugging bgp`
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit a98f8055d23ea97fb40343aa97c5cee9483ef535)
+
+bgpd: Add BGP-LS instance structure and initialization
+
+Introduce core BGP-LS module infrastructure.
+
+- Add `struct bgp_ls` to hold BGP-LS state
+- Add `bgp_ls_init()` to initialize BGP-LS module during BGP instance
+ creation
+- Add `bgp_ls_terminate()` to cleanup BGP-LS module during BGP instance
+ destruction
+
+Subsequent commits will extend this structure with NLRI and TED support.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 301b23373c3ad836f3cf9697c755faff00ba208e)
+
+bgpd: Add BGP-LS NLRI data structures
+
+Define structures for BGP Link-State topology elements per RFC 9552.
+
+- Add Node, Link, and Prefix NLRI structures
+- Support Node Descriptor TLVs (AS Number, BGP-LS ID, OSPF Area ID,
+ IGP Router ID)
+- Support Link Descriptor TLVs (Link ID, IPv4/IPv6 interface/neighbor
+ addresses, MT-ID)
+- Support Prefix Descriptor TLVs (OSPF Route Type, IP Reachability,
+ MT-ID)
+- Define BGP-LS Attribute structure for path attribute type 29
+
+The BGP-LS Attribute (codepoint 29) carries topology information
+for Link-State NLRIs including node attributes (router capabilities,
+SR capabilities), link attributes (TE metrics, bandwidth, delay),
+and prefix attributes (prefix metrics, SIDs).
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit e49a5cd8187b3ec1d24ab3b3e38a4546b161d8d7)
+
+bgpd: Add BGP-LS NLRI allocation and free functions
+
+Add memory management functions for BGP-LS NLRI structures.
+
+- Add bgp_ls_nlri_alloc() to allocate NLRI structures
+- Add bgp_ls_nlri_free() to free NLRI and cleanup dynamic fields
+- Add bgp_ls_attr_alloc() to allocate BGP-LS attribute structures
+- Add bgp_ls_attr_free() to free BGP-LS attributes and cleanup
+ dynamic fields
+
+These functions handle proper initialization and cleanup of all
+NLRI types (Node, Link, Prefix) and their associated BGP-LS
+attributes, ensuring no memory leaks from dynamically allocated
+descriptor fields.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit be4c9269a49894ea80a359f61e49f42518b314aa)
+
+bgpd: Add BGP-LS NLRI comparison functions
+
+Add functions to determine if two BGP-LS NLRIs are equal.
+
+These functions will be used in subsequent commits for NLRI hash table
+lookups and duplicate handling.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 3e6d2ddcd36108667f1e9c0230d5cae76fa04a79)
+
+bgpd: Add BGP-LS NLRI validation functions
+
+Add validation functions to verify BGP-LS NLRI is valid per RFC 9552.
+
+Used in subsequent commits to validate NLRIs received from peers.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 86e38b2cdec5b66df6a4698baf5ef8745a822ca6)
+
+bgpd: Add BGP-LS NLRI size calculation helper
+
+Add function to calculate wire format size of BGP-LS NLRI structures.
+
+Used in subsequent commits for NLRI encoding.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 280716cfbeb028b16602d596b091f55b82822339)
+
+bgpd: Add BGP-LS NLRI string conversion functions
+
+Add functions to convert BGP-LS enums to human-readable strings.
+
+Used for debugging output and display purposes.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 186ba5f1db7c20276ab1af4acb385f5cbadadb42)
+
+bgpd: Add BGP-LS NLRI deep copy function
+
+Add function to create deep copy of BGP-LS NLRI structures.
+
+Used in subsequent commits for NLRI storage and management.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit e96a272c5020c47c1e1fe88986f55b8197f9c8d2)
+
+bgpd: Add hash table to store BGP-LS NLRIs
+
+Add hash table infrastructure to store BGP-LS NLRIs originated by the
+local node and received from peers.
+
+- Add nlri_hash to bgp_ls structure for NLRI storage
+- Add hash functions to compute hash keys and compare NLRIs
+- Add bgp_ls_nlri_intern() to insert NLRI and increment reference count
+- Add bgp_ls_nlri_unintern() to remove NLRI and decrement reference count
+- Add bgp_ls_entry_lookup() to lookup NLRI in the hash table
+- Add bgp_ls_nlri_get() to return existing NLRI or create new entry
+- Integrate ID allocator to assign unique IDs when NLRI is interned
+
+NLRIs are reference counted and automatically freed when refcount
+reaches zero.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 81657bd198fd77a6e2af4eea941a2e05397fc2d1)
+
+bgpd: Add hash table to store BGP-LS Attributes
+
+Implement unified hash table for BGP-LS attribute interning and lookup.
+
+- Implement bgp_ls_attr_get(): Insert BGP-LS attribute into unified
+ hash table, returning existing entry if already present or creating
+ and inserting new entry
+- Implement bgp_ls_attr_lookup(): Search hash table for BGP-LS
+ attribute matching the provided attributes
+- Implement bgp_ls_attr_intern(): Perform attribute interning by
+ looking up attribute in hash table, incrementing reference counter
+ if found, storing pointer for later unintern
+- Implement bgp_ls_attr_unintern(): Decrement reference counter for
+ interned attribute and remove from hash table when counter reaches zero
+
+The unified hash table ensures that duplicate BGP-LS attributes across
+multiple paths and peers are deduplicated, reducing memory consumption
+and improving lookup performance for route decision making.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 8076ae20fac48f1be7815ef76cdfed763dc8fdc2)
+
+bgpd: Add error code for BGP-LS packet errors
+
+Add EC_BGP_LS_PACKET error code for BGP-LS NLRI and attribute packet
+parsing/encoding errors.
+
+This error code is used by BGP-LS decode functions to log warnings when
+malformed packets are received from peers, such as invalid TLV lengths,
+missing mandatory fields, or unknown NLRI types.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 5a49df37f5a333b934687183e11a9e4ed076b892)
+
+bgpd: Add BGP-LS NLRI encode functions
+
+Add functions to serialize BGP-LS NLRIs to wire format per RFC 9552.
+
+The encode functions convert in-memory NLRI structures to the TLV-based
+wire format used in MP_REACH_NLRI and MP_UNREACH_NLRI path attributes.
+
+- bgp_ls_encode_node_descriptor() - Encode Node Descriptors
+- bgp_ls_encode_link_descriptor() - Encode Link Descriptors
+- bgp_ls_encode_prefix_descriptor() - Encode Prefix Descriptors
+- bgp_ls_encode_node_nlri() - Encode Node NLRI
+- bgp_ls_encode_link_nlri() - Encode Link NLRI
+- bgp_ls_encode_prefix_nlri() - Encode IPv4/IPv6 Prefix NLRIs
+- bgp_ls_encode_nlri() - Top-level encoder for any BGP-LS NLRI
+- bgp_ls_encode_attr() - Encode BGP-LS Attributes (Type 29)
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 1ff2315405f719238d3bcb67f5625757c7bdf4c6)
+
+bgpd: Add BGP-LS NLRI decode functions
+
+Add functions to parse BGP-LS NLRIs from wire format per RFC 9552.
+
+The decode functions parse NLRIs received in MP_REACH_NLRI and
+MP_UNREACH_NLRI path attributes into in-memory structures.
+
+- bgp_ls_decode_node_descriptor() - Parse Node Descriptor
+- bgp_ls_decode_link_descriptor() - Parse Link Descriptor
+- bgp_ls_decode_prefix_descriptor() - Parse Prefix Descriptor
+- bgp_ls_decode_node_nlri() - Parse Node NLRI
+- bgp_ls_decode_link_nlri() - Parse Link NLRI
+- bgp_ls_decode_prefix_nlri() - Parse IPv4/IPv6 Prefix NLRI
+- bgp_ls_decode_nlri() - Top-level decoder for BGP-LS NLRI
+- bgp_ls_parse_attr() - Main attribute parser dispatcher
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 9a95368f300ef4ecfff5e514e1e288bea88e1ca8)
+
+bgpd: Add TED instance to BGP-LS data structure
+
+Add Traffic Engineering Database (TED) instance to BGP-LS structure to
+store link-state topology.
+
+In FRR's Link State architecture, IGP protocols (IS-IS, OSPF) act as
+producers that build link-state databases from protocol exchanges and
+send updates via ZAPI Opaque messages. BGP-LS acts as a consumer that
+synchronizes with IGPs and listens for updates, maintaining a TED with
+the topology information.
+
+The TED models network topology as a graph G(V, E) with:
+- Vertices (V): Network nodes from BGP-LS Node NLRIs
+- Edges (E): Unidirectional links from BGP-LS Link NLRIs
+- Subnets (P): Prefixes from BGP-LS Prefix NLRIs
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 337e6cd45bd100ab7be58aea80077e8365e01bd3)
+
+bgpd: Add helper functions to register linkstate db notifications
+
+Add helper functions to register and unregister BGP-LS for Link State
+database notifications from IGP producers.
+
+In FRR's Link State architecture, consumers must register with zebra to
+receive ZAPI Opaque Link State Update messages from producers (IS-IS, OSPF).
+These functions wrap the Link State API registration mechanism:
+
+- bgp_ls_register() - Register to receive LS updates from IGPs
+- bgp_ls_unregister() - Unregister from LS updates
+
+Registration enables BGP-LS to receive Node, Link, and Prefix updates
+when IGPs detect topology changes. This is required for BGP-LS to maintain
+an up-to-date TED and advertise topology to peers per RFC 9552.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 92404d7ae624f147c823251a7fc8797730257561)
+
+bgpd: Add BGP-LS Attribute support to attr structure
+
+Extend the core BGP attribute structure to support BGP-LS Attributes
+(RFC 9552 Section 5.3) by adding ls_attr field and updating all related
+attribute processing functions.
+
+Changes:
+- Add ls_attr field to struct attr to store BGP-LS Attribute TLVs
+- Update attrhash_key_make() to include ls_attr in attribute hash key
+- Update attrhash_cmp() to compare ls_attr when present
+- Extend bgp_attr_intern() to intern BGP-LS attributes
+- Extend bgp_attr_unintern() to properly deintern/free BGP-LS attributes
+
+This enables the core BGP attribute handling infrastructure to properly
+manage BGP-LS attributes through the standard attribute interning mechanism,
+ensuring reference counting and deduplication of BGP-LS attribute data
+across the RIB.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit c8acf7a2c75404e030b16f1326001152d60bfc46)
+
+bgpd: Add BGP-LS update and withdraw functions
+
+Implement BGP-LS route installation and withdrawal to enable processing
+of BGP-LS NLRIs received in BGP UPDATE messages.
+
+In BGP-LS, UPDATE messages contain BGP-LS NLRIs (not traditional prefixes).
+When a BGP UPDATE with BGP-LS NLRI is received, a bgp_dest is allocated
+to represent that NLRI location in the RIB and stored in the BGP RIB tree.
+The ls_nlri field in bgp_dest stores the BGP-LS NLRI at that destination.
+
+Changes:
+- Add bgp_ls_update() - Install or update BGP-LS NLRI in the BGP RIB
+ Called when a BGP UPDATE message contains a BGP-LS NLRI to add/modify
+ a BGP-LS route in the RIB
+- Add bgp_ls_withdraw() - Withdraw BGP-LS NLRI from the BGP RIB
+ Called when a BGP UPDATE message contains a withdrawn BGP-LS NLRI
+ to remove the BGP-LS route from the RIB
+- Add ls_nlri field to bgp_dest structure
+ Stores the BGP-LS NLRI at the destination in the RIB (used as the
+ destination key for BGP-LS routes)
+- Extend bgp_dest_unlock_node() cleanup logic
+ Properly free ls_nlri when destination is being freed from the RIB
+
+Behavior:
+- When a BGP UPDATE is received with BGP-LS NLRI, bgp_ls_update()
+ creates/updates a bgp_dest with that NLRI and installs it in the RIB
+- When a BGP UPDATE is received with withdrawn BGP-LS NLRI, bgp_ls_withdraw()
+ removes the bgp_dest containing that NLRI from the RIB
+- Proper resource cleanup ensures ls_nlri is freed when destinations
+ are removed from the RIB (bgp_dest_unlock_node)
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 31cffad03f28c33e9ac7294efe95005df967412c)
+
+bgpd: Add functions to parse updates received from linkstate db
+
+Add callback functions to parse and process Link State updates received
+from IGP producers via ZAPI Opaque messages.
+
+When IGPs (IS-IS, OSPF) detect topology changes, they send Link State
+Update messages through zebra to registered consumers. These functions
+handle the received messages:
+
+- bgp_ls_parse_update() - Parse ZAPI Opaque Link State messages
+- bgp_ls_process_node() - Process Node updates (ADD/UPDATE/DELETE)
+- bgp_ls_process_link() - Process Link updates (ADD/UPDATE/DELETE)
+- bgp_ls_process_prefix() - Process Prefix updates (ADD/UPDATE/DELETE)
+
+The functions use the Link State API to extract Node, Link, or Prefix
+information and update the TED accordingly. Updates trigger BGP-LS NLRI
+generation for advertisement to peers.
+
+This enables BGP-LS to maintain a synchronized TED reflecting current
+IGP topology and propagate changes to peers per RFC 9552.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit baeefede22fbceb7135ac23933bcc2f89670bf06)
+
+bgpd: Handle zebra opaque messages
+
+Add callback handler to process ZAPI Opaque messages from zebra containing
+Link State updates from IGP producers.
+
+Register bgp_zebra_opaque_msg_handler() as the callback for
+ZEBRA_OPAQUE_MESSAGE type in the zebra client handler table. The handler:
+
+- Decodes ZAPI Opaque message header using zclient_opaque_decode()
+- Routes Link State messages (LINK_STATE_SYNC, LINK_STATE_UPDATE) to
+ bgp_ls_process_linkstate_message() for TED processing
+- Logs warnings for unknown opaque message types
+
+This completes the communication path between IGP producers (IS-IS, OSPF)
+and BGP-LS consumer, enabling BGP-LS to receive topology updates and
+database synchronization messages via zebra's ZAPI Opaque mechanism.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 88ced6ec9eed41a5c371a07133ddaf7b5bd3cce4)
+
+lib: Add CLI node for BGP Link-State address family configuration
+
+Add BGP_LS_NODE to enum node_type in command.h to enable BGP Link-State
+address family configuration mode.
+
+This allows creating a dedicated configuration context for BGP-LS:
+
+ router bgp 65000
+ address-family link-state
+ <BGP-LS specific commands>
+ exit-address-family
+
+Follows the same pattern as other BGP address families (BGP_EVPN_NODE,
+BGP_SRV6_NODE).
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit a3e90577db3b3cdadf1aae1dc8238f09c5cba048)
+
+bgpd: Add BGP-LS address family CLI commands
+
+Install BGP_LS_NODE in the CLI node hierarchy and add commands to
+activate and configure BGP Link-State address family.
+
+New commands:
+- address-family link-state [link-state]
+ Enters BGP-LS configuration mode under router bgp
+
+- neighbor PEER activate
+ Activates BGP-LS address family for the specified neighbor
+
+Configuration example:
+ router bgp 65000
+ neighbor 192.0.2.1 remote-as 65001
+ address-family link-state
+ neighbor 192.0.2.1 activate
+ exit-address-family
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit d4f914528b784611f6526c213733806e774d3d20)
+
+bgpd: Add BGP-LS configuration write support
+
+Extend bgp_config_write() to serialize BGP Link-State address family
+configuration.
+
+This enables BGP-LS configuration to be:
+- Displayed via show running-config
+- Saved to startup-config (write memory)
+- Written to configuration files
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit a20dfd7c81622071ca5bafe96339051fa1abac5e)
+
+bgpd: Add show commands for BGP-LS NLRIs
+
+Add commands to display BGP Link-State NLRIs from the BGP RIB.
+
+New commands:
+- show bgp link-state link-state [json]
+ Display all BGP-LS routes with node/link/prefix NLRIs
+
+- show bgp link-state link-state NLRI [json]
+ Display specific BGP-LS route by NLRI identifier
+ NLRI format: [Type][ID][Protocol][Instance][AS][Area][Descriptors...]
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 5ccafe4b1c163ad83057cb30c249004be340120c)
+
+vtysh: Integrate BGP-LS node into vtysh
+
+Integrate BGP_LS_NODE into vtysh to enable link-state address family
+configuration.
+
+This allows users to configure BGP-LS through vtysh using:
+ router bgp 65000
+ address-family link-state
+ neighbor <peer> activate
+ exit-address-family
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 5251bf3aa4af17331bfc22e898fca9e626696aaa)
+
+bgpd: Register for linkstate db updates when peer is activated
+
+Register for Link State database updates when the first BGP-LS peer is
+activated, and unregister when the last peer is deactivated.
+
+Modify peer_activate() and peer_deactivate() to manage Link State
+database registration based on active BGP-LS peers:
+
+- When first peer is activated for link-state address family, call
+ bgp_ls_register() to subscribe to IGP topology updates from zebra
+- When last peer is deactivated, call bgp_ls_unregister() to stop
+ receiving updates
+
+This optimizes resource usage by only receiving IGP topology updates
+(from IS-IS, OSPF) when there are active BGP-LS peers that need to
+receive the information.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit c85fff38ea39ab708ae42e00f024f8de74451015)
+
+bgpd: Add BGP-LS capability display support
+
+Add display support for BGP-LS multi-protocol capability in BGP OPEN
+message output.
+
+Update bgp_capability_vty_out() to properly display BGP-LS capabilities:
+
+- Display "AFI BGP-LS" for AFI_BGP_LS (16388)
+- Display "SAFI BGP-LS" for SAFI_BGP_LS (71)
+- Add JSON output support for BGP-LS capabilities
+
+This allows users to verify BGP-LS capability negotiation between peers
+using "show bgp neighbor" commands, displaying both text and JSON output
+for link-state address family capability advertisement and reception.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit abccc105243cac9e7bcd94409b9135d04bdf3c19)
+
+bgpd: Add BGP-LS AFI/SAFI to negotiation functions
+
+Add BGP-LS capability checks to peer activity and address family
+negotiation functions.
+
+Update peer status and capability negotiation functions to include
+BGP-LS AFI/SAFI:
+
+- peer_active() - Check if peer has BGP-LS configured
+- peer_active_nego() - Check if BGP-LS was negotiated with peer
+- peer_group_af_configured() - Check if peer group has BGP-LS enabled
+- bgp_open_option_parse() - Validate BGP-LS in capability negotiation
+
+This ensures BGP sessions with link-state address family are properly
+recognized as active and that capability negotiation correctly validates
+BGP-LS multi-protocol extensions.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 50e92bbb7f4660d427c2a5243f5b54acd50cb4d2)
+
+bgpd: Encode BGP-LS NLRI and attribute when building UPDATE packet
+
+Add BGP-LS encoding support to UPDATE message generation for
+transmitting Link-State topology information to peers.
+
+Implement bgp_packet_ls_attribute() to encode BGP-LS Attribute (Type 29)
+by dispatching to type-specific encoders (node/link/prefix attributes).
+
+Extend packet building functions to accept ls_nlri parameter:
+- bgp_packet_attribute() - encode BGP-LS attribute when present
+- bgp_packet_mpattr_prefix() - call bgp_ls_encode_nlri() for SAFI_BGP_LS
+- bgp_packet_mpunreach_prefix() - handle BGP-LS withdrawals
+
+Update subgroup_update_packet() and subgroup_withdraw_packet() to extract
+ls_nlri from path_info->extra and pass to encoding functions.
+
+This enables BGP-LS routes to be advertised and withdrawn using the
+RFC 9552 wire format.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit bfb67d5364a18542606299e753c5696f096caf8e)
+
+bgpd: Receive BGP-LS attribute
+
+Add bgp_attr_ls() function to parse and intern BGP-LS attributes (Type 29)
+from received BGP update messages. The function validates peer configuration,
+parses Type 29 TLVs, and interns the resulting attribute for use in route
+processing.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit aa7c9b97f20069f33e7efad501f8ee0ceb1929a9)
+
+bgpd: Unregister from lsdb on termination
+
+Call bgp_ls_unregister() in bgp_ls_terminate() to cleanly unregister
+from the zebra link-state database when BGP instance terminates.
+
+This ensures proper cleanup and stops receiving IGP topology updates
+when the BGP-LS functionality is no longer needed.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit d70169ee48fc55ea8fe81d5c04ea1d81994b56fc)
+
+bgpd: Parse BGP-LS NLRI received from peers
+
+Add inbound BGP-LS message processing to receive Link-State topology
+information from BGP peers.
+
+When BGP receives a BGP UPDATE message from a peer, it calls
+bgp_nlri_parse() to parse the NLRI. The bgp_nlri_parse() function
+checks the SAFI (Subsequent Address Family Identifier) contained in
+the UPDATE message and dispatches to the appropriate SAFI-specific
+handler. Currently supported SAFIs include:
+- SAFI_UNICAST, SAFI_MULTICAST, SAFI_LABELED_UNICAST → bgp_nlri_parse_ip()
+- SAFI_MPLS_VPN → bgp_nlri_parse_vpn()
+- SAFI_ENCAP → bgp_nlri_parse_encap()
+- SAFI_EVPN → bgp_nlri_parse_evpn()
+- SAFI_FLOWSPEC → bgp_nlri_parse_flowspec()
+
+This commit extends bgp_nlri_parse() to handle SAFI_BGP_LS and
+dispatch to a new handler bgp_nlri_parse_ls().
+
+The bgp_nlri_parse_ls() function performs the following operations:
+- Decodes BGP-LS NLRIs from MP_REACH_NLRI and MP_UNREACH_NLRI
+ attributes using bgp_ls_decode_nlri()
+- Interns decoded NLRI into hash table via bgp_ls_nlri_get()
+- Creates synthetic prefix (AF_UNSPEC) with NLRI ID for RIB storage
+- Calls bgp_update() for route advertisements or bgp_withdraw() for
+ route removals to install/remove BGP-LS routes in the RIB
+
+This completes the inbound UPDATE message processing path, enabling
+BGP to receive and store Link-State topology from peers per RFC 9552.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 1da3f4420cbe9f95c4a0468db848b0a9d19b0903)
+
+bgpd: Set nexthop for BGP-LS based on peer address family
+
+BGP-LS routes advertised to peers require a nexthop with the same
+address family as the peer session (IPv4 peer needs IPv4 nexthop,
+IPv6 peer needs IPv6 nexthop).
+
+The MP_REACH_NLRI and MP_UNREACH_NLRI encoding code in bgp_attr does
+not have direct knowledge of the peer's address family, making it
+unable to select the appropriate nexthop address family for BGP-LS
+advertisements.
+
+Add peer flags PEER_FLAG_BGP_LS_IPV4 and PEER_FLAG_BGP_LS_IPV6 that
+are set during peer activation for BGP-LS address family. The bgp_attr
+encoding functions check these flags to determine whether to encode
+IPv4 or IPv6 nexthop addresses in MP_REACH_NLRI attributes.
+
+This ensures BGP-LS routes are advertised with nexthops matching the
+peer session's address family, allowing proper route exchange with
+both IPv4 and IPv6 BGP-LS peers.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 399c64b834ac488795d303057b8ab5cce5125642)
+
+tests: Add BGP-LS topotest suite
+
+Add pytest topology test for BGP Link-State covering producer and
+consumer modes with ISIS IGP backend.
+
+Tests BGP-LS capability negotiation, NLRI advertisement/reception
+(Node, Link, Prefix), and dynamic topology change propagation
+including route additions/removals and link state changes.
+
+Verifies BGP-LS updates flow correctly from IGP through BGP-LS
+producer to BGP-LS consumer.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 78deb305498ff4cf86fa5063a985f157487eb458)
+
+doc: Add documentation for BGP-LS
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 3a97a2faf9c9886873dc1a1dd23aa413667944ea)
+
+Fix compilation issues
+
+bgpd: Fix integer truncation of SRLG count when parsing SRLG TLV
+
+`count` is declared as `uint8_t` but receives `length / 4` where
+`length` is a `uint16_t`. For `length >= 1024`, the division result
+exceeds 255, which cannot be stored in `count` since `uint8_t` holds
+at most 255, and the value silently truncates. The result is then
+stored in `attr->srlg_count`, also `uint8_t`, which would re-truncate
+it.
+
+Widen `count` in `parse_srlg()` and `srlg_count` in `struct
+bgp_ls_attr` to `uint16_t`.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit ef8a4c68cb7bc7a8dc3dc63d81ffefc53823ba77)
+
+bgpd: Fix integer truncation of count when parsing Route Tag TLV
+
+`count` is declared as `uint8_t` but receives `length / 4` where
+`length` is a `uint16_t`. For `length >= 1024`, the division result
+exceeds 255, which cannot be stored in `count` since `uint8_t` holds
+at most 255, and the value silently truncates. The result is then
+stored in `attr->route_tag_count`, also `uint8_t`, which would
+re-truncate it.
+
+Widen `count` in `parse_route_tag()` and `route_tag_count` in `struct
+bgp_ls_attr` to `uint16_t`.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 83b9a4fb8b0b9168465bf375f78a52eeea15d0b0)
+
+bgpd: Treat duplicate TLVs as malformed NLRI
+
+BGP receives BGP Update messages that can contain a BGP-LS NLRI, then
+processes the TLVs carried in that NLRI. The NLRI may include duplicate
+TLVs. Today, when a duplicate TLV is received, BGP-LS overwrites the
+previous value with the new one.
+
+RFC 9552 requires duplicate TLVs to be treated as a malformed NLRI.
+Also, replacing an earlier TLV value with a new one without freeing
+previously allocated dynamic memory can cause a memory leak.
+
+Fix by checking whether the same TLV type appears more than once and
+returning an error on duplicates. This propagates the parse failure to
+the caller, which then treats the NLRI as malformed.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 3098d5f03e51760c59da71c5d27ee0b3ab9b7e19)
+
+bgpd: Check stream space before encoding BGP-LS TLV data
+
+Some encode paths wrote to the stream without first checking that enough
+space was available.
+
+Writing when the stream is full can trigger an assert or crash.
+
+Fix by adding `STREAM_WRITEABLE()` checks before writing, and return -1
+when space is insufficient.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit f02173760f17d350e1a26dc671fd89d1773b1d08)
+
+bgpd: Bound Extended Admin Group TLV decode size
+
+parse_ext_admin_group() only verified that the TLV length was a multiple
+of 4, with no upper bound on the number of 32-bit words to decode.
+A very large length could trigger excessive growth/work in
+admin_group_bulk_set().
+
+Define BGP_LS_MAX_EXT_ADMIN_GROUP_WORDS in bgp_ls_nlri.h and reject
+Extended Admin Group TLVs that exceed this limit during decode.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit ff2ab26af4e04b066e1f1be41f58fba6bd74f957)
+
+bgpd: Fix Extended Admin Group decode byte order
+
+`parse_ext_admin_group()` applied `ntohl()` to the value returned by
+`stream_getl()`. `stream_getl()` already returns the 32-bit value in host
+byte order, so this caused a second conversion.
+
+On little-endian systems, the extra conversion byte-swapped the value
+again and corrupted decoded Extended Admin Group words.
+
+Fix by removing the redundant conversion and passing `stream_getl()`
+output directly to `admin_group_bulk_set()`.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit d57a9ed3087158f896f825ee57bb0883e89eb9ba)
+
+bgpd: Fix leak in BGP-LS NLRI parsing loop
+
+In `bgp_nlri_parse_ls()`, `bgp_ls_nlri` was declared on the stack and then
+filled by `bgp_ls_decode_nlri()` from the input stream.
+
+`bgp_ls_decode_nlri()` can allocate dynamic fields inside the NLRI object.
+Because the parsing loop reused the same stack object on each iteration,
+those dynamic allocations were leaked across iterations.
+
+Fix by moving the NLRI object to heap allocation and using the BGP-LS NLRI
+helpers for lifecycle management: allocate with `bgp_ls_nlri_alloc()` and
+free with `bgp_ls_nlri_free()` on both success and error paths.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit fa0eebc6c87091616336b2e8afc442d33425a32f)
+
+bgpd: Free descriptor MT-ID on BGP-LS parse errors
+
+bgp_ls_decode_link_descriptor() and bgp_ls_decode_prefix_descriptor()
+can allocate desc->mt_id when parsing MT-ID TLVs.
+
+If a later TLV in the same descriptor fails validation, the functions
+returned -1 without releasing that allocation, leaking memory on error
+unwind.
+
+Fix by adding centralized error cleanup paths in both descriptor parsers
+that free desc->mt_id before returning -1.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit a773e278a2bc61e85d290f9ff5f4bcf8528c40cb)
+
+bgpd: Use extended length for BGP-LS path attribute
+
+bgp_packet_ls_attribute() encoded BGP-LS attributes with a 1-byte
+length field, which can encode up to 255 bytes of BGP-LS attribute data.
+
+BGP-LS attributes often exceed 255 bytes when carrying multiple
+node/link/prefix TLVs, so a 1-byte length is not sufficient.
+
+Fix by setting BGP_ATTR_FLAG_EXTLEN for BGP-LS attributes and
+encoding the attribute length as 2 bytes. Add a bounds check to
+ensure the encoded attribute length fits in the extended-length field.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit a8b23e3df8e2966e424bbc3ceb1939ed063f1778)
+
+bgpd: Guard BGP-LS NLRI parsing when `ls_info` is unavailable
+
+`bgp_nlri_parse_ls()` dereferenced `peer->bgp->ls_info` without checking
+that BGP-LS state was initialized.
+
+Add defensive checks for peer, peer->bgp, peer->bgp->ls_info, and
+packet before parsing, and return BGP_NLRI_PARSE_ERROR on invalid state.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 2e533d3d1310a2516e8366633cb526ec396a49ae)
+
+bgpd: Fix CID 1670210 remove dead code in `bgp_ls_process_edge`
+
+`bgp_ls_process_edge()` builds a Link NLRI from an edge, which has both a
+source node and a destination node.
+
+Before this change, edge processing was split into two switch blocks:
+the first switch populated source (local) router-id fields, and the second
+switch populated destination (remote) router-id fields.
+
+Coverity reported DEADCODE in the second switch, where several protocol
+cases were flagged as unreachable.
+
+Simplify the logic by merging source and destination handling into a single
+switch statement. This removes unreachable branches, keeps behavior unchanged,
+and fixes CID 1670210.
+
+** CID 1670210: (DEADCODE)
+/bgpd/bgp_ls_ted.c: 1030 in bgp_ls_process_edge()
+/bgpd/bgp_ls_ted.c: 1027 in bgp_ls_process_edge()
+/bgpd/bgp_ls_ted.c: 1029 in bgp_ls_process_edge()
+/bgpd/bgp_ls_ted.c: 1028 in bgp_ls_process_edge()
+/bgpd/bgp_ls_ted.c: 1031 in bgp_ls_process_edge()
+
+_____________________________________________________________________________________________
+*** CID 1670210: (DEADCODE)
+/bgpd/bgp_ls_ted.c: 1030 in bgp_ls_process_edge()
+1024 // router_id_len = 7;
+1025 remote_router_id_len = 6;
+1026 break;
+1027 case BGP_LS_PROTO_DIRECT:
+1028 case BGP_LS_PROTO_STATIC:
+1029 case BGP_LS_PROTO_OSPFV3:
+>>> CID 1670210: (DEADCODE)
+>>> Execution cannot reach this statement: "case BGP_LS_PROTO_BGP:".
+1030 case BGP_LS_PROTO_BGP:
+1031 case BGP_LS_PROTO_RESERVED:
+1032 zlog_err("BGP-LS: Unsupported protocol %u", protocol_id);
+1033 return -1;
+1034 }
+1035
+/bgpd/bgp_ls_ted.c: 1027 in bgp_ls_process_edge()
+1021 memcpy(remote_router_id, edge->destination->node->adv.id.iso.sys_id,
+1022 ISO_SYS_ID_LEN);
+1023 // router_id[6] = 0; /* Pseudonode ID = 0 for router */
+1024 // router_id_len = 7;
+1025 remote_router_id_len = 6;
+1026 break;
+>>> CID 1670210: (DEADCODE)
+>>> Execution cannot reach this statement: "case BGP_LS_PROTO_DIRECT:".
+1027 case BGP_LS_PROTO_DIRECT:
+1028 case BGP_LS_PROTO_STATIC:
+1029 case BGP_LS_PROTO_OSPFV3:
+1030 case BGP_LS_PROTO_BGP:
+1031 case BGP_LS_PROTO_RESERVED:
+1032 zlog_err("BGP-LS: Unsupported protocol %u", protocol_id);
+/bgpd/bgp_ls_ted.c: 1029 in bgp_ls_process_edge()
+1023 // router_id[6] = 0; /* Pseudonode ID = 0 for router */
+1024 // router_id_len = 7;
+1025 remote_router_id_len = 6;
+1026 break;
+1027 case BGP_LS_PROTO_DIRECT:
+1028 case BGP_LS_PROTO_STATIC:
+>>> CID 1670210: (DEADCODE)
+>>> Execution cannot reach this statement: "case BGP_LS_PROTO_OSPFV3:".
+1029 case BGP_LS_PROTO_OSPFV3:
+1030 case BGP_LS_PROTO_BGP:
+1031 case BGP_LS_PROTO_RESERVED:
+1032 zlog_err("BGP-LS: Unsupported protocol %u", protocol_id);
+1033 return -1;
+1034 }
+/bgpd/bgp_ls_ted.c: 1028 in bgp_ls_process_edge()
+1022 ISO_SYS_ID_LEN);
+1023 // router_id[6] = 0; /* Pseudonode ID = 0 for router */
+1024 // router_id_len = 7;
+1025 remote_router_id_len = 6;
+1026 break;
+1027 case BGP_LS_PROTO_DIRECT:
+>>> CID 1670210: (DEADCODE)
+>>> Execution cannot reach this statement: "case BGP_LS_PROTO_STATIC:".
+1028 case BGP_LS_PROTO_STATIC:
+1029 case BGP_LS_PROTO_OSPFV3:
+1030 case BGP_LS_PROTO_BGP:
+1031 case BGP_LS_PROTO_RESERVED:
+1032 zlog_err("BGP-LS: Unsupported protocol %u", protocol_id);
+1033 return -1;
+/bgpd/bgp_ls_ted.c: 1031 in bgp_ls_process_edge()
+1025 remote_router_id_len = 6;
+1026 break;
+1027 case BGP_LS_PROTO_DIRECT:
+1028 case BGP_LS_PROTO_STATIC:
+1029 case BGP_LS_PROTO_OSPFV3:
+1030 case BGP_LS_PROTO_BGP:
+>>> CID 1670210: (DEADCODE)
+>>> Execution cannot reach this statement: "case BGP_LS_PROTO_RESERVED:".
+1031 case BGP_LS_PROTO_RESERVED:
+1032 zlog_err("BGP-LS: Unsupported protocol %u", protocol_id);
+1033 return -1;
+1034 }
+1035
+1036 switch (event) {
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 48690880f6386edf56e919f2ff9b52d3adc1f865)
+
+bgpd: Fix IGP Router-ID buffer overflow in BGP-LS node descriptor
+
+RFC 9552 defines the IGP Router-ID as variable length from 4 to 16 bytes.
+Right now, igp_router_id is defined as an 8-byte array in the
+bgp_ls_node_descriptor data structure.
+
+This can cause an overflow when decoding a valid IGP Router-ID TLV with
+length greater than 8, because the parser accepts up to 16 bytes and then
+copies up to 16 bytes into the fixed 8-byte buffer.
+
+Fix by sizing igp_router_id to BGP_LS_IGP_ROUTER_ID_MAX_SIZE (16 bytes),
+so any RFC-valid IGP Router-ID length fits safely and decoding cannot
+write past the end of the descriptor buffer.
+
+Fix CID 1670214, 1670212, 1670211, 1670208, 1670207, 1670206, 1670205, 1670204.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 36cd87dc13c6193d3d2ddaeb6c91fe884e4a2807)
+
+bgpd: Fix null dereference when `bgp->ls_info` is NULL
+
+`bgp_ls_register()` calls `bgp_ls_is_registered()` as an early-return
+guard, but that function returns false when `bgp->ls_info` is NULL.
+The code then proceeds to dereference `bgp->ls_info->registered_ls_db`
+unconditionally, causing a crash.
+
+Add explicit null guards at the top of `bgp_ls_register()`,
+`bgp_ls_unregister()`, and `bgp_ls_cleanup()` to return early when
+`bgp->ls_info` is not initialized.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit e310588e9918fcb3707f33e3ef7067b4969e9e0d)
+
+bgpd: Fix memory leak in `bgp_ls_originate_node()`
+
+In `bgp_ls_originate_node()`, `ls_attr` is allocated with
+`bgp_ls_attr_alloc()` and populated before being passed to
+`bgp_ls_update()`. If `bgp_ls_update()` fails, the function
+returns -1 without freeing `ls_attr`, causing a memory leak.
+
+Free `ls_attr` before returning on the `bgp_ls_update()` failure
+path.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit ef1da17f6dc78cabf5a26502b6fff93525a26651)
+
+bgpd: Align `bgp_ls_populate_prefix_attr()` with node/link pattern
+
+`bgp_ls_populate_prefix_attr()` used a double-pointer `**attr` and
+allocated `ls_attr` internally, unlike `bgp_ls_populate_node_attr()`
+and `bgp_ls_populate_link_attr()` which receive a pre-allocated
+`*attr` from the caller.
+
+The internal `!encoded` path also had a bug: `attr = NULL` set the
+local variable instead of `*attr`, leaving the caller with a dangling
+pointer to freed memory.
+
+Refactor `bgp_ls_populate_prefix_attr()` to take a single `*attr`
+pointer, removing the internal `bgp_ls_attr_alloc()` call and the
+`encoded`/free logic. Update `bgp_ls_originate_prefix()` to
+pre-allocate `ls_attr` with `bgp_ls_attr_alloc()` before calling
+the function, consistent with how node and link attributes are
+handled.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 1a454f66cd811831f0c3c65b52108c442881595a)
+
+bgpd: Remove duplicate log calls in `bgp_ls_withdraw_node()`
+
+`bgp_ls_withdraw_node()` called `zlog_err()` immediately followed by
+`flog_err()` with identical messages on two error paths. `flog_err()`
+already logs to the standard output, making the `zlog_err()` calls
+redundant. Remove the duplicates.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit d0c1911106b8304d289a3b122b940ea89606e986)
+
+bgpd: Demote unknown opaque message warning to debug in opaque handler
+
+BGP registers only for `LINK_STATE_SYNC` and `LINK_STATE_UPDATE`
+opaque messages, but other daemons may send opaque messages on the
+same channel for their own purposes. The `default` case in
+`bgp_zebra_opaque_msg_handler()` emitted a `zlog_warn()` for every
+such message, which would be noisy and gives the operator nothing to
+act on.
+
+Demote to a debug log guarded by `BGP_DEBUG(zebra, ZEBRA)`.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit f232a7fbd18375318022051d68e978c7b6710684)
+
+bgpd: Fix `edge->destination` null deref in `bgp_ls_withdraw_link()`
+
+`bgp_ls_withdraw_link()` dereferenced `edge->destination->node` to
+read the remote AS number, but the guard only checked
+`edge->attributes` — it did not verify that `edge->destination` or
+`edge->destination->node` were non-NULL, leading to a potential null
+dereference.
+
+Add the missing null checks on `edge->destination` and
+`edge->destination->node`, consistent with the guard used for the
+same field in `bgp_ls_originate_link()`.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit d649b691938ab08885e8642f018930595ff09816)
+
+bgpd: Use `bgp_node_lookup()` in `bgp_ls_withdraw()`
+
+`bgp_ls_withdraw()` was calling `bgp_afi_node_get()` to locate the RIB
+destination before marking the route as withdrawn. `bgp_afi_node_get()`
+creates a new RIB node when none exists, so a withdraw for an NLRI that
+was never installed silently created a phantom RIB entry with no path
+info attached, wasting memory and polluting the table.
+
+Replace the call with `bgp_node_lookup()`, which is a pure read-only
+lookup that returns NULL when the prefix is not present. Add an early
+return with a debug-guarded log message for the not-found case so the
+condition is visible under `debug bgp linkstate` without being noisy in
+production.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit bfc0419434afb230ea1c23e307071d076d94c5c7)
+
+bgpd: Unintern old NLRI reference in `bgp_ls_update()`
+
+`bgp_afi_node_get()` returns an existing RIB node when the prefix is
+already present. In that case `dest->ls_nlri` already holds an
+interned pointer. Unconditionally overwriting it with the new value
+dropped the old reference without calling `bgp_ls_nlri_unintern()`,
+leaking the previous allocation.
+
+Unintern the existing pointer before installing the new one so the
+refcount is kept accurate in both the new-node and update paths.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit a24ac728e9173589a11bd975ac3f00aa535555e0)
+
+bgpd: Clear `registered_ls_db` before calling `ls_unregister()`
+
+`bgp_ls_unregister()` only cleared `registered_ls_db` after a
+successful `ls_unregister()` call. When `ls_unregister()` failed, the
+flag was left as true, making `bgp_ls_is_registered()` report "still
+registered". Any subsequent call to `bgp_ls_register()` would then
+return early thinking registration was already in place, leaving BGP
+permanently unable to receive link-state updates from zebra.
+
+Fix this by clearing `registered_ls_db` before the `ls_unregister()`
+call. If the zebra unregister fails, the flag is already false so
+`bgp_ls_register()` can attempt re-registration and recover. The
+`false` return value is preserved to signal the zebra-side failure to
+the caller.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 5f118f0dca9cc9ae7ceba20ac143b4e143af83b7)
+
+bgpd: Fix use-after-free in BGP-LS node origination
+
+`bgp_ls_originate_node()` could free `ls_attr` after `bgp_ls_populate_node_attr()`
+failure, then continue and pass the freed pointer to `bgp_ls_update()`.
+
+Fix by returning immediately after `bgp_ls_attr_free(ls_attr)` on
+populate failure.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 06036a10e40ccc3e50d23507e1679c4ecce553de)
+
+bgpd: Include local-node ASN in link withdraw NLRI
+
+A Link NLRI contains two nodes: local node and remote node.
+Per RFC 9552, each node is identified by ASN, OSPF Area ID, and
+IGP Router ID.
+
+For the remote node, `bgp_ls_withdraw_link` sets ASN, OSPF Area ID, and
+IGP Router ID when generating the NLRI.
+
+For the local node, `bgp_ls_withdraw_link` sets only OSPF Area ID and
+IGP Router ID when generating the NLRI.
+
+Add ASN for the local node as well, to keep symmetry with the remote
+node and remain compliant with RFC 9552.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 46c3d1c85275cd48fbe82b79e0e8a3fd8a285c08)
+
+bgpd: Require valid TED objects in BGP-LS originate/withdraw APIs
+
+BGP-LS node/link/prefix originate and withdraw handlers are expected to
+receive valid TED objects.
+
+Add explicit checks at the beginning of each function and return early on
+invalid inputs, before any further processing.
+
+This makes the API contract clear, avoids NULL dereferences, and keeps the
+originate/withdraw paths consistent.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 6e2323aabe65888274f0f2a7ad7de3c6805a2c69)
+
+bgpd: Fix missing present_tlvs bit for Link ID in link NLRI
+
+When originating/withdrawing a Link NLRI, link_remote_id is filled in
+the bgp_ls_nlri structure but BGP_LS_LINK_DESC_LINK_ID_BIT is not set
+in link_desc.present_tlvs.
+
+Fix by setting BGP_LS_LINK_DESC_LINK_ID_BIT in both bgp_ls_originate_link()
+and bgp_ls_withdraw_link().
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 8fbdb4fa767a79c7cb3b0fb984df21f1ef8a8dfb)
+
+bgpd: Fix destination linkage for delayed reverse-edge updates
+
+An edge A->B can arrive before reverse edge B->A is present in TED.
+When that happens, A->B may not have a destination yet and link
+origination for that direction is deferred.
+
+Today, when the reverse edge arrives, the direct edge is not updated
+in TED and can remain without a destination.
+
+As a result, when bgp_ls_process_edge tries to generate BGP-LS
+NLRI, generation fails because the edge still has no destination.
+
+Fix this by looking up the reverse edge and completing missing
+destination linkage in both directions before processing.
+
+This follows the same logic used in ISIS TED edge handling.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 350933411c82c51e857e5712f36949bf6aecec2c)
+
+bgpd: show BGP-LS attributes in json output
+
+show bgp link-state link-state NLRI json didn't show any BGP-LS
+attributes.
+
+* Add `bgp_ls_attr_to_json` that converts `bgp_ls_attr` to json_object
+* Call it from `route_vty_out_detail` and attach to output
+* Small fix in `bgp_ls_attr_display`: show "Packet Loss" in %
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit a2576fd6b0191095bf8c1821138c4d8ceb77c6f0)
+
+bgpd: BGP-LS: add Prefix SID (TLV 1158)
+
+ADD TLV 1158 Prefix SID to BGP-LS: RFC 9085 Section 2.3.1
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit e67b6c7a25aa229b0bda7fdeedebf9e26b07df21)
+
+tests: BGP-LS Prefix SID attribute
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit 500018fb32d1a14e0ca7121bb0653e149b168eab)
+
+bgpd: Fix incorrect comparisons in BGP-LS *_cmp() functions
+
+Comparison functions in bgp_ls_nlri.c return (a - b) on unsigned
+integer fields. The unsigned subtraction result is then implicitly
+converted to int (the return type). When the difference exceeds
+INT_MAX the sign flips, inverting the result:
+
+ a = 0xFFFFFFFF, b = 0
+ (uint32_t)a - b = 0xFFFFFFFF -> cast to int gives -1
+ -> caller sees a < b, which is wrong
+
+These functions are expected to return a negative value when a < b,
+zero when a == b, and a positive value when a > b. Currently the
+top-level functions bgp_ls_nlri_cmp() and bgp_ls_attr_cmp() are
+registered as hash comparison functions (DECLARE_HASH), which only
+tests them for equality (== 0). All other cmp functions are called
+exclusively from within those two, so in practice the sign is never
+observed. However, a future caller relying on the sign to determine
+ordering would silently get incorrect results.
+
+Replace all subtraction-based comparisons with numcmp(a, b), which
+correctly implements the three-way comparator contract.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit a462e15977c761860d03a239644bc3b4825ec41f)
+
+bgpd: Request initial TED sync on link-state registration
+
+After registering with the LS database, no initial sync is requested,
+so the TED remains empty until the IGP sends unsolicited updates.
+Any topology changes that occurred before registration are
+permanently missed and never originated as BGP-LS NLRIs.
+
+Additionally, LS_MSG_EVENT_SYNC messages are not handled in the TED
+processors, so any sync response from zebra is silently dropped.
+
+Request a sync via ls_request_sync() immediately after registration,
+following the same pattern used by other TED consumers such as pathd.
+Handle LS_MSG_EVENT_SYNC alongside LS_MSG_EVENT_ADD in
+bgp_ls_process_vertex(), bgp_ls_process_edge(), and
+bgp_ls_process_subnet().
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 13cc5be80825d3087873dbfe4e5b721983e30372)
+
+bgpd: Withdraw BGP-LS routes and reset TED on last peer deactivation
+
+When the last BGP-LS peer is deactivated, locally originated BGP-LS
+routes are not withdrawn from the RIB, leaving stale routes on peers.
+The TED is also not cleared, so the next registration re-originates
+on top of stale state.
+
+Add bgp_ls_withdraw_ted() which removes all self-originated paths via
+bgp_clear_route() and clears all TED entries. Call it in
+peer_deactivate() when the last BGP-LS peer is being deactivated,
+before unregistering from the LS database.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 906445760f177df1850bef6977b7ecbe47df8391)
+
+tests: Verify BGP-LS routes withdrawn on peer deactivate
+
+Add test_bgp_ls_peer_deactivate() to verify that deactivating the
+last BGP-LS peer on r2 withdraws all locally originated routes on
+r2 and clears all received routes on rr.
+
+Add test_bgp_ls_peer_reactivate() to verify that reactivating the
+peer triggers a fresh TED sync, re-originates all BGP-LS NLRIs on
+r2, and re-advertises them to rr.
+
+Signed-off-by: Carmine Scarpitta <cscarpit@cisco.com>
+(cherry picked from commit 431b6814721723fe02c04c022d6c65d6e6656deb)
+
+bgpd: BGP-LS Fix bgp_ls_attr_prefix_sid_len
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit e34b0cf424bf21b2911fe43bae17ec2f64988492)
+
+bgpd: BGP-LS: add SR Capabilities (TLV 1034)
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit 349679c2d17f9d1c90b960fc25f98718e18ee113)
+
+tests: BGP-LS SR Capabilities (TLV 1034)
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit 8b8c198300a4cc5a89bc087235778c3651f803e7)
+
+bgpd: BGP-LS: add Node MSD (TLV 266)
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit b33f7f4cd36ef0ec8044a514bf09a527ab99a322)
+
+tests: BGP-LS add Node MSD (TLV 266)
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit cfe13bb9fb9e19f051fbcfb2076c0ef4d6574a67)
+
+isis, lib: add isis area id to ls_node
+
+It is needed for BGP-LS IS-IS Area Identifier (TLV 1027)
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit 19adc1a2f1fe180fcab37be672e31ed890b97d00)
+
+bgpd: BGP-LS: update IS-IS Area Identifier (TLV 1027)
+
+* Console output (bgp_ls_attr_display)
+* JSON output (bgp_ls_attr_to_json)
+* Fill in from TED (bgp_ls_populate_node_attr)
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit 1bb63fabb913d7d9c92ec74322fefa907ec6a5bf)
+
+tests: BGP-LS IS-IS Area Identifier (TLV 1027)
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit a23737163f808ab25fc0f9ac06266bb43f971c90)
+
+bgpd: BGP-LS: simplify bgp_ls_attr_to_json
+
+Simplify bgp_ls_attr_to_json using json_object_string_addf
+instead of snprintfrr + json_object_string_add
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit b4c5646d4cbb0caaae4185414a6ab30c518175ac)
+
+bgpd: BGP-LS: add Adjacency SID (TLV 1099)
+
+To properly encode/decode `Adjacency SID` protocol id must be known
+as length of SID depends on flags V and L that have different bit
+positions for IS-IS and OSPF.
+
+This commit adds `protocol_id` argument to `bgp_ls_encode_attr`
+and `bgp_packet_ls_attribute`.
+
+During run of `bgp_ls_parse_attr` NLRI is not decoded yet, so instead of
+adding complex logic of pre-parsing NLRI and getting protocol id out of
+it function `bgp_ls_attr_process_with_protocol_id` is added. It is
+called after parsing of attributes and after protocol id is parsed in
+NLRI.
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit 3df60175a487148f79dc2a2985001893412d85f2)
+
+tests: BGP-LS Adjacency SID (TLV 1099)
+
+Signed-off-by: Kyrylo Yatsenko <hedrok@gmail.com>
+(cherry picked from commit 7461a4a3aee80bbeb7524de164481102469af7da)
+
+isisd: convert area_addrs from linked list to typesafe DLIST
+
+Convert the isis_area->area_addrs field from the legacy struct list *
+to the FRR typesafe DLIST container. This change is part of the ongoing
+effort to migrate IS-IS data structures to typesafe containers for
+improved type safety and code clarity.
+
+Signed-off-by: Vincent Jardin <vjardin@free.fr>
+(cherry picked from commit e62419b8940ce25233386630d7ecd1311404a575)
+---
+ bgpd/bgp_attr.c | 139 +-
+ bgpd/bgp_attr.h | 18 +-
+ bgpd/bgp_bmp.c | 8 +-
+ bgpd/bgp_debug.c | 52 +
+ bgpd/bgp_debug.h | 3 +
+ bgpd/bgp_errors.c | 6 +
+ bgpd/bgp_errors.h | 1 +
+ bgpd/bgp_ls.c | 878 +++
+ bgpd/bgp_ls.h | 86 +
+ bgpd/bgp_ls_nlri.c | 5406 +++++++++++++++++
+ bgpd/bgp_ls_nlri.h | 895 +++
+ bgpd/bgp_ls_ted.c | 1403 +++++
+ bgpd/bgp_ls_ted.h | 226 +
+ bgpd/bgp_open.c | 19 +-
+ bgpd/bgp_packet.c | 3 +
+ bgpd/bgp_route.c | 178 +-
+ bgpd/bgp_route.h | 10 +
+ bgpd/bgp_table.c | 8 +
+ bgpd/bgp_table.h | 2 +
+ bgpd/bgp_updgrp.c | 4 +
+ bgpd/bgp_updgrp_packet.c | 116 +-
+ bgpd/bgp_vty.c | 70 +-
+ bgpd/bgp_zebra.c | 34 +-
+ bgpd/bgpd.c | 76 +-
+ bgpd/bgpd.h | 29 +-
+ bgpd/rfapi/rfapi_import.c | 5 +
+ bgpd/rfapi/rfapi_monitor.c | 2 +
+ bgpd/subdir.am | 6 +
+ doc/user/bgp-linkstate.rst | 207 +
+ doc/user/bgp.rst | 2 +
+ isisd/isis_lsp.c | 2 +-
+ isisd/isis_nb_config.c | 28 +-
+ isisd/isis_pdu.c | 16 +-
+ isisd/isis_snmp.c | 6 +-
+ isisd/isis_te.c | 11 +
+ isisd/isis_tlvs.c | 22 +-
+ isisd/isis_tlvs.h | 17 +-
+ isisd/isis_zebra.c | 2 +-
+ isisd/isisd.c | 56 +-
+ isisd/isisd.h | 2 +-
+ lib/command.h | 1 +
+ lib/iana_afi.h | 10 +
+ lib/iso.h | 10 +
+ lib/link_state.c | 34 +
+ lib/link_state.h | 4 +
+ lib/prefix.c | 6 +
+ lib/zebra.h | 6 +-
+ pbrd/pbr_zebra.c | 8 +
+ staticd/static_vty.c | 1 +
+ tests/bgpd/test_peer_attr.c | 2 +
+ tests/isisd/test_isis_spf.c | 2 +-
+ tests/topotests/bgp_link_state/__init__.py | 0
+ tests/topotests/bgp_link_state/r1/frr.conf | 57 +
+ .../topotests/bgp_link_state/r1/isis_adj.json | 18 +
+ .../bgp_link_state/r2/bgp_capability.json | 11 +
+ .../bgp_link_state/r2/bgp_ls_nlri.json | 1507 +++++
+ .../bgp_link_state/r2/bgp_neighbor.json | 7 +
+ tests/topotests/bgp_link_state/r2/frr.conf | 72 +
+ .../topotests/bgp_link_state/r2/isis_adj.json | 18 +
+ tests/topotests/bgp_link_state/r3/frr.conf | 56 +
+ .../topotests/bgp_link_state/r3/isis_adj.json | 18 +
+ tests/topotests/bgp_link_state/r4/frr.conf | 54 +
+ .../topotests/bgp_link_state/r4/isis_adj.json | 18 +
+ .../bgp_link_state/rr/bgp_capability.json | 11 +
+ .../rr/bgp_ls_attrs_link43.json | 14 +
+ .../bgp_link_state/rr/bgp_ls_attrs_node4.json | 15 +
+ .../bgp_link_state/rr/bgp_ls_nlri.json | 1552 +++++
+ .../bgp_link_state/rr/bgp_ls_prefix4.json | 13 +
+ .../bgp_link_state/rr/bgp_neighbor.json | 7 +
+ tests/topotests/bgp_link_state/rr/frr.conf | 23 +
+ .../bgp_link_state/test_bgp_link_state.py | 1007 +++
+ vtysh/vtysh.c | 27 +-
+ zebra/connected.c | 2 +
+ zebra/router-id.c | 2 +
+ zebra/zapi_msg.c | 1 +
+ zebra/zebra_nhg.c | 2 +
+ 76 files changed, 14508 insertions(+), 142 deletions(-)
+ create mode 100644 bgpd/bgp_ls.c
+ create mode 100644 bgpd/bgp_ls.h
+ create mode 100644 bgpd/bgp_ls_nlri.c
+ create mode 100644 bgpd/bgp_ls_nlri.h
+ create mode 100644 bgpd/bgp_ls_ted.c
+ create mode 100644 bgpd/bgp_ls_ted.h
+ create mode 100644 doc/user/bgp-linkstate.rst
+ create mode 100644 tests/topotests/bgp_link_state/__init__.py
+ create mode 100644 tests/topotests/bgp_link_state/r1/frr.conf
+ create mode 100644 tests/topotests/bgp_link_state/r1/isis_adj.json
+ create mode 100644 tests/topotests/bgp_link_state/r2/bgp_capability.json
+ create mode 100644 tests/topotests/bgp_link_state/r2/bgp_ls_nlri.json
+ create mode 100644 tests/topotests/bgp_link_state/r2/bgp_neighbor.json
+ create mode 100644 tests/topotests/bgp_link_state/r2/frr.conf
+ create mode 100644 tests/topotests/bgp_link_state/r2/isis_adj.json
+ create mode 100644 tests/topotests/bgp_link_state/r3/frr.conf
+ create mode 100644 tests/topotests/bgp_link_state/r3/isis_adj.json
+ create mode 100644 tests/topotests/bgp_link_state/r4/frr.conf
+ create mode 100644 tests/topotests/bgp_link_state/r4/isis_adj.json
+ create mode 100644 tests/topotests/bgp_link_state/rr/bgp_capability.json
+ create mode 100644 tests/topotests/bgp_link_state/rr/bgp_ls_attrs_link43.json
+ create mode 100644 tests/topotests/bgp_link_state/rr/bgp_ls_attrs_node4.json
+ create mode 100644 tests/topotests/bgp_link_state/rr/bgp_ls_nlri.json
+ create mode 100644 tests/topotests/bgp_link_state/rr/bgp_ls_prefix4.json
+ create mode 100644 tests/topotests/bgp_link_state/rr/bgp_neighbor.json
+ create mode 100644 tests/topotests/bgp_link_state/rr/frr.conf
+ create mode 100644 tests/topotests/bgp_link_state/test_bgp_link_state.py
+
+diff --git a/bgpd/bgp_attr.c b/bgpd/bgp_attr.c
+index fcfff91cd7..924e24fd19 100644
+--- a/bgpd/bgp_attr.c
++++ b/bgpd/bgp_attr.c
+@@ -44,6 +44,7 @@
+ #include "bgp_evpn.h"
+ #include "bgp_flowspec_private.h"
+ #include "bgp_mac.h"
++#include "bgpd/bgp_ls_nlri.h"
+
+ /* Attribute strings for logging. */
+ static const struct message attr_str[] = {
+@@ -992,6 +993,8 @@ unsigned int attrhash_key_make(const void *p)
+ key = jhash(&attr->rmac, sizeof(attr->rmac), key);
+ if (bgp_attr_get_nhc(attr))
+ MIX(bgp_nhc_hash_key_make(bgp_attr_get_nhc(attr)));
++ if (attr->ls_attr)
++ MIX(bgp_ls_attr_hash_key(attr->ls_attr));
+
+ return key;
+ }
+@@ -1041,7 +1044,8 @@ bool attrhash_cmp(const void *p1, const void *p2)
+ attr1->srte_color == attr2->srte_color && attr1->nh_type == attr2->nh_type &&
+ attr1->bh_type == attr2->bh_type && attr1->otc == attr2->otc &&
+ !memcmp(&attr1->rmac, &attr2->rmac, sizeof(struct ethaddr)) &&
+- bgp_nhc_same(bgp_attr_get_nhc(attr1), bgp_attr_get_nhc(attr2)))
++ bgp_nhc_same(bgp_attr_get_nhc(attr1), bgp_attr_get_nhc(attr2)) &&
++ bgp_ls_attr_same(attr1->ls_attr, attr2->ls_attr))
+ return true;
+ }
+
+@@ -1291,6 +1295,13 @@ struct attr *bgp_attr_intern(struct attr *attr)
+ nhc->refcnt++;
+ }
+
++ if (attr->ls_attr) {
++ if (!attr->ls_attr->refcnt)
++ attr->ls_attr = bgp_ls_attr_intern(attr->ls_attr);
++ else
++ attr->ls_attr->refcnt++;
++ }
++
+ /* At this point, attr only contains intern'd pointers. that means
+ * if we find it in attrhash, it has all the same pointers and we
+ * correctly updated the refcounts on these.
+@@ -1504,6 +1515,8 @@ void bgp_attr_unintern_sub(struct attr *attr)
+ bre = bgp_attr_get_evpn_overlay(attr);
+ evpn_overlay_unintern(&bre);
+ bgp_attr_set_evpn_overlay(attr, NULL);
++
++ bgp_ls_attr_unintern(&attr->ls_attr);
+ }
+
+ /* Free bgp attribute and aspath. */
+@@ -3830,6 +3843,37 @@ otc_ignore:
+ return bgp_attr_ignore(peer, args->type);
+ }
+
++/* BGP-LS attribute (rfc9552) */
++static enum bgp_attr_parse_ret bgp_attr_ls(struct bgp_attr_parser_args *args)
++{
++ struct peer *const peer = args->peer;
++ struct attr *const attr = args->attr;
++ int ret;
++ struct bgp_ls_attr *ls_attr;
++
++ if (peer->discard_attrs[args->type] || peer->withdraw_attrs[args->type])
++ goto ls_attr_ignore;
++
++ ls_attr = bgp_ls_attr_alloc();
++
++ ret = bgp_ls_parse_attr(peer->curr, args->length, ls_attr);
++ if (ret != 0) {
++ bgp_ls_attr_free(ls_attr);
++ return BGP_ATTR_PARSE_ERROR;
++ }
++
++ attr->ls_attr = bgp_ls_attr_intern(ls_attr);
++
++ bgp_ls_attr_free(ls_attr);
++
++ return BGP_ATTR_PARSE_PROCEED;
++
++ls_attr_ignore:
++ stream_forward_getp(peer->curr, args->length);
++
++ return bgp_attr_ignore(peer, args->type);
++}
++
+ /* BGP unknown attribute treatment. */
+ static enum bgp_attr_parse_ret
+ bgp_attr_unknown(struct bgp_attr_parser_args *args)
+@@ -4260,6 +4304,9 @@ enum bgp_attr_parse_ret bgp_attr_parse(struct peer *peer, struct attr *attr,
+ case BGP_ATTR_NHC:
+ ret = bgp_attr_nhc(&attr_args);
+ break;
++ case BGP_ATTR_LINK_STATE:
++ ret = bgp_attr_ls(&attr_args);
++ break;
+ default:
+ ret = bgp_attr_unknown(&attr_args);
+ break;
+@@ -4511,6 +4558,10 @@ size_t bgp_packet_mpattr_start(struct stream *s, struct peer *peer, afi_t afi,
+ nh_afi = peer_cap_enhe(peer, afi, safi) ? AFI_IP6 : AFI_IP;
+ else if (safi == SAFI_FLOWSPEC)
+ nh_afi = afi;
++ else if (safi == SAFI_BGP_LS)
++ nh_afi = CHECK_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV6)
++ ? AFI_IP6
++ : AFI_IP;
+ else
+ nh_afi = BGP_NEXTHOP_AFI_FROM_NHLEN(attr->mp_nexthop_len);
+
+@@ -4544,6 +4595,10 @@ size_t bgp_packet_mpattr_start(struct stream *s, struct peer *peer, afi_t afi,
+ stream_put_ipv4(s, attr->nexthop.s_addr);
+ }
+ break;
++ case SAFI_BGP_LS:
++ stream_putc(s, IPV4_MAX_BYTELEN);
++ stream_put_ipv4(s, attr->mp_nexthop_global_in.s_addr);
++ break;
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+ assert(!"SAFI's UNSPEC or MAX being specified are a DEV ESCAPE");
+@@ -4598,6 +4653,12 @@ size_t bgp_packet_mpattr_start(struct stream *s, struct peer *peer, afi_t afi,
+ case SAFI_FLOWSPEC:
+ stream_putc(s, 0); /* no nexthop for flowspec */
+ break;
++ case SAFI_BGP_LS:
++ stream_putc(s, attr->mp_nexthop_len);
++ stream_put(s, &attr->mp_nexthop_global, IPV6_MAX_BYTELEN);
++ if (attr->mp_nexthop_len == BGP_ATTR_NHLEN_IPV6_GLOBAL_AND_LL)
++ stream_put(s, &attr->mp_nexthop_local, IPV6_MAX_BYTELEN);
++ break;
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+ assert(!"SAFI's UNSPEC or MAX being specified are a DEV ESCAPE");
+@@ -4611,9 +4672,10 @@ size_t bgp_packet_mpattr_start(struct stream *s, struct peer *peer, afi_t afi,
+ "Bad nexthop when sending to %s, AFI %u SAFI %u nhlen %d",
+ peer->host, afi, safi, attr->mp_nexthop_len);
+ break;
++ case AFI_BGP_LS:
+ case AFI_UNSPEC:
+ case AFI_MAX:
+- assert(!"DEV ESCAPE: AFI_UNSPEC or AFI_MAX should not be used here");
++ assert(!"DEV ESCAPE: AFI_BGP_LS, AFI_UNSPEC or AFI_MAX should not be used here");
+ break;
+ }
+
+@@ -4717,11 +4779,42 @@ static void bgp_packet_nhc(struct stream *s, struct peer *peer, afi_t afi, safi_
+ stream_putc_at(s, sizep, (stream_get_endp(s) - sizep) - 1);
+ }
+
+-void bgp_packet_mpattr_prefix(struct stream *s, afi_t afi, safi_t safi,
+- const struct prefix *p,
+- const struct prefix_rd *prd, mpls_label_t *label,
+- uint8_t num_labels, bool addpath_capable,
+- uint32_t addpath_tx_id, struct attr *attr)
++static void bgp_packet_ls_attribute(struct stream *s, struct bgp *bgp, struct attr *attr,
++ struct bgp_path_info *bpi, enum bgp_ls_protocol_id protocol_id)
++{
++ struct bgp_ls_attr *ls_attr = attr->ls_attr;
++ size_t attr_start, len_pos, attr_len;
++ int ret = -1;
++
++ /* Write BGP-LS attribute header (RFC 9552 Section 4) */
++ attr_start = stream_get_endp(s);
++ stream_putc(s, BGP_ATTR_FLAG_OPTIONAL | BGP_ATTR_FLAG_EXTLEN);
++ stream_putc(s, BGP_ATTR_LINK_STATE);
++ len_pos = stream_get_endp(s);
++ stream_putw(s, 0); /* Placeholder for extended length */
++
++ ret = bgp_ls_encode_attr(s, ls_attr, protocol_id);
++
++ if (ret < 0) {
++ /* No attributes or encoding failed - rollback the stream */
++ stream_set_endp(s, attr_start);
++ return;
++ }
++
++ /* Update the length field */
++ attr_len = stream_get_endp(s) - len_pos - 2;
++ if (attr_len > UINT16_MAX) {
++ stream_set_endp(s, attr_start);
++ return;
++ }
++
++ stream_putw_at(s, len_pos, attr_len);
++}
++
++void bgp_packet_mpattr_prefix(struct stream *s, afi_t afi, safi_t safi, const struct prefix *p,
++ const struct prefix_rd *prd, mpls_label_t *label, uint8_t num_labels,
++ bool addpath_capable, uint32_t addpath_tx_id, struct attr *attr,
++ struct bgp_ls_nlri *ls_nlri)
+ {
+ mpls_label_t restore_label;
+
+@@ -4748,6 +4841,9 @@ void bgp_packet_mpattr_prefix(struct stream *s, afi_t afi, safi_t safi,
+ else
+ assert(!"Add encoding bits here for other AFI's");
+ break;
++ case SAFI_BGP_LS:
++ bgp_ls_encode_nlri(s, ls_nlri);
++ break;
+ case SAFI_LABELED_UNICAST:
+ if (num_labels > 1) {
+ zlog_warn("%s: stream_put_labeled_prefix currently supports only one label, ignoring rest (num_labels=%d)",
+@@ -4811,6 +4907,10 @@ size_t bgp_packet_mpattr_prefix_size(afi_t afi, safi_t safi,
+ case SAFI_FLOWSPEC:
+ size = ((struct prefix_fs *)p)->prefix.prefixlen;
+ break;
++ case SAFI_BGP_LS:
++ /* TODO: add explaination */
++ size = 0;
++ break;
+ }
+
+ return size;
+@@ -4961,7 +5061,7 @@ bgp_size_t bgp_packet_attribute(struct bgp *bgp, struct peer *peer, struct strea
+ struct prefix *p, afi_t afi, safi_t safi, struct peer *from,
+ struct prefix_rd *prd, mpls_label_t *label, uint8_t num_labels,
+ bool addpath_capable, uint32_t addpath_tx_id,
+- struct bgp_path_info *bpi)
++ struct bgp_path_info *bpi, struct bgp_ls_nlri *ls_nlri)
+ {
+ size_t cp;
+ size_t aspath_sizep;
+@@ -4984,9 +5084,8 @@ bgp_size_t bgp_packet_attribute(struct bgp *bgp, struct peer *peer, struct strea
+
+ mpattrlen_pos = bgp_packet_mpattr_start(s, peer, afi, safi,
+ vecarr, attr);
+- bgp_packet_mpattr_prefix(s, afi, safi, p, prd, label,
+- num_labels, addpath_capable,
+- addpath_tx_id, attr);
++ bgp_packet_mpattr_prefix(s, afi, safi, p, prd, label, num_labels, addpath_capable,
++ addpath_tx_id, attr, ls_nlri);
+ bgp_packet_mpattr_end(s, mpattrlen_pos);
+ }
+
+@@ -5444,6 +5543,10 @@ bgp_size_t bgp_packet_attribute(struct bgp *bgp, struct peer *peer, struct strea
+ stream_put_bgp_aigp_tlv_metric(s, attr->aigp_metric);
+ }
+
++ /* BGP-LS Attribute (Type 29) - RFC 9552 Section 4 */
++ if (afi == AFI_BGP_LS && safi == SAFI_BGP_LS && attr->ls_attr && ls_nlri)
++ bgp_packet_ls_attribute(s, bgp, attr, bpi, bgp_ls_nlri_protocol_id(ls_nlri));
++
+ /* draft-ietf-idr-entropy-label */
+ if (peergroup_flag_check(peer, PEER_FLAG_SEND_NHC_ATTRIBUTE))
+ bgp_packet_nhc(s, peer, afi, safi, attr, bpi);
+@@ -5480,12 +5583,10 @@ size_t bgp_packet_mpunreach_start(struct stream *s, afi_t afi, safi_t safi)
+ return attrlen_pnt;
+ }
+
+-void bgp_packet_mpunreach_prefix(struct stream *s, const struct prefix *p,
+- afi_t afi, safi_t safi,
+- const struct prefix_rd *prd,
+- mpls_label_t *label, uint8_t num_labels,
+- bool addpath_capable, uint32_t addpath_tx_id,
+- struct attr *attr)
++void bgp_packet_mpunreach_prefix(struct stream *s, const struct prefix *p, afi_t afi, safi_t safi,
++ const struct prefix_rd *prd, mpls_label_t *label,
++ uint8_t num_labels, bool addpath_capable, uint32_t addpath_tx_id,
++ struct attr *attr, struct bgp_ls_nlri *ls_nlri)
+ {
+ uint8_t wlabel[4] = {0x80, 0x00, 0x00};
+
+@@ -5500,8 +5601,8 @@ void bgp_packet_mpunreach_prefix(struct stream *s, const struct prefix *p,
+ num_labels = 1;
+ }
+
+- bgp_packet_mpattr_prefix(s, afi, safi, p, prd, label, num_labels,
+- addpath_capable, addpath_tx_id, attr);
++ bgp_packet_mpattr_prefix(s, afi, safi, p, prd, label, num_labels, addpath_capable,
++ addpath_tx_id, attr, ls_nlri);
+ }
+
+ void bgp_packet_mpunreach_end(struct stream *s, size_t attrlen_pnt)
+diff --git a/bgpd/bgp_attr.h b/bgpd/bgp_attr.h
+index 3340822d58..88fb156bc1 100644
+--- a/bgpd/bgp_attr.h
++++ b/bgpd/bgp_attr.h
+@@ -321,6 +321,9 @@ struct attr {
+
+ /* Next-hop characteristics */
+ struct bgp_nhc *nhc;
++
++ /* For BGP-LS Attribute (RFC 9552) */
++ struct bgp_ls_attr *ls_attr;
+ };
+
+ /* rmap_change_flags definition */
+@@ -399,7 +402,8 @@ extern bgp_size_t bgp_packet_attribute(struct bgp *bgp, struct peer *peer, struc
+ struct prefix *p, afi_t afi, safi_t safi, struct peer *from,
+ struct prefix_rd *prd, mpls_label_t *label,
+ uint8_t num_labels, bool addpath_capable,
+- uint32_t addpath_tx_id, struct bgp_path_info *bpi);
++ uint32_t addpath_tx_id, struct bgp_path_info *bpi,
++ struct bgp_ls_nlri *ls_nlri);
+ extern void bgp_dump_routes_attr(struct stream *s, struct bgp_path_info *bpi,
+ const struct prefix *p);
+ extern bool attrhash_cmp(const void *arg1, const void *arg2);
+@@ -458,17 +462,19 @@ extern void bgp_packet_mpattr_prefix(struct stream *s, afi_t afi, safi_t safi,
+ const struct prefix_rd *prd,
+ mpls_label_t *label, uint8_t num_labels,
+ bool addpath_capable,
+- uint32_t addpath_tx_id, struct attr *);
++ uint32_t addpath_tx_id, struct attr *,
++ struct bgp_ls_nlri *ls_nlri);
+ extern size_t bgp_packet_mpattr_prefix_size(afi_t afi, safi_t safi,
+ const struct prefix *p);
+ extern void bgp_packet_mpattr_end(struct stream *s, size_t sizep);
+
+ extern size_t bgp_packet_mpunreach_start(struct stream *s, afi_t afi,
+ safi_t safi);
+-extern void bgp_packet_mpunreach_prefix(
+- struct stream *s, const struct prefix *p, afi_t afi, safi_t safi,
+- const struct prefix_rd *prd, mpls_label_t *label, uint8_t num_labels,
+- bool addpath_capable, uint32_t addpath_tx_id, struct attr *attr);
++extern void bgp_packet_mpunreach_prefix(struct stream *s, const struct prefix *p, afi_t afi,
++ safi_t safi, const struct prefix_rd *prd,
++ mpls_label_t *label, uint8_t num_labels,
++ bool addpath_capable, uint32_t addpath_tx_id,
++ struct attr *attr, struct bgp_ls_nlri *ls_nlri);
+ extern void bgp_packet_mpunreach_end(struct stream *s, size_t attrlen_pnt);
+
+ extern enum bgp_attr_parse_ret bgp_attr_nexthop_valid(struct peer *peer,
+diff --git a/bgpd/bgp_bmp.c b/bgpd/bgp_bmp.c
+index c095a3ea77..554766433e 100644
+--- a/bgpd/bgp_bmp.c
++++ b/bgpd/bgp_bmp.c
+@@ -1118,7 +1118,7 @@ static struct stream *bmp_update(const struct prefix *p, struct prefix_rd *prd,
+
+ /* 5: Encode all the attributes, except MP_REACH_NLRI attr. */
+ total_attr_len = bgp_packet_attribute(NULL, peer, s, attr, &vecarr, NULL, afi, safi, peer,
+- NULL, NULL, 0, 0, 0, NULL);
++ NULL, NULL, 0, 0, 0, NULL, NULL);
+
+ /* space check? */
+
+@@ -1132,8 +1132,7 @@ static struct stream *bmp_update(const struct prefix *p, struct prefix_rd *prd,
+
+ mpattrlen_pos = bgp_packet_mpattr_start(s, peer, afi, safi,
+ &vecarr, attr);
+- bgp_packet_mpattr_prefix(s, afi, safi, p, prd, label,
+- num_labels, 0, 0, attr);
++ bgp_packet_mpattr_prefix(s, afi, safi, p, prd, label, num_labels, 0, 0, attr, NULL);
+ bgp_packet_mpattr_end(s, mpattrlen_pos);
+ total_attr_len += stream_get_endp(s) - p1;
+ }
+@@ -1171,8 +1170,7 @@ static struct stream *bmp_withdraw(const struct prefix *p,
+ mp_start = stream_get_endp(s);
+ mplen_pos = bgp_packet_mpunreach_start(s, afi, safi);
+
+- bgp_packet_mpunreach_prefix(s, p, afi, safi, prd, NULL, 0, 0, 0,
+- NULL);
++ bgp_packet_mpunreach_prefix(s, p, afi, safi, prd, NULL, 0, 0, 0, NULL, NULL);
+ /* Set the mp_unreach attr's length */
+ bgp_packet_mpunreach_end(s, mplen_pos);
+
+diff --git a/bgpd/bgp_debug.c b/bgpd/bgp_debug.c
+index 73b5280525..bb2fe37304 100644
+--- a/bgpd/bgp_debug.c
++++ b/bgpd/bgp_debug.c
+@@ -62,6 +62,7 @@ unsigned long conf_bgp_debug_evpn_mh;
+ unsigned long conf_bgp_debug_bfd;
+ unsigned long conf_bgp_debug_cond_adv;
+ unsigned long conf_bgp_debug_aggregate;
++unsigned long conf_bgp_debug_linkstate;
+
+ unsigned long term_bgp_debug_as4;
+ unsigned long term_bgp_debug_neighbor_events;
+@@ -83,6 +84,7 @@ unsigned long term_bgp_debug_evpn_mh;
+ unsigned long term_bgp_debug_bfd;
+ unsigned long term_bgp_debug_cond_adv;
+ unsigned long term_bgp_debug_aggregate;
++unsigned long term_bgp_debug_linkstate;
+
+ struct list *bgp_debug_neighbor_events_peers = NULL;
+ struct list *bgp_debug_keepalive_peers = NULL;
+@@ -2255,6 +2257,41 @@ DEFPY (debug_bgp_cond_adv,
+ return CMD_SUCCESS;
+ }
+
++DEFPY (debug_bgp_linkstate,
++ debug_bgp_linkstate_cmd,
++ "debug bgp link-state",
++ DEBUG_STR
++ BGP_STR
++ "BGP Link-State\n")
++{
++ if (vty->node == CONFIG_NODE)
++ DEBUG_ON(linkstate, LINKSTATE);
++ else {
++ TERM_DEBUG_ON(linkstate, LINKSTATE);
++ vty_out(vty, "BGP Link-State debugging is on\n");
++ }
++
++ return CMD_SUCCESS;
++}
++
++DEFPY (no_debug_bgp_linkstate,
++ no_debug_bgp_linkstate_cmd,
++ "no debug bgp link-state",
++ NO_STR
++ DEBUG_STR
++ BGP_STR
++ "BGP Link-State\n")
++{
++ if (vty->node == CONFIG_NODE)
++ DEBUG_OFF(linkstate, LINKSTATE);
++ else {
++ TERM_DEBUG_OFF(linkstate, LINKSTATE);
++ vty_out(vty, "BGP Link-State debugging is off\n");
++ }
++
++ return CMD_SUCCESS;
++}
++
+ DEFUN (no_debug_bgp,
+ no_debug_bgp_cmd,
+ "no debug bgp",
+@@ -2299,6 +2336,7 @@ DEFUN (no_debug_bgp,
+ TERM_DEBUG_OFF(evpn_mh, EVPN_MH_RT);
+ TERM_DEBUG_OFF(bfd, BFD_LIB);
+ TERM_DEBUG_OFF(cond_adv, COND_ADV);
++ TERM_DEBUG_OFF(linkstate, LINKSTATE);
+
+ vty_out(vty, "All possible debugging has been turned off\n");
+
+@@ -2396,6 +2434,9 @@ DEFUN_NOSH (show_debugging_bgp,
+ vty_out(vty,
+ " BGP conditional advertisement debugging is on\n");
+
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ vty_out(vty, " BGP Link-State debugging is on\n");
++
+ cmd_show_lib_debugs(vty);
+
+ hook_call(bgp_hook_config_write_debug, vty, false);
+@@ -2534,6 +2575,11 @@ static int bgp_config_write_debug(struct vty *vty)
+ write++;
+ }
+
++ if (CONF_BGP_DEBUG(linkstate, LINKSTATE)) {
++ vty_out(vty, "debug bgp link-state\n");
++ write++;
++ }
++
+ if (CONF_BGP_DEBUG(aggregate, AGGREGATE)) {
+ if (!bgp_debug_aggregate_prefixes || list_isempty(bgp_debug_aggregate_prefixes)) {
+ vty_out(vty, "debug bgp aggregate\n");
+@@ -2691,6 +2737,12 @@ void bgp_debug_init(void)
+ /* debug bgp conditional advertisement */
+ install_element(ENABLE_NODE, &debug_bgp_cond_adv_cmd);
+ install_element(CONFIG_NODE, &debug_bgp_cond_adv_cmd);
++
++ /* debug bgp link-state */
++ install_element(ENABLE_NODE, &debug_bgp_linkstate_cmd);
++ install_element(CONFIG_NODE, &debug_bgp_linkstate_cmd);
++ install_element(ENABLE_NODE, &no_debug_bgp_linkstate_cmd);
++ install_element(CONFIG_NODE, &no_debug_bgp_linkstate_cmd);
+ }
+
+ /* Return true if this prefix is on the per_prefix_list of prefixes to debug
+diff --git a/bgpd/bgp_debug.h b/bgpd/bgp_debug.h
+index e7d43bfacd..f16c6bcbc4 100644
+--- a/bgpd/bgp_debug.h
++++ b/bgpd/bgp_debug.h
+@@ -72,6 +72,7 @@ extern unsigned long conf_bgp_debug_evpn_mh;
+ extern unsigned long conf_bgp_debug_bfd;
+ extern unsigned long conf_bgp_debug_cond_adv;
+ extern unsigned long conf_bgp_debug_aggregate;
++extern unsigned long conf_bgp_debug_linkstate;
+
+ extern unsigned long term_bgp_debug_as4;
+ extern unsigned long term_bgp_debug_neighbor_events;
+@@ -91,6 +92,7 @@ extern unsigned long term_bgp_debug_evpn_mh;
+ extern unsigned long term_bgp_debug_bfd;
+ extern unsigned long term_bgp_debug_cond_adv;
+ extern unsigned long term_bgp_debug_aggregate;
++extern unsigned long term_bgp_debug_linkstate;
+
+ extern struct list *bgp_debug_neighbor_events_peers;
+ extern struct list *bgp_debug_keepalive_peers;
+@@ -139,6 +141,7 @@ struct bgp_debug_filter {
+ #define BGP_DEBUG_BFD_LIB 0x01
+ #define BGP_DEBUG_COND_ADV 0x01
+ #define BGP_DEBUG_AGGREGATE 0x01
++#define BGP_DEBUG_LINKSTATE 0x01
+
+ #define CONF_DEBUG_ON(a, b) (conf_bgp_debug_ ## a |= (BGP_DEBUG_ ## b))
+ #define CONF_DEBUG_OFF(a, b) (conf_bgp_debug_ ## a &= ~(BGP_DEBUG_ ## b))
+diff --git a/bgpd/bgp_errors.c b/bgpd/bgp_errors.c
+index 977868adc7..f084022482 100644
+--- a/bgpd/bgp_errors.c
++++ b/bgpd/bgp_errors.c
+@@ -491,6 +491,12 @@ static struct log_ref ferr_bgp_err[] = {
+ .description = "BGP could not find the label for the specified path_info",
+ .suggestion = "Most likely a bug. This should never happen. Please try to build a reproducer and report.",
+ },
++ {
++ .code = EC_BGP_LS_PACKET,
++ .title = "BGP-LS packet error",
++ .description = "BGP-LS NLRI or attribute packet parsing/encoding error",
++ .suggestion = "Check that BGP-LS peer is sending valid packets per RFC 9552. May indicate interoperability issue or malformed data.",
++ },
+ {
+ .code = END_FERR,
+ }
+diff --git a/bgpd/bgp_errors.h b/bgpd/bgp_errors.h
+index c15692112c..d65dbc8a52 100644
+--- a/bgpd/bgp_errors.h
++++ b/bgpd/bgp_errors.h
+@@ -93,6 +93,7 @@ enum bgp_log_refs {
+ EC_BGP_SENDQ_STUCK_WARN,
+ EC_BGP_SENDQ_STUCK_PROPER,
+ EC_BGP_PATH_WITHOUT_LABEL,
++ EC_BGP_LS_PACKET,
+ };
+
+ extern void bgp_error_init(void);
+diff --git a/bgpd/bgp_ls.c b/bgpd/bgp_ls.c
+new file mode 100644
+index 0000000000..199e390bee
+--- /dev/null
++++ b/bgpd/bgp_ls.c
+@@ -0,0 +1,878 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * BGP Link-State (RFC 9552) - Core Implementation
++ * Copyright (C) 2025 Carmine Scarpitta
++ */
++
++#include <zebra.h>
++
++#include "lib/json.h"
++
++#include "stream.h"
++#include "bgpd/bgpd.h"
++#include "bgpd/bgp_aspath.h"
++#include "bgpd/bgp_ls.h"
++#include "bgpd/bgp_ls_nlri.h"
++#include "bgpd/bgp_ls_ted.h"
++#include "bgpd/bgp_debug.h"
++#include "bgpd/bgp_attr.h"
++#include "bgpd/bgp_route.h"
++#include "bgpd/bgp_errors.h"
++#include "bgpd/bgp_zebra.h"
++#define UNKNOWN LS_UNKNOWN
++#include "lib/link_state.h"
++#undef UNKNOWN
++
++DEFINE_MTYPE_STATIC(BGPD, BGP_LS, "BGP-LS instance");
++
++/*
++ * Helper Functions for NLRI Formatting
++ */
++
++/* Convert node descriptor to JSON */
++static json_object *node_desc_to_json(struct bgp_ls_node_descriptor *node)
++{
++ json_object *json_node = json_object_new_object();
++
++ if (BGP_LS_TLV_CHECK(node->present_tlvs, BGP_LS_NODE_DESC_AS_BIT))
++ json_object_int_add(json_node, "asn", node->asn);
++
++ if (BGP_LS_TLV_CHECK(node->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT))
++ json_object_int_add(json_node, "bgplsId", node->bgp_ls_id);
++
++ if (BGP_LS_TLV_CHECK(node->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT))
++ json_object_string_addf(json_node, "ospfAreaId", "%pI4",
++ (in_addr_t *)&node->ospf_area_id);
++
++ if (BGP_LS_TLV_CHECK(node->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT)) {
++ char igp_router_id[256];
++ char *p = igp_router_id;
++
++ for (int i = 0; i < node->igp_router_id_len; i++) {
++ p += snprintfrr(p, sizeof(igp_router_id) - (p - igp_router_id), "%02x",
++ node->igp_router_id[i]);
++ if (i < node->igp_router_id_len - 1 && (i + 1) % 2 == 0) {
++ p += snprintfrr(p, sizeof(igp_router_id) - (p - igp_router_id),
++ ".");
++ }
++ }
++ json_object_string_add(json_node, "igpRouterId", igp_router_id);
++ }
++
++ return json_node;
++}
++
++/* Convert link descriptor to JSON */
++static json_object *link_desc_to_json(struct bgp_ls_link_descriptor *link_desc)
++{
++ json_object *json_link = json_object_new_object();
++
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT)) {
++ json_object_int_add(json_link, "linkLocalId", link_desc->link_local_id);
++ json_object_int_add(json_link, "linkRemoteId", link_desc->link_remote_id);
++ }
++
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT))
++ json_object_string_addf(json_link, "ipv4InterfaceAddress", "%pI4",
++ &link_desc->ipv4_intf_addr);
++
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT))
++ json_object_string_addf(json_link, "ipv4NeighborAddress", "%pI4",
++ &link_desc->ipv4_neigh_addr);
++
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT))
++ json_object_string_addf(json_link, "ipv6InterfaceAddress", "%pI6",
++ &link_desc->ipv6_intf_addr);
++
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT))
++ json_object_string_addf(json_link, "ipv6NeighborAddress", "%pI6",
++ &link_desc->ipv6_neigh_addr);
++
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_MT_ID_BIT) &&
++ link_desc->mt_id_count > 0)
++ json_object_int_add(json_link, "mtId", link_desc->mt_id[0]);
++
++ return json_link;
++}
++
++/* Convert prefix descriptor to JSON */
++static json_object *prefix_desc_to_json(struct bgp_ls_prefix_descriptor *prefix_desc,
++ enum bgp_ls_nlri_type nlri_type)
++{
++ json_object *json_prefix = json_object_new_object();
++
++ /* IP Reachability Information */
++ json_object_string_addf(json_prefix, "ipReachabilityInformation", "%pFX",
++ &prefix_desc->prefix);
++
++ /* OSPF Route Type */
++ if (BGP_LS_TLV_CHECK(prefix_desc->present_tlvs, BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT))
++ json_object_string_addf(json_prefix, "ospfRouteType", "%s",
++ bgp_ls_ospf_route_type_str_json(
++ prefix_desc->ospf_route_type));
++
++ return json_prefix;
++}
++
++/* Convert NLRI to JSON */
++json_object *bgp_ls_nlri_to_json(struct bgp_ls_nlri *nlri)
++{
++ json_object *json_nlri = json_object_new_object();
++ const char *nlri_type_str = NULL;
++ enum bgp_ls_protocol_id protocol_id = 0;
++ uint64_t identifier = 0;
++
++ /* NLRI Type */
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE:
++ nlri_type_str = "node";
++ protocol_id = nlri->nlri_data.node.protocol_id;
++ identifier = nlri->nlri_data.node.identifier;
++ break;
++ case BGP_LS_NLRI_TYPE_LINK:
++ nlri_type_str = "link";
++ protocol_id = nlri->nlri_data.link.protocol_id;
++ identifier = nlri->nlri_data.link.identifier;
++ break;
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ nlri_type_str = "ipv4Prefix";
++ protocol_id = nlri->nlri_data.prefix.protocol_id;
++ identifier = nlri->nlri_data.prefix.identifier;
++ break;
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ nlri_type_str = "ipv6Prefix";
++ protocol_id = nlri->nlri_data.prefix.protocol_id;
++ identifier = nlri->nlri_data.prefix.identifier;
++ break;
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ nlri_type_str = "unknown";
++ break;
++ }
++ json_object_string_add(json_nlri, "nlriType", nlri_type_str);
++
++ /* Protocol ID */
++ json_object_int_add(json_nlri, "protocolId", protocol_id);
++
++ /* Identifier */
++ json_object_int_add(json_nlri, "identifier", identifier);
++
++ /* Type-specific descriptors */
++ if (nlri->nlri_type == BGP_LS_NLRI_TYPE_NODE) {
++ json_object *json_local = node_desc_to_json(&nlri->nlri_data.node.local_node);
++
++ json_object_object_add(json_nlri, "localNodeDescriptors", json_local);
++ } else if (nlri->nlri_type == BGP_LS_NLRI_TYPE_LINK) {
++ json_object *json_local = node_desc_to_json(&nlri->nlri_data.link.local_node);
++ json_object *json_remote = node_desc_to_json(&nlri->nlri_data.link.remote_node);
++ json_object *json_link = link_desc_to_json(&nlri->nlri_data.link.link_desc);
++
++ json_object_object_add(json_nlri, "localNodeDescriptors", json_local);
++ json_object_object_add(json_nlri, "remoteNodeDescriptors", json_remote);
++ json_object_object_add(json_nlri, "linkDescriptors", json_link);
++ } else if (nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV4_PREFIX ||
++ nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV6_PREFIX) {
++ json_object *json_local = node_desc_to_json(&nlri->nlri_data.prefix.local_node);
++ json_object *json_prefix = prefix_desc_to_json(&nlri->nlri_data.prefix.prefix_desc,
++ nlri->nlri_type);
++ json_object_object_add(json_nlri, "localNodeDescriptors", json_local);
++ json_object_object_add(json_nlri, "prefixDescriptors", json_prefix);
++ }
++
++ return json_nlri;
++}
++
++/* Format node descriptor to string */
++static void format_node_desc(char **p, size_t *remain, struct bgp_ls_node_descriptor *node,
++ const char *prefix_str)
++{
++ int len;
++
++ len = snprintfrr(*p, *remain, "[%s", prefix_str);
++ *p += len;
++ *remain -= len;
++
++ /* AS Number */
++ if (BGP_LS_TLV_CHECK(node->present_tlvs, BGP_LS_NODE_DESC_AS_BIT)) {
++ len = snprintfrr(*p, *remain, "[c%u]", node->asn);
++ *p += len;
++ *remain -= len;
++ }
++
++ /* BGP-LS Identifier (deprecated but still used) */
++ if (BGP_LS_TLV_CHECK(node->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT)) {
++ len = snprintfrr(*p, *remain, "[b%u.%u.%u.%u]", (node->bgp_ls_id >> 24) & 0xFF,
++ (node->bgp_ls_id >> 16) & 0xFF, (node->bgp_ls_id >> 8) & 0xFF,
++ node->bgp_ls_id & 0xFF);
++ *p += len;
++ *remain -= len;
++ }
++
++ /* IGP Router ID */
++ if (BGP_LS_TLV_CHECK(node->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT)) {
++ len = snprintfrr(*p, *remain, "[s");
++ *p += len;
++ *remain -= len;
++ for (int i = 0; i < node->igp_router_id_len; i++) {
++ len = snprintfrr(*p, *remain, "%02x", node->igp_router_id[i]);
++ *p += len;
++ *remain -= len;
++ if (i < node->igp_router_id_len - 1 && (i + 1) % 2 == 0) {
++ len = snprintfrr(*p, *remain, ".");
++ *p += len;
++ *remain -= len;
++ }
++ }
++ len = snprintfrr(*p, *remain, "]");
++ *p += len;
++ *remain -= len;
++ }
++
++ len = snprintfrr(*p, *remain, "]");
++ *p += len;
++ *remain -= len;
++}
++
++/* Format link descriptor to string */
++static void format_link_desc(char **p, size_t *remain, struct bgp_ls_link_descriptor *link_desc)
++{
++ int len;
++
++ len = snprintfrr(*p, *remain, "[L");
++ *p += len;
++ *remain -= len;
++
++ /* Link Local/Remote Identifiers (TLV 258) */
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT)) {
++ len = snprintfrr(*p, *remain, "[l%u/%u]", link_desc->link_local_id,
++ link_desc->link_remote_id);
++ *p += len;
++ *remain -= len;
++ }
++
++ /* IPv4 Interface Address (TLV 259) */
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT)) {
++ len = snprintfrr(*p, *remain, "[i%pI4]", &link_desc->ipv4_intf_addr);
++ *p += len;
++ *remain -= len;
++ }
++
++ /* IPv4 Neighbor Address (TLV 260) */
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT)) {
++ len = snprintfrr(*p, *remain, "[n%pI4]", &link_desc->ipv4_neigh_addr);
++ *p += len;
++ *remain -= len;
++ }
++
++ /* IPv6 Interface Address (TLV 261) */
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT)) {
++ len = snprintfrr(*p, *remain, "[i%pI6]", &link_desc->ipv6_intf_addr);
++ *p += len;
++ *remain -= len;
++ }
++
++ /* IPv6 Neighbor Address (TLV 262) */
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT)) {
++ len = snprintfrr(*p, *remain, "[n%pI6]", &link_desc->ipv6_neigh_addr);
++ *p += len;
++ *remain -= len;
++ }
++
++ len = snprintfrr(*p, *remain, "]");
++ *p += len;
++ *remain -= len;
++}
++
++void bgp_ls_nlri_format(struct bgp_ls_nlri *nlri, char *buf, size_t buf_len)
++{
++ char tmp[512];
++ char *p = buf;
++ size_t remain = buf_len;
++ int len;
++
++ if (!nlri || !buf || buf_len == 0)
++ return;
++
++ /* NLRI Type prefix */
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ len = snprintfrr(p, remain, "[R]");
++ break;
++ case BGP_LS_NLRI_TYPE_NODE:
++ len = snprintfrr(p, remain, "[V]");
++ break;
++ case BGP_LS_NLRI_TYPE_LINK:
++ len = snprintfrr(p, remain, "[E]");
++ break;
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ len = snprintfrr(p, remain, "[T]");
++ break;
++ default:
++ len = snprintfrr(p, remain, "[U]");
++ break;
++ }
++ p += len;
++ remain -= len;
++
++ /* Protocol ID and Instance ID - directly from NLRI structures */
++ const char *proto_str = NULL;
++ uint64_t instance_id = 0;
++ enum bgp_ls_protocol_id protocol_id = 0;
++
++ if (nlri->nlri_type == BGP_LS_NLRI_TYPE_NODE) {
++ protocol_id = nlri->nlri_data.node.protocol_id;
++ instance_id = nlri->nlri_data.node.identifier;
++ } else if (nlri->nlri_type == BGP_LS_NLRI_TYPE_LINK) {
++ protocol_id = nlri->nlri_data.link.protocol_id;
++ instance_id = nlri->nlri_data.link.identifier;
++ } else if (nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV4_PREFIX ||
++ nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV6_PREFIX) {
++ protocol_id = nlri->nlri_data.prefix.protocol_id;
++ instance_id = nlri->nlri_data.prefix.identifier;
++ }
++
++ switch (protocol_id) {
++ case BGP_LS_PROTO_ISIS_L1:
++ proto_str = "L1";
++ break;
++ case BGP_LS_PROTO_ISIS_L2:
++ proto_str = "L2";
++ break;
++ case BGP_LS_PROTO_OSPFV2:
++ proto_str = "O";
++ break;
++ case BGP_LS_PROTO_OSPFV3:
++ proto_str = "O3";
++ break;
++ case BGP_LS_PROTO_DIRECT:
++ proto_str = "D";
++ break;
++ case BGP_LS_PROTO_STATIC:
++ proto_str = "S";
++ break;
++ case BGP_LS_PROTO_BGP:
++ proto_str = "B";
++ break;
++ case BGP_LS_PROTO_RESERVED:
++ proto_str = "U";
++ break;
++ }
++ len = snprintfrr(p, remain, "[%s][I0x%llx]", proto_str, (unsigned long long)instance_id);
++ p += len;
++ remain -= len;
++
++ /* Add NLRI type-specific descriptors */
++ if (nlri->nlri_type == BGP_LS_NLRI_TYPE_NODE) {
++ format_node_desc(&p, &remain, &nlri->nlri_data.node.local_node, "N");
++ } else if (nlri->nlri_type == BGP_LS_NLRI_TYPE_LINK) {
++ format_node_desc(&p, &remain, &nlri->nlri_data.link.local_node, "N");
++ format_node_desc(&p, &remain, &nlri->nlri_data.link.remote_node, "R");
++ format_link_desc(&p, &remain, &nlri->nlri_data.link.link_desc);
++ } else if (nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV4_PREFIX ||
++ nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV6_PREFIX) {
++ format_node_desc(&p, &remain, &nlri->nlri_data.prefix.local_node, "N");
++
++ /* Format prefix */
++ len = snprintfrr(p, remain, "[P[p");
++ p += len;
++ remain -= len;
++
++ if (nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV4_PREFIX) {
++ inet_ntop(AF_INET, &nlri->nlri_data.prefix.prefix_desc.prefix.u.prefix4,
++ tmp, sizeof(tmp));
++ } else {
++ inet_ntop(AF_INET6, &nlri->nlri_data.prefix.prefix_desc.prefix.u.prefix6,
++ tmp, sizeof(tmp));
++ }
++ len = snprintfrr(p, remain, "%s/%u", tmp,
++ nlri->nlri_data.prefix.prefix_desc.prefix.prefixlen);
++ p += len;
++ remain -= len;
++
++ len = snprintfrr(p, remain, "]]");
++ p += len;
++ remain -= len;
++ }
++}
++
++/*
++ * Helper function to lookup BGP-LS NLRI by string representation
++ */
++struct bgp_dest *bgp_ls_lookup_nlri_by_str(struct bgp *bgp, const char *nlri_str)
++{
++ struct bgp_table *table;
++ struct bgp_dest *dest;
++ char formatted_nlri[1024];
++ struct bgp_ls_nlri *entry;
++
++ if (!bgp || !bgp->ls_info)
++ return NULL;
++
++ table = bgp->rib[AFI_BGP_LS][SAFI_BGP_LS];
++ if (!table)
++ return NULL;
++
++ /* Iterate through the table and match formatted NLRI string */
++ for (dest = bgp_table_top(table); dest; dest = bgp_route_next(dest)) {
++ entry = dest->ls_nlri;
++ if (!entry)
++ continue;
++ bgp_ls_nlri_format(entry, formatted_nlri, sizeof(formatted_nlri));
++ if (strcmp(formatted_nlri, nlri_str) == 0)
++ return dest;
++ }
++
++ return NULL;
++}
++
++/*
++ * ===========================================================================
++ * RIB Operations
++ * ===========================================================================
++ */
++
++/*
++ * Install or update BGP-LS route in RIB (RFC 9552 Section 6)
++ *
++ * This function handles locally originated BGP-LS routes from IGP.
++ * It creates a synthetic prefix in the standard BGP RIB using AF_UNSPEC
++ * and stores the full NLRI in bgp_path_info_extra->ls_nlri.
++ *
++ * The function uses a dual-storage approach:
++ * 1. BGP-LS hash table - for fast NLRI lookups and ID allocation
++ * 2. Standard BGP RIB - for integration with existing BGP processing
++ *
++ * @param bgp - BGP instance
++ * @param nlri - BGP-LS NLRI to install
++ * @return 0 on success, -1 on error
++ */
++int bgp_ls_update(struct bgp *bgp, struct bgp_ls_nlri *nlri, struct bgp_ls_attr *ls_attr)
++{
++ struct bgp_path_info *bpi;
++ struct bgp_path_info *new;
++ struct attr attr;
++ struct attr *attr_new;
++ struct bgp_dest *dest;
++ struct bgp_ls_nlri *ls_nlri;
++ struct prefix p;
++
++ if (!bgp || !nlri) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Invalid parameters to %s", __func__);
++ return -1;
++ }
++
++ if (CHECK_FLAG(bgp->flags, BGP_FLAG_DELETE_IN_PROGRESS) || bgp->peer_self == NULL)
++ return 0;
++
++ if (!bgp->ls_info) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: ls_info not initialized");
++ return -1;
++ }
++
++ /* Lookup or insert NLRI in hash table */
++ ls_nlri = bgp_ls_nlri_intern(nlri);
++
++ /* Make BGP-LS NLRI prefix */
++ memset(&p, 0, sizeof(p));
++ p.family = AF_UNSPEC;
++ p.prefixlen = 32;
++ p.u.val32[0] = ls_nlri->id;
++
++ dest = bgp_afi_node_get(bgp->rib[AFI_BGP_LS][SAFI_BGP_LS], AFI_BGP_LS, SAFI_BGP_LS, &p,
++ NULL);
++
++ /*
++ * Unintern any existing NLRI reference before installing the new one
++ * to avoid leaking the previous interned pointer.
++ */
++ if (dest->ls_nlri)
++ bgp_ls_nlri_unintern(&dest->ls_nlri);
++
++ dest->ls_nlri = ls_nlri;
++
++ /* Make default attribute. */
++ bgp_attr_default_set(&attr, bgp, BGP_ORIGIN_INCOMPLETE);
++
++ attr.ls_attr = ls_attr;
++
++ attr_new = bgp_attr_intern(&attr);
++
++ for (bpi = bgp_dest_get_bgp_path_info(dest); bpi; bpi = bpi->next)
++ if (bpi->peer == bgp->peer_self)
++ break;
++
++ if (bpi) {
++ if (!CHECK_FLAG(bpi->flags, BGP_PATH_REMOVED) && attrhash_cmp(bpi->attr, attr_new)) {
++ /* The attribute is not changed. */
++ bgp_attr_unintern(&attr_new);
++ aspath_unintern(&attr.aspath);
++ bgp_dest_unlock_node(dest);
++ } else {
++ /* The attribute is changed. */
++ bgp_path_info_set_flag(dest, bpi, BGP_PATH_ATTR_CHANGED);
++
++ UNSET_FLAG(bpi->flags, BGP_PATH_REMOVED);
++ /* unset of previous already took care of pcount */
++ SET_FLAG(bpi->flags, BGP_PATH_VALID);
++
++ bgp_attr_unintern(&bpi->attr);
++ bpi->attr = attr_new;
++ bpi->uptime = monotime(NULL);
++
++ /* Process change. */
++ bgp_process(bgp, dest, bpi, AFI_BGP_LS, SAFI_BGP_LS);
++ bgp_dest_unlock_node(dest);
++ aspath_unintern(&attr.aspath);
++ }
++
++ return 0;
++ }
++
++ /* Make new BGP info. */
++ new = info_make(ZEBRA_ROUTE_BGP, BGP_ROUTE_REDISTRIBUTE, 0, bgp->peer_self, attr_new, dest);
++ SET_FLAG(new->flags, BGP_PATH_VALID);
++
++ /* Register new BGP information. */
++ bgp_path_info_add(dest, new);
++
++ /* Process change */
++ bgp_process(bgp, dest, new, AFI_BGP_LS, SAFI_BGP_LS);
++
++ /* Unlock node from bgp_afi_node_get */
++ bgp_dest_unlock_node(dest);
++
++ /* Unintern original */
++ aspath_unintern(&attr.aspath);
++
++ return 0;
++}
++
++
++/*
++ * Remove BGP-LS route from RIB (RFC 9552 Section 6)
++ *
++ * This function handles withdrawal of locally originated BGP-LS routes.
++ * It marks the route as removed and triggers BGP processing for
++ * withdrawal advertisement to peers.
++ *
++ * @param bgp - BGP instance
++ * @param nlri - BGP-LS NLRI to withdraw
++ * @return 0 on success, -1 on error
++ */
++int bgp_ls_withdraw(struct bgp *bgp, struct bgp_ls_nlri *nlri)
++{
++ struct bgp_path_info *bpi;
++ struct bgp_dest *dest;
++ struct prefix p;
++ struct bgp_ls_nlri *ls_nlri;
++
++ if (!bgp || !nlri) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Invalid parameters to %s", __func__);
++ return -1;
++ }
++
++ if (CHECK_FLAG(bgp->flags, BGP_FLAG_DELETE_IN_PROGRESS) || bgp->peer_self == NULL)
++ return 0;
++
++ if (!bgp->ls_info) {
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s: No BGP-LS info exists for withdraw", __func__);
++
++ return 0;
++ }
++
++ /* Lookup NLRI in hash table */
++ ls_nlri = bgp_ls_nlri_lookup(&bgp->ls_info->nlri_hash, nlri);
++ if (!ls_nlri) {
++ if (BGP_DEBUG(linkstate, LINKSTATE)) {
++ zlog_debug("%s: BGP-LS WITHDRAW for non-existent NLRI type=%u", __func__,
++ nlri->nlri_type);
++ }
++ return 0; /* Not an error - may have been withdrawn already */
++ }
++
++ /* Make synthetic prefix using hash table ID */
++ memset(&p, 0, sizeof(p));
++ p.family = AF_UNSPEC;
++ p.prefixlen = 32;
++ p.u.val32[0] = ls_nlri->id;
++
++ dest = bgp_node_lookup(bgp->rib[AFI_BGP_LS][SAFI_BGP_LS], &p);
++ if (!dest) {
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s: No RIB entry found for NLRI type=%u", __func__,
++ nlri->nlri_type);
++ return 0;
++ }
++
++ /* Find path from local peer */
++ for (bpi = bgp_dest_get_bgp_path_info(dest); bpi; bpi = bpi->next)
++ if (bpi->peer == bgp->peer_self)
++ break;
++
++ if (bpi) {
++ if (BGP_DEBUG(linkstate, LINKSTATE)) {
++ zlog_debug("%s: Withdrawing BGP-LS route type=%u", __func__,
++ nlri->nlri_type);
++ }
++
++ /* Mark for deletion */
++ SET_FLAG(bpi->flags, BGP_PATH_REMOVED);
++ UNSET_FLAG(bpi->flags, BGP_PATH_VALID);
++
++ /* Process change - triggers withdrawal to peers */
++ bgp_process(bgp, dest, bpi, AFI_BGP_LS, SAFI_BGP_LS);
++ } else {
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s: No path found for NLRI type=%u", __func__, nlri->nlri_type);
++ }
++
++ /* Unlock node from bgp_node_lookup */
++ bgp_dest_unlock_node(dest);
++
++ return 0;
++}
++
++/*
++ * ===========================================================================
++ * BGP-LS NLRI Parsing
++ * ===========================================================================
++ */
++
++/*
++ * Parse BGP-LS NLRI from UPDATE/WITHDRAW messages
++ *
++ * Called from bgp_nlri_parse() for SAFI_BGP_LS packets.
++ * Decodes NLRIs from MP_REACH_NLRI or MP_UNREACH_NLRI and processes them.
++ *
++ * @param peer - BGP peer sending the NLRI
++ * @param attr - BGP path attributes (NULL for withdrawals)
++ * @param packet - NLRI packet containing AFI/SAFI and NLRI data
++ * @return BGP_NLRI_PARSE_OK on success, error code otherwise
++ */
++int bgp_nlri_parse_ls(struct peer *peer, struct attr *attr, struct bgp_nlri *packet)
++{
++ struct stream *s;
++ struct bgp_ls_nlri *nlri;
++ struct prefix p;
++ struct bgp_ls_nlri *ls_entry;
++ struct bgp_dest *dest;
++ int ret = BGP_NLRI_PARSE_OK;
++
++ if (!peer || !peer->bgp || !peer->bgp->ls_info || !packet)
++ return BGP_NLRI_PARSE_ERROR;
++
++ s = stream_new(packet->length);
++ stream_put(s, packet->nlri, packet->length);
++ stream_set_getp(s, 0);
++
++ while (STREAM_READABLE(s) > 0) {
++ nlri = bgp_ls_nlri_alloc();
++ if (!nlri) {
++ ret = BGP_NLRI_PARSE_ERROR;
++ goto done;
++ }
++
++ ret = bgp_ls_decode_nlri(s, nlri);
++ if (ret < 0) {
++ bgp_ls_nlri_free(nlri);
++ flog_warn(EC_BGP_LS_PACKET, "%s [Error] Failed to decode BGP-LS NLRI",
++ peer->host);
++ ret = BGP_NLRI_PARSE_ERROR;
++ goto done;
++ }
++
++ ls_entry = bgp_ls_nlri_get(&peer->bgp->ls_info->nlri_hash, peer->bgp, nlri);
++
++ memset(&p, 0, sizeof(p));
++ p.family = AF_UNSPEC;
++ p.prefixlen = 32;
++ p.u.val32[0] = ls_entry->id;
++
++ if (attr) {
++ if (attr->ls_attr &&
++ bgp_ls_attr_process_with_protocol_id(attr->ls_attr,
++ bgp_ls_nlri_protocol_id(nlri)) <
++ 0) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "%s [Error] Processing of BGP-LS attributes failed",
++ peer->host);
++ ret = BGP_NLRI_PARSE_ERROR;
++ goto done;
++ }
++
++ dest = bgp_afi_node_get(bgp_get_default()->rib[AFI_BGP_LS][SAFI_BGP_LS],
++ AFI_BGP_LS, SAFI_BGP_LS, &p, NULL);
++ dest->ls_nlri = ls_entry;
++
++ bgp_update(peer, &p, 0, attr, packet->afi, packet->safi, ZEBRA_ROUTE_BGP,
++ BGP_ROUTE_NORMAL, NULL, NULL, 0, 0, NULL);
++
++ bgp_dest_unlock_node(dest);
++ } else
++ bgp_withdraw(peer, &p, 0, packet->afi, packet->safi, ZEBRA_ROUTE_BGP,
++ BGP_ROUTE_NORMAL, NULL, NULL, 0);
++
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s processed BGP-LS %s NLRI type=%u", peer->host,
++ attr ? "UPDATE" : "WITHDRAW", nlri->nlri_type);
++
++ bgp_ls_nlri_free(nlri);
++ }
++
++done:
++ stream_free(s);
++ return ret;
++}
++
++/*
++ * ===========================================================================
++ * BGP-LS Link State Database Registration
++ * ===========================================================================
++ */
++
++/*
++ * Register BGP with zebra link-state database to receive updates from IGPs
++ *
++ * @return true on success, false on failure
++ */
++bool bgp_ls_register(struct bgp *bgp)
++{
++ if (!bgp->ls_info)
++ return false;
++
++ /* Already registered */
++ if (bgp_ls_is_registered(bgp))
++ return true;
++
++ if (ls_register(bgp_zclient, false) != 0) {
++ zlog_err("BGP-LS: Failed to register with Link State database");
++ return false;
++ }
++
++ if (ls_request_sync(bgp_zclient) == -1)
++ zlog_warn("BGP-LS: Failed to request initial Link State database sync");
++ else
++ zlog_info("BGP-LS: Requested initial Link State database sync");
++
++ bgp->ls_info->registered_ls_db = true;
++
++ zlog_info("BGP-LS: Registered with Link State database for BGP instance %s",
++ bgp->name_pretty);
++ return true;
++}
++
++/*
++ * Unregister BGP from zebra link-state database
++ *
++ * @return true on success, false on failure
++ */
++bool bgp_ls_unregister(struct bgp *bgp)
++{
++ if (!bgp->ls_info)
++ return false;
++
++ /* Not registered */
++ if (!bgp_ls_is_registered(bgp))
++ return true;
++
++ /*
++ * Clear the local registration flag *before* the zebra call.
++ *
++ * If ls_unregister() fails, BGP has lost sync with zebra. Leaving
++ * registered_ls_db set to true in that case would make
++ * bgp_ls_is_registered() report "still registered", preventing any
++ * subsequent bgp_ls_register() call from attempting re-registration
++ * and leaving BGP permanently unable to receive link-state updates.
++ *
++ * By clearing the flag eagerly, bgp_ls_register() will see
++ * registered_ls_db=false and attempt re-registration, giving the
++ * system a chance to recover.
++ */
++ bgp->ls_info->registered_ls_db = false;
++
++ if (ls_unregister(bgp_zclient, false) != 0) {
++ zlog_err("BGP-LS: Failed to unregister from Link State database");
++ return false;
++ }
++
++ zlog_info("BGP-LS: Unregistered from Link State database for BGP instance %s",
++ bgp->name_pretty);
++ return true;
++}
++
++/*
++ * Check if BGP is registered with zebra link-state database
++ * Returns true if registered, false otherwise
++ */
++bool bgp_ls_is_registered(struct bgp *bgp)
++{
++ if (!bgp || !bgp->ls_info)
++ return false;
++
++ return bgp->ls_info->registered_ls_db;
++}
++
++/*
++ * ===========================================================================
++ * Module Initialization and Cleanup
++ * ===========================================================================
++ */
++
++/*
++ * Initialize BGP-LS module for a BGP instance
++ * Called from bgp_create() for the default BGP instance only
++ */
++void bgp_ls_init(struct bgp *bgp)
++{
++ if (bgp->inst_type != BGP_INSTANCE_TYPE_DEFAULT)
++ return;
++
++ bgp->ls_info = XCALLOC(MTYPE_BGP_LS, sizeof(struct bgp_ls));
++ bgp->ls_info->bgp = bgp;
++ bgp->ls_info->allocator = idalloc_new("BGP-LS NLRI ID Allocator");
++ bgp_ls_nlri_hash_init(&bgp->ls_info->nlri_hash);
++ bgp_ls_attr_hash_init(&bgp->ls_info->ls_attr_hash);
++
++ bgp->ls_info->ted = ls_ted_new(bgp->as, "BGP-LS TED", bgp->as);
++
++ zlog_info("BGP-LS: Module initialized for instance %s", bgp->name_pretty);
++}
++
++/*
++ * Cleanup BGP-LS module for a BGP instance
++ * Called from bgp_free() for the default BGP instance only
++ */
++void bgp_ls_cleanup(struct bgp *bgp)
++{
++ struct bgp_ls_nlri *entry;
++ struct bgp_ls_attr *ls_attr;
++
++ if (bgp->inst_type != BGP_INSTANCE_TYPE_DEFAULT)
++ return;
++
++ if (!bgp->ls_info)
++ return;
++
++ bgp_ls_unregister(bgp);
++
++ frr_each_safe (bgp_ls_nlri_hash, &bgp->ls_info->nlri_hash, entry) {
++ bgp_ls_nlri_hash_del(&bgp->ls_info->nlri_hash, entry);
++ bgp_ls_nlri_free(entry);
++ }
++ bgp_ls_nlri_hash_fini(&bgp->ls_info->nlri_hash);
++
++ frr_each_safe (bgp_ls_attr_hash, &bgp->ls_info->ls_attr_hash, ls_attr) {
++ bgp_ls_attr_hash_del(&bgp->ls_info->ls_attr_hash, ls_attr);
++ bgp_ls_attr_free(ls_attr);
++ }
++ bgp_ls_attr_hash_fini(&bgp->ls_info->ls_attr_hash);
++
++ ls_ted_del_all(&bgp->ls_info->ted);
++
++ idalloc_destroy(bgp->ls_info->allocator);
++
++ XFREE(MTYPE_BGP_LS, bgp->ls_info);
++
++ zlog_info("BGP-LS: Module terminated for instance %s", bgp->name_pretty);
++}
+diff --git a/bgpd/bgp_ls.h b/bgpd/bgp_ls.h
+new file mode 100644
+index 0000000000..06078783bb
+--- /dev/null
++++ b/bgpd/bgp_ls.h
+@@ -0,0 +1,86 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * BGP Link-State (RFC 9552) - Core Constants and Structures
++ * Copyright (C) 2025 Carmine Scarpitta
++ */
++
++#ifndef _FRR_BGP_LS_H
++#define _FRR_BGP_LS_H
++
++#include "bgpd/bgpd.h"
++#include "bgpd/bgp_ls_nlri.h"
++
++struct bgp_ls {
++ /* Back-pointer to parent BGP instance */
++ struct bgp *bgp;
++
++ /* Hash table for BGP-LS NLRIs (nodes, links, prefixes) */
++ struct bgp_ls_nlri_hash_head nlri_hash;
++
++ /* Hash table for BGP-LS Attributes */
++ struct bgp_ls_attr_hash_head ls_attr_hash;
++
++ /* Traffic Engineering Database */
++ struct ls_ted *ted;
++
++ /* NLRI ID allocator */
++ struct id_alloc *allocator;
++
++ /* Link-state database registration status */
++ bool registered_ls_db;
++};
++
++/* Function prototypes */
++
++/*
++ * ===========================================================================
++ * RIB Operations
++ * ===========================================================================
++ */
++
++/*
++ * Install or update BGP-LS route in RIB
++ *
++ * This function handles locally originated BGP-LS routes from IGP.
++ * It creates a synthetic prefix in the standard BGP RIB and stores
++ * the full NLRI in bgp_path_info_extra->ls_nlri.
++ *
++ * @param bgp - BGP instance
++ * @param nlri - Decoded BGP-LS NLRI
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_update(struct bgp *bgp, struct bgp_ls_nlri *nlri, struct bgp_ls_attr *ls_attr);
++
++/*
++ * Remove BGP-LS route from RIB
++ *
++ * This function handles withdrawal of locally originated BGP-LS routes.
++ * It marks the route as removed and triggers BGP processing for
++ * withdrawal advertisement to peers.
++ *
++ * @param bgp - BGP instance
++ * @param nlri - Decoded BGP-LS NLRI
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_withdraw(struct bgp *bgp, struct bgp_ls_nlri *nlri);
++
++/* BGP-LS NLRI parsing */
++extern int bgp_nlri_parse_ls(struct peer *peer, struct attr *attr, struct bgp_nlri *packet);
++
++/* BGP-LS registration with link-state database */
++extern bool bgp_ls_register(struct bgp *bgp);
++extern bool bgp_ls_unregister(struct bgp *bgp);
++extern bool bgp_ls_is_registered(struct bgp *bgp);
++
++/* BGP-LS NLRI lookup helpers */
++extern struct bgp_dest *bgp_ls_lookup_nlri_by_str(struct bgp *bgp, const char *nlri_str);
++
++/* BGP-LS NLRI helpers */
++extern struct json_object *bgp_ls_nlri_to_json(struct bgp_ls_nlri *nlri);
++extern void bgp_ls_nlri_format(struct bgp_ls_nlri *nlri, char *buf, size_t buf_len);
++
++/* Module initialization and cleanup */
++extern void bgp_ls_init(struct bgp *bgp);
++extern void bgp_ls_cleanup(struct bgp *bgp);
++
++#endif /* _FRR_BGP_LS_H */
+diff --git a/bgpd/bgp_ls_nlri.c b/bgpd/bgp_ls_nlri.c
+new file mode 100644
+index 0000000000..db93d2fc29
+--- /dev/null
++++ b/bgpd/bgp_ls_nlri.c
+@@ -0,0 +1,5406 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * BGP Link-State NLRI (RFC 9552)
++ * Copyright (C) 2025 Carmine Scarpitta
++ */
++
++#include <zebra.h>
++
++#include "bgpd/bgp_ls.h"
++#include "bgpd/bgp_ls_nlri.h"
++#include "bgpd/bgp_errors.h"
++#include "bgpd/bgp_debug.h"
++#include "lib/iso.h"
++
++DEFINE_MTYPE_STATIC(BGPD, BGP_LS_NLRI, "BGP-LS NLRI");
++DEFINE_MTYPE(BGPD, BGP_LS_ATTR, "BGP-LS Attribute");
++
++/*
++ * ===========================================================================
++ * Node Descriptor Functions
++ * ===========================================================================
++ */
++
++/*
++ * Compare two node descriptors for equality
++ * Returns 0 if equal, non-zero otherwise
++ */
++int bgp_ls_node_descriptor_cmp(const struct bgp_ls_node_descriptor *d1,
++ const struct bgp_ls_node_descriptor *d2)
++{
++ int ret;
++
++ /* Must have same TLVs present */
++ if (d1->present_tlvs != d2->present_tlvs)
++ return numcmp(d1->present_tlvs, d2->present_tlvs);
++
++ /* Compare AS Number if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_NODE_DESC_AS_BIT)) {
++ if (d1->asn != d2->asn)
++ return numcmp(d1->asn, d2->asn);
++ }
++
++ /* Compare BGP-LS ID if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT)) {
++ if (d1->bgp_ls_id != d2->bgp_ls_id)
++ return numcmp(d1->bgp_ls_id, d2->bgp_ls_id);
++ }
++
++ /* Compare OSPF Area ID if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT)) {
++ if (d1->ospf_area_id != d2->ospf_area_id)
++ return numcmp(d1->ospf_area_id, d2->ospf_area_id);
++ }
++
++ /* Compare IGP Router ID if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT)) {
++ if (d1->igp_router_id_len != d2->igp_router_id_len)
++ return numcmp(d1->igp_router_id_len, d2->igp_router_id_len);
++ ret = memcmp(d1->igp_router_id, d2->igp_router_id, d1->igp_router_id_len);
++ if (ret != 0)
++ return ret;
++ }
++
++ return 0;
++}
++
++/*
++ * ===========================================================================
++ * Link Descriptor Functions
++ * ===========================================================================
++ */
++
++/*
++ * Compare two link descriptors for equality
++ * Returns 0 if equal, non-zero otherwise
++ */
++int bgp_ls_link_descriptor_cmp(const struct bgp_ls_link_descriptor *d1,
++ const struct bgp_ls_link_descriptor *d2)
++{
++ int ret;
++
++ /* Must have same TLVs present */
++ if (d1->present_tlvs != d2->present_tlvs)
++ return numcmp(d1->present_tlvs, d2->present_tlvs);
++
++ /* Compare Link IDs if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT)) {
++ if (d1->link_local_id != d2->link_local_id)
++ return numcmp(d1->link_local_id, d2->link_local_id);
++ if (d1->link_remote_id != d2->link_remote_id)
++ return numcmp(d1->link_remote_id, d2->link_remote_id);
++ }
++
++ /* Compare IPv4 Interface Address if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT)) {
++ ret = IPV4_ADDR_CMP(&d1->ipv4_intf_addr, &d2->ipv4_intf_addr);
++ if (ret != 0)
++ return ret;
++ }
++
++ /* Compare IPv4 Neighbor Address if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT)) {
++ ret = IPV4_ADDR_CMP(&d1->ipv4_neigh_addr, &d2->ipv4_neigh_addr);
++ if (ret != 0)
++ return ret;
++ }
++
++ /* Compare IPv6 Interface Address if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT)) {
++ ret = IPV6_ADDR_CMP(&d1->ipv6_intf_addr, &d2->ipv6_intf_addr);
++ if (ret != 0)
++ return ret;
++ }
++
++ /* Compare IPv6 Neighbor Address if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT)) {
++ ret = IPV6_ADDR_CMP(&d1->ipv6_neigh_addr, &d2->ipv6_neigh_addr);
++ if (ret != 0)
++ return ret;
++ }
++
++ /* Compare Multi-Topology IDs if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_LINK_DESC_MT_ID_BIT)) {
++ if (d1->mt_id_count != d2->mt_id_count)
++ return numcmp(d1->mt_id_count, d2->mt_id_count);
++ for (int i = 0; i < d1->mt_id_count; i++) {
++ if (d1->mt_id[i] != d2->mt_id[i])
++ return numcmp(d1->mt_id[i], d2->mt_id[i]);
++ }
++ }
++
++ return 0;
++}
++
++/*
++ * ===========================================================================
++ * Prefix Descriptor Functions
++ * ===========================================================================
++ */
++
++/*
++ * Compare two prefix descriptors for equality
++ * Returns 0 if equal, non-zero otherwise
++ */
++int bgp_ls_prefix_descriptor_cmp(const struct bgp_ls_prefix_descriptor *d1,
++ const struct bgp_ls_prefix_descriptor *d2)
++{
++ int ret;
++
++ /* Must have same TLVs present */
++ if (d1->present_tlvs != d2->present_tlvs)
++ return numcmp(d1->present_tlvs, d2->present_tlvs);
++
++ /* Compare prefix */
++ ret = prefix_cmp(&d1->prefix, &d2->prefix);
++ if (ret != 0)
++ return ret;
++
++ /* Compare OSPF Route Type if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT)) {
++ if (d1->ospf_route_type != d2->ospf_route_type)
++ return numcmp(d1->ospf_route_type, d2->ospf_route_type);
++ }
++
++ /* Compare Multi-Topology IDs if present */
++ if (BGP_LS_TLV_CHECK(d1->present_tlvs, BGP_LS_PREFIX_DESC_MT_ID_BIT)) {
++ if (d1->mt_id_count != d2->mt_id_count)
++ return numcmp(d1->mt_id_count, d2->mt_id_count);
++ for (int i = 0; i < d1->mt_id_count; i++) {
++ if (d1->mt_id[i] != d2->mt_id[i])
++ return numcmp(d1->mt_id[i], d2->mt_id[i]);
++ }
++ }
++
++ return 0;
++}
++
++/*
++ * ===========================================================================
++ * NLRI Comparison Functions
++ * ===========================================================================
++ */
++
++/*
++ * Compare two NLRIs for equality
++ * Returns 0 if equal, non-zero otherwise
++ */
++int bgp_ls_nlri_cmp(const struct bgp_ls_nlri *nlri1, const struct bgp_ls_nlri *nlri2)
++{
++ int ret;
++
++ /* Different types are never equal */
++ if (nlri1->nlri_type != nlri2->nlri_type)
++ return numcmp(nlri1->nlri_type, nlri2->nlri_type);
++
++ /* Type-specific comparison */
++ switch (nlri1->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE: {
++ const struct bgp_ls_node_nlri *n1 = &nlri1->nlri_data.node;
++ const struct bgp_ls_node_nlri *n2 = &nlri2->nlri_data.node;
++
++ /* Compare protocol ID */
++ if (n1->protocol_id != n2->protocol_id)
++ return numcmp(n1->protocol_id, n2->protocol_id);
++
++ /* Compare identifier */
++ if (n1->identifier != n2->identifier)
++ return numcmp(n1->identifier, n2->identifier);
++
++ /* Compare local node descriptor */
++ return bgp_ls_node_descriptor_cmp(&n1->local_node, &n2->local_node);
++ }
++
++ case BGP_LS_NLRI_TYPE_LINK: {
++ const struct bgp_ls_link_nlri *l1 = &nlri1->nlri_data.link;
++ const struct bgp_ls_link_nlri *l2 = &nlri2->nlri_data.link;
++
++ /* Compare protocol ID */
++ if (l1->protocol_id != l2->protocol_id)
++ return numcmp(l1->protocol_id, l2->protocol_id);
++
++ /* Compare identifier */
++ if (l1->identifier != l2->identifier)
++ return numcmp(l1->identifier, l2->identifier);
++
++ /* Compare local node descriptor */
++ ret = bgp_ls_node_descriptor_cmp(&l1->local_node, &l2->local_node);
++ if (ret != 0)
++ return ret;
++
++ /* Compare remote node descriptor */
++ ret = bgp_ls_node_descriptor_cmp(&l1->remote_node, &l2->remote_node);
++ if (ret != 0)
++ return ret;
++
++ /* Compare link descriptor */
++ return bgp_ls_link_descriptor_cmp(&l1->link_desc, &l2->link_desc);
++ }
++
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX: {
++ const struct bgp_ls_prefix_nlri *p1 = &nlri1->nlri_data.prefix;
++ const struct bgp_ls_prefix_nlri *p2 = &nlri2->nlri_data.prefix;
++
++ /* Compare protocol ID */
++ if (p1->protocol_id != p2->protocol_id)
++ return numcmp(p1->protocol_id, p2->protocol_id);
++
++ /* Compare identifier */
++ if (p1->identifier != p2->identifier)
++ return numcmp(p1->identifier, p2->identifier);
++
++ /* Compare local node descriptor */
++ ret = bgp_ls_node_descriptor_cmp(&p1->local_node, &p2->local_node);
++ if (ret != 0)
++ return ret;
++
++ /* Compare prefix descriptor */
++ return bgp_ls_prefix_descriptor_cmp(&p1->prefix_desc, &p2->prefix_desc);
++ }
++
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ /* Reserved type should never be compared */
++ return 0;
++ }
++
++ return 0;
++}
++
++/*
++ * Compare two BGP-LS attributes for equality
++ * Returns 0 if equal, non-zero otherwise
++ */
++int bgp_ls_attr_cmp(const struct bgp_ls_attr *attr1, const struct bgp_ls_attr *attr2)
++{
++ int ret;
++
++ if (attr1->present_tlvs != attr2->present_tlvs)
++ return numcmp(attr1->present_tlvs, attr2->present_tlvs);
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_NODE_FLAGS_BIT)) {
++ if (attr1->node_flags != attr2->node_flags)
++ return numcmp(attr1->node_flags, attr2->node_flags);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_NODE_NAME_BIT)) {
++ ret = strcmp(attr1->node_name, attr2->node_name);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_ISIS_AREA_BIT)) {
++ if (attr1->isis_area_id_len != attr2->isis_area_id_len)
++ return numcmp(attr1->isis_area_id_len, attr2->isis_area_id_len);
++ ret = memcmp(attr1->isis_area_id, attr2->isis_area_id, attr1->isis_area_id_len);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_SR_CAPABILITIES_BIT)) {
++ if (attr1->srgb.flag != attr2->srgb.flag)
++ return numcmp(attr1->srgb.flag, attr2->srgb.flag);
++ if (attr1->srgb.lower_bound != attr2->srgb.lower_bound)
++ return numcmp(attr1->srgb.lower_bound, attr2->srgb.lower_bound);
++ if (attr1->srgb.range_size != attr2->srgb.range_size)
++ return numcmp(attr1->srgb.range_size, attr2->srgb.range_size);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_NODE_MSD_BIT)) {
++ if (attr1->msd != attr2->msd)
++ return numcmp(attr1->msd, attr2->msd);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT)) {
++ ret = IPV4_ADDR_CMP(&attr1->ipv4_router_id_local, &attr2->ipv4_router_id_local);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT)) {
++ ret = IPV6_ADDR_CMP(&attr1->ipv6_router_id_local, &attr2->ipv6_router_id_local);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE_BIT)) {
++ ret = IPV4_ADDR_CMP(&attr1->ipv4_router_id_remote, &attr2->ipv4_router_id_remote);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE_BIT)) {
++ ret = IPV6_ADDR_CMP(&attr1->ipv6_router_id_remote, &attr2->ipv6_router_id_remote);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_ADMIN_GROUP_BIT)) {
++ if (attr1->admin_group != attr2->admin_group)
++ return numcmp(attr1->admin_group, attr2->admin_group);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_MAX_LINK_BW_BIT)) {
++ if (attr1->max_link_bw < attr2->max_link_bw)
++ return -1;
++ if (attr1->max_link_bw > attr2->max_link_bw)
++ return 1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_MAX_RESV_BW_BIT)) {
++ if (attr1->max_resv_bw < attr2->max_resv_bw)
++ return -1;
++ if (attr1->max_resv_bw > attr2->max_resv_bw)
++ return 1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_UNRESV_BW_BIT)) {
++ for (int i = 0; i < BGP_LS_MAX_UNRESV_BW; i++) {
++ if (attr1->unreserved_bw[i] < attr2->unreserved_bw[i])
++ return -1;
++ if (attr1->unreserved_bw[i] > attr2->unreserved_bw[i])
++ return 1;
++ }
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_TE_METRIC_BIT)) {
++ if (attr1->te_metric != attr2->te_metric)
++ return numcmp(attr1->te_metric, attr2->te_metric);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_LINK_PROTECTION_BIT)) {
++ if (attr1->link_protection != attr2->link_protection)
++ return numcmp(attr1->link_protection, attr2->link_protection);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_MPLS_PROTOCOL_BIT)) {
++ if (attr1->mpls_protocol_mask != attr2->mpls_protocol_mask)
++ return numcmp(attr1->mpls_protocol_mask, attr2->mpls_protocol_mask);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_IGP_METRIC_BIT)) {
++ if (attr1->igp_metric_len != attr2->igp_metric_len)
++ return numcmp(attr1->igp_metric_len, attr2->igp_metric_len);
++ if (attr1->igp_metric != attr2->igp_metric)
++ return numcmp(attr1->igp_metric, attr2->igp_metric);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_SRLG_BIT)) {
++ if (attr1->srlg_count != attr2->srlg_count)
++ return numcmp(attr1->srlg_count, attr2->srlg_count);
++ for (int i = 0; i < attr1->srlg_count; i++) {
++ if (attr1->srlg_values[i] != attr2->srlg_values[i])
++ return numcmp(attr1->srlg_values[i], attr2->srlg_values[i]);
++ }
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_LINK_NAME_BIT)) {
++ ret = strcmp(attr1->link_name, attr2->link_name);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_ADJ_SID_BIT)) {
++ if (attr1->adj_sid_count != attr2->adj_sid_count)
++ return numcmp(attr1->adj_sid_count, attr2->adj_sid_count);
++ for (int i = 0; i < attr1->adj_sid_count; i++) {
++ if (attr1->adj_sid[i].sid != attr2->adj_sid[i].sid)
++ return numcmp(attr1->adj_sid[i].sid, attr2->adj_sid[i].sid);
++ if (attr1->adj_sid[i].flags != attr2->adj_sid[i].flags)
++ return numcmp(attr1->adj_sid[i].flags, attr2->adj_sid[i].flags);
++ if (attr1->adj_sid[i].weight != attr2->adj_sid[i].weight)
++ return numcmp(attr1->adj_sid[i].weight, attr2->adj_sid[i].weight);
++ }
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_IGP_FLAGS_BIT)) {
++ if (attr1->igp_flags != attr2->igp_flags)
++ return numcmp(attr1->igp_flags, attr2->igp_flags);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_ROUTE_TAG_BIT)) {
++ if (attr1->route_tag_count != attr2->route_tag_count)
++ return numcmp(attr1->route_tag_count, attr2->route_tag_count);
++ for (int i = 0; i < attr1->route_tag_count; i++) {
++ if (attr1->route_tags[i] != attr2->route_tags[i])
++ return numcmp(attr1->route_tags[i], attr2->route_tags[i]);
++ }
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_EXTENDED_TAG_BIT)) {
++ if (attr1->extended_tag_count != attr2->extended_tag_count)
++ return numcmp(attr1->extended_tag_count, attr2->extended_tag_count);
++ for (int i = 0; i < attr1->extended_tag_count; i++) {
++ if (attr1->extended_tags[i] != attr2->extended_tags[i])
++ return numcmp(attr1->extended_tags[i], attr2->extended_tags[i]);
++ }
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_PREFIX_METRIC_BIT)) {
++ if (attr1->prefix_metric != attr2->prefix_metric)
++ return numcmp(attr1->prefix_metric, attr2->prefix_metric);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_OSPF_FWD_ADDR_BIT)) {
++ ret = IPV4_ADDR_CMP(&attr1->ospf_fwd_addr, &attr2->ospf_fwd_addr);
++ if (ret != 0)
++ return ret;
++ ret = IPV6_ADDR_CMP(&attr1->ospf_fwd_addr6, &attr2->ospf_fwd_addr6);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr1->present_tlvs, BGP_LS_ATTR_PREFIX_SID_BIT)) {
++ if (attr1->prefix_sid.sid != attr2->prefix_sid.sid)
++ return numcmp(attr1->prefix_sid.sid, attr2->prefix_sid.sid);
++ if (attr1->prefix_sid.sid_flag != attr2->prefix_sid.sid_flag)
++ return numcmp(attr1->prefix_sid.sid_flag, attr2->prefix_sid.sid_flag);
++ if (attr1->prefix_sid.algo != attr2->prefix_sid.algo)
++ return numcmp(attr1->prefix_sid.algo, attr2->prefix_sid.algo);
++ }
++
++ if (attr1->opaque_len != attr2->opaque_len)
++ return numcmp(attr1->opaque_len, attr2->opaque_len);
++ if (attr1->opaque_len > 0) {
++ ret = memcmp(attr1->opaque_data, attr2->opaque_data, attr1->opaque_len);
++ if (ret != 0)
++ return ret;
++ }
++
++ if (attr1->mt_id_count != attr2->mt_id_count)
++ return numcmp(attr1->mt_id_count, attr2->mt_id_count);
++ for (int i = 0; i < attr1->mt_id_count; i++) {
++ if (attr1->mt_id[i] != attr2->mt_id[i])
++ return numcmp(attr1->mt_id[i], attr2->mt_id[i]);
++ }
++
++ return 0;
++}
++
++bool bgp_ls_attr_same(const struct bgp_ls_attr *attr1, const struct bgp_ls_attr *attr2)
++{
++ if (attr1 == attr2)
++ return true;
++
++ if (!attr1 || !attr2)
++ return false;
++
++ return bgp_ls_attr_cmp(attr1, attr2) == 0;
++}
++
++/*
++ * ===========================================================================
++ * NLRI Memory Management Functions
++ * ===========================================================================
++ */
++
++struct bgp_ls_nlri *bgp_ls_nlri_alloc(void)
++{
++ struct bgp_ls_nlri *nlri;
++
++ nlri = XCALLOC(MTYPE_BGP_LS_NLRI, sizeof(struct bgp_ls_nlri));
++ return nlri;
++}
++
++void bgp_ls_nlri_free(struct bgp_ls_nlri *nlri)
++{
++ if (!nlri)
++ return;
++
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE:
++ break;
++
++ case BGP_LS_NLRI_TYPE_LINK:
++ XFREE(MTYPE_BGP_LS_NLRI, nlri->nlri_data.link.link_desc.mt_id);
++ break;
++
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ XFREE(MTYPE_BGP_LS_NLRI, nlri->nlri_data.prefix.prefix_desc.mt_id);
++ break;
++
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ break;
++ }
++
++ XFREE(MTYPE_BGP_LS_NLRI, nlri);
++}
++
++struct bgp_ls_attr *bgp_ls_attr_alloc(void)
++{
++ struct bgp_ls_attr *attr;
++
++ attr = XCALLOC(MTYPE_BGP_LS_ATTR, sizeof(struct bgp_ls_attr));
++ return attr;
++}
++
++void bgp_ls_attr_free(struct bgp_ls_attr *attr)
++{
++ if (!attr)
++ return;
++
++ XFREE(MTYPE_BGP_LS_ATTR, attr->node_name);
++ XFREE(MTYPE_BGP_LS_ATTR, attr->isis_area_id);
++ XFREE(MTYPE_BGP_LS_ATTR, attr->mt_id);
++ XFREE(MTYPE_BGP_LS_ATTR, attr->srlg_values);
++ XFREE(MTYPE_BGP_LS_ATTR, attr->link_name);
++ XFREE(MTYPE_BGP_LS_ATTR, attr->route_tags);
++ XFREE(MTYPE_BGP_LS_ATTR, attr->extended_tags);
++ XFREE(MTYPE_BGP_LS_ATTR, attr->opaque_data);
++
++ XFREE(MTYPE_BGP_LS_ATTR, attr);
++}
++
++/* Deep copy BGP-LS NLRI (including dynamic fields) */
++struct bgp_ls_nlri *bgp_ls_nlri_copy(const struct bgp_ls_nlri *nlri)
++{
++ struct bgp_ls_nlri *nlri_copy;
++
++ if (!nlri)
++ return NULL;
++
++ /* Allocate and copy NLRI */
++ nlri_copy = XCALLOC(MTYPE_BGP_LS_NLRI, sizeof(*nlri_copy));
++ memcpy(nlri_copy, nlri, sizeof(*nlri_copy));
++ nlri_copy->refcnt = 0;
++
++ /* Copy type-specific dynamically allocated fields */
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE:
++ break;
++
++ case BGP_LS_NLRI_TYPE_LINK:
++ /* Copy link descriptor mt_id */
++ if (nlri->nlri_data.link.link_desc.mt_id) {
++ size_t mt_size = nlri->nlri_data.link.link_desc.mt_id_count *
++ sizeof(uint16_t);
++ nlri_copy->nlri_data.link.link_desc.mt_id = XCALLOC(MTYPE_BGP_LS_NLRI,
++ mt_size);
++ memcpy(nlri_copy->nlri_data.link.link_desc.mt_id,
++ nlri->nlri_data.link.link_desc.mt_id, mt_size);
++ }
++ break;
++
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ /* Copy prefix descriptor mt_id */
++ if (nlri->nlri_data.prefix.prefix_desc.mt_id) {
++ size_t mt_size = nlri->nlri_data.prefix.prefix_desc.mt_id_count *
++ sizeof(uint16_t);
++ nlri_copy->nlri_data.prefix.prefix_desc.mt_id = XCALLOC(MTYPE_BGP_LS_NLRI,
++ mt_size);
++ memcpy(nlri_copy->nlri_data.prefix.prefix_desc.mt_id,
++ nlri->nlri_data.prefix.prefix_desc.mt_id, mt_size);
++ }
++ break;
++
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ break;
++ }
++
++ return nlri_copy;
++}
++
++struct bgp_ls_attr *bgp_ls_attr_copy(const struct bgp_ls_attr *src)
++{
++ struct bgp_ls_attr *dst;
++ size_t mt_size;
++ size_t srlg_size;
++ size_t tag_size;
++
++ if (!src)
++ return NULL;
++
++ dst = XCALLOC(MTYPE_BGP_LS_ATTR, sizeof(*dst));
++ memcpy(dst, src, sizeof(*dst));
++
++ if (src->node_name)
++ dst->node_name = XSTRDUP(MTYPE_BGP_LS_ATTR, src->node_name);
++
++ if (src->isis_area_id) {
++ dst->isis_area_id = XCALLOC(MTYPE_BGP_LS_ATTR, src->isis_area_id_len);
++ memcpy(dst->isis_area_id, src->isis_area_id, src->isis_area_id_len);
++ }
++
++ if (src->mt_id) {
++ mt_size = src->mt_id_count * sizeof(uint16_t);
++ dst->mt_id = XCALLOC(MTYPE_BGP_LS_ATTR, mt_size);
++ memcpy(dst->mt_id, src->mt_id, mt_size);
++ }
++
++ if (src->srlg_values) {
++ srlg_size = src->srlg_count * sizeof(uint32_t);
++ dst->srlg_values = XCALLOC(MTYPE_BGP_LS_ATTR, srlg_size);
++ memcpy(dst->srlg_values, src->srlg_values, srlg_size);
++ }
++
++ if (src->link_name)
++ dst->link_name = XSTRDUP(MTYPE_BGP_LS_ATTR, src->link_name);
++
++ if (src->route_tags) {
++ tag_size = src->route_tag_count * sizeof(uint32_t);
++ dst->route_tags = XCALLOC(MTYPE_BGP_LS_ATTR, tag_size);
++ memcpy(dst->route_tags, src->route_tags, tag_size);
++ }
++
++ if (src->extended_tags) {
++ tag_size = src->extended_tag_count * sizeof(uint64_t);
++ dst->extended_tags = XCALLOC(MTYPE_BGP_LS_ATTR, tag_size);
++ memcpy(dst->extended_tags, src->extended_tags, tag_size);
++ }
++
++ if (src->opaque_data) {
++ dst->opaque_data = XCALLOC(MTYPE_BGP_LS_ATTR, src->opaque_len);
++ memcpy(dst->opaque_data, src->opaque_data, src->opaque_len);
++ }
++
++ return dst;
++}
++
++/*
++ * ===========================================================================
++ * NLRI Validation Functions
++ * ===========================================================================
++ */
++
++/*
++ * Validate IGP Router-ID length based on protocol (RFC 9552 Section 5.2.1.4)
++ *
++ * Valid lengths:
++ * 4 octets - OSPFv2/v3 non-pseudonode, Direct/Static IPv4
++ * 6 octets - IS-IS non-pseudonode
++ * 7 octets - IS-IS pseudonode
++ * 8 octets - OSPFv2/v3 pseudonode
++ * 16 octets - Direct/Static IPv6, BGP IPv6
++ */
++static bool bgp_ls_igp_router_id_len_valid(enum bgp_ls_protocol_id proto_id, uint8_t len)
++{
++ switch (proto_id) {
++ case BGP_LS_PROTO_ISIS_L1:
++ case BGP_LS_PROTO_ISIS_L2:
++ /* IS-IS: 6 octets (non-pseudonode) or 7 octets (pseudonode) */
++ return (len == BGP_LS_IGP_ROUTER_ID_ISIS_LEN ||
++ len == BGP_LS_IGP_ROUTER_ID_ISIS_PSEUDO_LEN);
++
++ case BGP_LS_PROTO_OSPFV2:
++ case BGP_LS_PROTO_OSPFV3:
++ /* OSPF: 4 octets (non-pseudonode) or 8 octets (pseudonode) */
++ return (len == BGP_LS_IGP_ROUTER_ID_OSPF_LEN ||
++ len == BGP_LS_IGP_ROUTER_ID_OSPF_PSEUDO_LEN);
++
++ case BGP_LS_PROTO_DIRECT:
++ case BGP_LS_PROTO_STATIC:
++ /* Direct/Static: Accept IPv4 (4) or IPv6 (16), or IGP formats if IGP is running */
++ return (len == BGP_LS_IGP_ROUTER_ID_DIRECT_IPV4_LEN ||
++ len == BGP_LS_IGP_ROUTER_ID_DIRECT_IPV6_LEN ||
++ len == BGP_LS_IGP_ROUTER_ID_OSPF_LEN ||
++ len == BGP_LS_IGP_ROUTER_ID_ISIS_LEN ||
++ len == BGP_LS_IGP_ROUTER_ID_ISIS_PSEUDO_LEN ||
++ len == BGP_LS_IGP_ROUTER_ID_OSPF_PSEUDO_LEN);
++
++ case BGP_LS_PROTO_BGP:
++ /* BGP: Typically uses IPv4 (4) or IPv6 (16) Router-ID */
++ return (len == BGP_LS_IGP_ROUTER_ID_DIRECT_IPV4_LEN ||
++ len == BGP_LS_IGP_ROUTER_ID_DIRECT_IPV6_LEN);
++
++ case BGP_LS_PROTO_RESERVED:
++ return false;
++ }
++
++ return false;
++}
++
++bool bgp_ls_nlri_validate(const struct bgp_ls_nlri *nlri)
++{
++ if (!nlri)
++ return false;
++
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE: {
++ const struct bgp_ls_node_nlri *n = &nlri->nlri_data.node;
++
++ if (n->protocol_id == BGP_LS_PROTO_RESERVED)
++ return false;
++
++ /* IGP Router ID is mandatory (RFC 9552 Section 5.2.1.4) */
++ if (!BGP_LS_TLV_CHECK(n->local_node.present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT))
++ return false;
++
++ if (!bgp_ls_igp_router_id_len_valid(n->protocol_id,
++ n->local_node.igp_router_id_len))
++ return false;
++
++ return true;
++ }
++
++ case BGP_LS_NLRI_TYPE_LINK: {
++ const struct bgp_ls_link_nlri *l = &nlri->nlri_data.link;
++
++ if (l->protocol_id == BGP_LS_PROTO_RESERVED)
++ return false;
++
++ /* Local node IGP Router ID is mandatory */
++ if (!BGP_LS_TLV_CHECK(l->local_node.present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT))
++ return false;
++
++ /* Remote node IGP Router ID is mandatory */
++ if (!BGP_LS_TLV_CHECK(l->remote_node.present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT))
++ return false;
++
++ if (!bgp_ls_igp_router_id_len_valid(l->protocol_id,
++ l->local_node.igp_router_id_len) ||
++ !bgp_ls_igp_router_id_len_valid(l->protocol_id,
++ l->remote_node.igp_router_id_len))
++ return false;
++
++ return true;
++ }
++
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX: {
++ const struct bgp_ls_prefix_nlri *p = &nlri->nlri_data.prefix;
++
++ if (p->protocol_id == BGP_LS_PROTO_RESERVED)
++ return false;
++
++ /* Local node IGP Router ID is mandatory */
++ if (!BGP_LS_TLV_CHECK(p->local_node.present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT))
++ return false;
++
++ if (!bgp_ls_igp_router_id_len_valid(p->protocol_id,
++ p->local_node.igp_router_id_len))
++ return false;
++
++ /* Prefix family must be IPv4 */
++ if (p->prefix_desc.prefix.family != AF_INET)
++ return false;
++
++ return true;
++ }
++
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX: {
++ const struct bgp_ls_prefix_nlri *p = &nlri->nlri_data.prefix;
++
++ if (p->protocol_id == BGP_LS_PROTO_RESERVED)
++ return false;
++
++ /* Local node IGP Router ID is mandatory */
++ if (!BGP_LS_TLV_CHECK(p->local_node.present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT))
++ return false;
++
++ if (!bgp_ls_igp_router_id_len_valid(p->protocol_id,
++ p->local_node.igp_router_id_len))
++ return false;
++
++ /* Prefix family must be IPv6 */
++ if (p->prefix_desc.prefix.family != AF_INET6)
++ return false;
++
++ return true;
++ }
++
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ /* Reserved type is invalid */
++ return false;
++ }
++
++ return false;
++}
++
++/*
++ * ===========================================================================
++ * NLRI Size Calculation Functions
++ * ===========================================================================
++ */
++
++/* Calculate size of node descriptor TLV */
++static size_t bgp_ls_node_descriptor_size(const struct bgp_ls_node_descriptor *desc)
++{
++ size_t size = BGP_LS_TLV_HDR_SIZE; /* Descriptor TLV header */
++
++ /* AS Number TLV */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_AS_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_AS_NUMBER_SIZE;
++ /* BGP-LS ID TLV */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_BGP_LS_ID_SIZE;
++ /* OSPF Area ID TLV */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_OSPF_AREA_ID_SIZE;
++ /* IGP Router ID TLV */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + desc->igp_router_id_len;
++
++ return size;
++}
++
++/* Calculate wire format size of NLRI */
++size_t bgp_ls_nlri_size(const struct bgp_ls_nlri *nlri)
++{
++ size_t size = 0;
++
++ if (!nlri)
++ return 0;
++
++ /* Common NLRI header: Protocol-ID + Identifier */
++ size += BGP_LS_NLRI_HDR_SIZE;
++
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE: {
++ const struct bgp_ls_node_nlri *n = &nlri->nlri_data.node;
++
++ /* Local Node Descriptor */
++ size += bgp_ls_node_descriptor_size(&n->local_node);
++
++ break;
++ }
++
++ case BGP_LS_NLRI_TYPE_LINK: {
++ const struct bgp_ls_link_nlri *l = &nlri->nlri_data.link;
++
++ /* Local Node Descriptor */
++ size += bgp_ls_node_descriptor_size(&l->local_node);
++
++ /* Remote Node Descriptor */
++ size += bgp_ls_node_descriptor_size(&l->remote_node);
++
++ /* Link Descriptor */
++ if (BGP_LS_TLV_CHECK(l->link_desc.present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_LINK_ID_SIZE;
++ if (BGP_LS_TLV_CHECK(l->link_desc.present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_IPV4_ADDR_SIZE;
++ if (BGP_LS_TLV_CHECK(l->link_desc.present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_IPV4_ADDR_SIZE;
++ if (BGP_LS_TLV_CHECK(l->link_desc.present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_IPV6_ADDR_SIZE;
++ if (BGP_LS_TLV_CHECK(l->link_desc.present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_IPV6_ADDR_SIZE;
++ if (BGP_LS_TLV_CHECK(l->link_desc.present_tlvs, BGP_LS_LINK_DESC_MT_ID_BIT))
++ size += BGP_LS_TLV_HDR_SIZE +
++ (l->link_desc.mt_id_count * BGP_LS_MT_ID_SIZE);
++
++ break;
++ }
++
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX: {
++ const struct bgp_ls_prefix_nlri *p = &nlri->nlri_data.prefix;
++
++ /* Local Node Descriptor */
++ size += bgp_ls_node_descriptor_size(&p->local_node);
++
++ /* Prefix Descriptor */
++ if (BGP_LS_TLV_CHECK(p->prefix_desc.present_tlvs, BGP_LS_PREFIX_DESC_MT_ID_BIT))
++ size += BGP_LS_TLV_HDR_SIZE +
++ (p->prefix_desc.mt_id_count * BGP_LS_MT_ID_SIZE);
++ if (BGP_LS_TLV_CHECK(p->prefix_desc.present_tlvs,
++ BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT))
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_OSPF_ROUTE_TYPE_SIZE;
++
++ /* IP Reachability Info TLV */
++ if (nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV4_PREFIX)
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_PREFIX_LEN_SIZE +
++ BGP_LS_IPV4_ADDR_SIZE;
++ else
++ size += BGP_LS_TLV_HDR_SIZE + BGP_LS_PREFIX_LEN_SIZE +
++ BGP_LS_IPV6_ADDR_SIZE;
++
++ break;
++ }
++
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ return 0;
++ }
++
++ return size;
++}
++
++/* NLRI get protocol_id helper */
++enum bgp_ls_protocol_id bgp_ls_nlri_protocol_id(const struct bgp_ls_nlri *nlri)
++{
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE:
++ return nlri->nlri_data.node.protocol_id;
++ case BGP_LS_NLRI_TYPE_LINK:
++ return nlri->nlri_data.link.protocol_id;
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ return nlri->nlri_data.prefix.protocol_id;
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ return BGP_LS_PROTO_RESERVED;
++ }
++ return BGP_LS_PROTO_RESERVED;
++}
++
++/*
++ * ===========================================================================
++ * String Conversion Functions
++ * ===========================================================================
++ */
++
++const char *bgp_ls_protocol_id_str(enum bgp_ls_protocol_id proto_id)
++{
++ switch (proto_id) {
++ case BGP_LS_PROTO_RESERVED:
++ return "Reserved";
++ case BGP_LS_PROTO_ISIS_L1:
++ return "IS-IS Level 1";
++ case BGP_LS_PROTO_ISIS_L2:
++ return "IS-IS Level 2";
++ case BGP_LS_PROTO_OSPFV2:
++ return "OSPFv2";
++ case BGP_LS_PROTO_DIRECT:
++ return "Direct";
++ case BGP_LS_PROTO_STATIC:
++ return "Static";
++ case BGP_LS_PROTO_OSPFV3:
++ return "OSPFv3";
++ case BGP_LS_PROTO_BGP:
++ return "BGP";
++ }
++
++ return "Unknown";
++}
++
++const char *bgp_ls_nlri_type_str(enum bgp_ls_nlri_type nlri_type)
++{
++ switch (nlri_type) {
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ return "Reserved";
++ case BGP_LS_NLRI_TYPE_NODE:
++ return "Node";
++ case BGP_LS_NLRI_TYPE_LINK:
++ return "Link";
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ return "IPv4 Prefix";
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ return "IPv6 Prefix";
++ }
++
++ return "Unknown";
++}
++
++const char *bgp_ls_node_descriptor_tlv_str(enum bgp_ls_node_descriptor_tlv tlv_type)
++{
++ switch (tlv_type) {
++ case BGP_LS_TLV_LOCAL_NODE_DESC:
++ return "Local Node Descriptors";
++ case BGP_LS_TLV_REMOTE_NODE_DESC:
++ return "Remote Node Descriptors";
++ case BGP_LS_TLV_AS_NUMBER:
++ return "Autonomous System";
++ case BGP_LS_TLV_BGP_LS_ID:
++ return "BGP-LS Identifier";
++ case BGP_LS_TLV_OSPF_AREA_ID:
++ return "OSPF Area-ID";
++ case BGP_LS_TLV_IGP_ROUTER_ID:
++ return "IGP Router-ID";
++ }
++
++ return "Unknown";
++}
++
++const char *bgp_ls_link_descriptor_tlv_str(enum bgp_ls_link_descriptor_tlv tlv_type)
++{
++ switch (tlv_type) {
++ case BGP_LS_TLV_LINK_ID:
++ return "Link Local/Remote Identifiers";
++ case BGP_LS_TLV_IPV4_INTF_ADDR:
++ return "IPv4 Interface Address";
++ case BGP_LS_TLV_IPV4_NEIGH_ADDR:
++ return "IPv4 Neighbor Address";
++ case BGP_LS_TLV_IPV6_INTF_ADDR:
++ return "IPv6 Interface Address";
++ case BGP_LS_TLV_IPV6_NEIGH_ADDR:
++ return "IPv6 Neighbor Address";
++ case BGP_LS_TLV_MT_ID:
++ return "Multi-Topology Identifier";
++ case BGP_LS_TLV_REMOTE_AS_NUMBER:
++ return "Remote AS Number";
++ }
++
++ return "Unknown";
++}
++
++const char *bgp_ls_prefix_descriptor_tlv_str(enum bgp_ls_prefix_descriptor_tlv tlv_type)
++{
++ switch (tlv_type) {
++ case BGP_LS_TLV_OSPF_ROUTE_TYPE:
++ return "OSPF Route Type";
++ case BGP_LS_TLV_IP_REACH_INFO:
++ return "IP Reachability Information";
++ }
++
++ return "Unknown";
++}
++
++const char *bgp_ls_ospf_route_type_str(enum bgp_ls_ospf_route_type route_type)
++{
++ switch (route_type) {
++ case BGP_LS_OSPF_RT_INTRA_AREA:
++ return "Intra-Area";
++ case BGP_LS_OSPF_RT_INTER_AREA:
++ return "Inter-Area";
++ case BGP_LS_OSPF_RT_EXTERNAL_1:
++ return "External Type 1";
++ case BGP_LS_OSPF_RT_EXTERNAL_2:
++ return "External Type 2";
++ case BGP_LS_OSPF_RT_NSSA_1:
++ return "NSSA Type 1";
++ case BGP_LS_OSPF_RT_NSSA_2:
++ return "NSSA Type 2";
++ }
++
++ return "Unknown";
++}
++
++const char *bgp_ls_ospf_route_type_str_json(enum bgp_ls_ospf_route_type route_type)
++{
++ switch (route_type) {
++ case BGP_LS_OSPF_RT_INTRA_AREA:
++ return "intraArea";
++ case BGP_LS_OSPF_RT_INTER_AREA:
++ return "interArea";
++ case BGP_LS_OSPF_RT_EXTERNAL_1:
++ return "externalType1";
++ case BGP_LS_OSPF_RT_EXTERNAL_2:
++ return "externalType2";
++ case BGP_LS_OSPF_RT_NSSA_1:
++ return "nssaType1";
++ case BGP_LS_OSPF_RT_NSSA_2:
++ return "nssaType2";
++ }
++
++ return "Unknown";
++}
++
++/*
++ * ===========================================================================
++ * Hash Key Functions
++ * ===========================================================================
++ */
++
++/* Hash a node descriptor */
++static unsigned int bgp_ls_node_descriptor_hash(const struct bgp_ls_node_descriptor *desc,
++ uint32_t key)
++{
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_AS_BIT))
++ key = jhash_1word(desc->asn, key);
++
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT))
++ key = jhash_1word(desc->bgp_ls_id, key);
++
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT))
++ key = jhash_1word(desc->ospf_area_id, key);
++
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT))
++ key = jhash(desc->igp_router_id, desc->igp_router_id_len, key);
++
++ return key;
++}
++
++/* Hash key generation for Node NLRI */
++static unsigned int bgp_ls_node_hash_key_internal(const struct bgp_ls_nlri *nlri)
++{
++ const struct bgp_ls_node_nlri *node = &nlri->nlri_data.node;
++ uint32_t key = 0;
++
++ /* Hash protocol_id and identifier */
++ key = jhash_2words(node->protocol_id, (uint32_t)(node->identifier >> 32), key);
++ key = jhash_1word((uint32_t)node->identifier, key);
++
++ /* Hash node descriptor */
++ key = bgp_ls_node_descriptor_hash(&node->local_node, key);
++
++ return key;
++}
++
++/* Hash key generation for Link NLRI */
++static unsigned int bgp_ls_link_hash_key_internal(const struct bgp_ls_nlri *nlri)
++{
++ const struct bgp_ls_link_nlri *link = &nlri->nlri_data.link;
++ uint32_t key = 0;
++
++ /* Hash protocol_id and identifier */
++ key = jhash_2words(link->protocol_id, (uint32_t)(link->identifier >> 32), key);
++ key = jhash_1word((uint32_t)link->identifier, key);
++
++ /* Hash local and remote node descriptors */
++ key = bgp_ls_node_descriptor_hash(&link->local_node, key);
++ key = bgp_ls_node_descriptor_hash(&link->remote_node, key);
++
++ /* Hash link descriptor */
++ if (BGP_LS_TLV_CHECK(link->link_desc.present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT))
++ key = jhash_2words(link->link_desc.link_local_id, link->link_desc.link_remote_id,
++ key);
++
++ if (BGP_LS_TLV_CHECK(link->link_desc.present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT))
++ key = jhash_1word(link->link_desc.ipv4_intf_addr.s_addr, key);
++
++ if (BGP_LS_TLV_CHECK(link->link_desc.present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT))
++ key = jhash_1word(link->link_desc.ipv4_neigh_addr.s_addr, key);
++
++ if (BGP_LS_TLV_CHECK(link->link_desc.present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT))
++ key = jhash(&link->link_desc.ipv6_intf_addr, sizeof(struct in6_addr), key);
++
++ if (BGP_LS_TLV_CHECK(link->link_desc.present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT))
++ key = jhash(&link->link_desc.ipv6_neigh_addr, sizeof(struct in6_addr), key);
++
++ return key;
++}
++
++/* Hash key generation for Prefix NLRI (IPv4 and IPv6) */
++static unsigned int bgp_ls_prefix_hash_key_internal(const struct bgp_ls_nlri *nlri)
++{
++ const struct bgp_ls_prefix_nlri *prefix = &nlri->nlri_data.prefix;
++ uint32_t key = 0;
++
++ /* Hash protocol_id and identifier */
++ key = jhash_2words(prefix->protocol_id, (uint32_t)(prefix->identifier >> 32), key);
++ key = jhash_1word((uint32_t)prefix->identifier, key);
++
++ /* Hash local node descriptor */
++ key = bgp_ls_node_descriptor_hash(&prefix->local_node, key);
++
++ /* Hash prefix descriptor */
++ key = jhash_1word(prefix->prefix_desc.prefix.prefixlen, key);
++
++ if (prefix->prefix_desc.prefix.family == AF_INET)
++ key = jhash_1word(prefix->prefix_desc.prefix.u.prefix4.s_addr, key);
++ else if (prefix->prefix_desc.prefix.family == AF_INET6)
++ key = jhash(&prefix->prefix_desc.prefix.u.prefix6, sizeof(struct in6_addr), key);
++
++ /* Hash OSPF route type if present */
++ if (BGP_LS_TLV_CHECK(prefix->prefix_desc.present_tlvs, BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT))
++ key = jhash_1word(prefix->prefix_desc.ospf_route_type, key);
++
++ return key;
++}
++
++/* Hash key generation for BGP-LS NLRI */
++unsigned int bgp_ls_nlri_hash_key(const struct bgp_ls_nlri *nlri)
++{
++ /* Include NLRI type in hash to ensure different types don't collide */
++ uint32_t key = jhash_1word(nlri->nlri_type, 0);
++
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE:
++ return key ^ bgp_ls_node_hash_key_internal(nlri);
++ case BGP_LS_NLRI_TYPE_LINK:
++ return key ^ bgp_ls_link_hash_key_internal(nlri);
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ return key ^ bgp_ls_prefix_hash_key_internal(nlri);
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ return key;
++ }
++
++ return key;
++}
++
++/*
++ * Generate hash key for BGP-LS attribute
++ * This hashes all fields present in the attribute based on present_tlvs bitmask
++ */
++unsigned int bgp_ls_attr_hash_key(const struct bgp_ls_attr *attr)
++{
++ uint32_t key = 0;
++
++ if (!attr)
++ return 0;
++
++ /* Hash the present_tlvs bitmask first */
++ key = jhash_1word((uint32_t)(attr->present_tlvs >> 32), key);
++ key = jhash_1word((uint32_t)attr->present_tlvs, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_FLAGS_BIT))
++ key = jhash_1word(attr->node_flags, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_NAME_BIT))
++ key = jhash(attr->node_name, strlen(attr->node_name), key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ISIS_AREA_BIT))
++ key = jhash(attr->isis_area_id, attr->isis_area_id_len, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_SR_CAPABILITIES_BIT)) {
++ key = jhash(&attr->srgb.flag, sizeof(attr->srgb.flag), key);
++ key = jhash(&attr->srgb.lower_bound, sizeof(attr->srgb.lower_bound), key);
++ key = jhash(&attr->srgb.range_size, sizeof(attr->srgb.range_size), key);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_MSD_BIT))
++ key = jhash(&attr->msd, sizeof(attr->msd), key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT))
++ key = jhash_1word(attr->ipv4_router_id_local.s_addr, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT))
++ key = jhash(&attr->ipv6_router_id_local, sizeof(struct in6_addr), key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE_BIT))
++ key = jhash_1word(attr->ipv4_router_id_remote.s_addr, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE_BIT))
++ key = jhash(&attr->ipv6_router_id_remote, sizeof(struct in6_addr), key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ADMIN_GROUP_BIT))
++ key = jhash_1word(attr->admin_group, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MAX_LINK_BW_BIT)) {
++ uint32_t bw;
++
++ memcpy(&bw, &attr->max_link_bw, sizeof(uint32_t));
++ key = jhash_1word(bw, key);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MAX_RESV_BW_BIT)) {
++ uint32_t bw;
++
++ memcpy(&bw, &attr->max_resv_bw, sizeof(uint32_t));
++ key = jhash_1word(bw, key);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_UNRESV_BW_BIT))
++ key = jhash(attr->unreserved_bw, sizeof(attr->unreserved_bw), key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_TE_METRIC_BIT))
++ key = jhash_1word(attr->te_metric, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_LINK_PROTECTION_BIT))
++ key = jhash_1word(attr->link_protection, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MPLS_PROTOCOL_BIT))
++ key = jhash_1word(attr->mpls_protocol_mask, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IGP_METRIC_BIT))
++ key = jhash_1word(attr->igp_metric, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_SRLG_BIT)) {
++ for (int i = 0; i < attr->srlg_count; i++)
++ key = jhash_1word(attr->srlg_values[i], key);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_LINK_NAME_BIT))
++ key = jhash(attr->link_name, strlen(attr->link_name), key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ADJ_SID_BIT)) {
++ key = jhash(&attr->adj_sid_count, sizeof(attr->adj_sid_count), key);
++ for (int i = 0; i < attr->adj_sid_count; i++) {
++ key = jhash(&attr->adj_sid[i].sid, sizeof(attr->adj_sid[i].sid), key);
++ key = jhash(&attr->adj_sid[i].flags, sizeof(attr->adj_sid[i].flags), key);
++ key = jhash(&attr->adj_sid[i].weight, sizeof(attr->adj_sid[i].weight), key);
++ }
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IGP_FLAGS_BIT))
++ key = jhash_1word(attr->igp_flags, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ROUTE_TAG_BIT)) {
++ for (int i = 0; i < attr->route_tag_count; i++)
++ key = jhash_1word(attr->route_tags[i], key);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_EXTENDED_TAG_BIT)) {
++ for (int i = 0; i < attr->extended_tag_count; i++) {
++ key = jhash_1word((uint32_t)(attr->extended_tags[i] >> 32), key);
++ key = jhash_1word((uint32_t)attr->extended_tags[i], key);
++ }
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_PREFIX_METRIC_BIT))
++ key = jhash_1word(attr->prefix_metric, key);
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_OSPF_FWD_ADDR_BIT)) {
++ key = jhash_1word(attr->ospf_fwd_addr.s_addr, key);
++ key = jhash(&attr->ospf_fwd_addr6, sizeof(struct in6_addr), key);
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_PREFIX_SID_BIT))
++ key = jhash(&attr->prefix_sid, sizeof(attr->prefix_sid), key);
++
++ if (attr->opaque_len > 0)
++ key = jhash(attr->opaque_data, attr->opaque_len, key);
++
++ for (int i = 0; i < attr->mt_id_count; i++)
++ key = jhash_1word(attr->mt_id[i], key);
++
++ return key;
++}
++
++/*
++ * ===========================================================================
++ * Hash Comparison Functions
++ * ===========================================================================
++ */
++
++/*
++ * Hash table comparison function for BGP-LS NLRI
++ * Returns 0 if equal, non-zero if not equal
++ */
++int bgp_ls_nlri_hash_cmp(const struct bgp_ls_nlri *n1, const struct bgp_ls_nlri *n2)
++{
++ return bgp_ls_nlri_cmp(n1, n2);
++}
++
++/*
++ * Hash table comparison function for BGP-LS attributes
++ * Returns 0 if equal, non-zero if not equal
++ */
++int bgp_ls_attr_hash_cmp(const struct bgp_ls_attr *a1, const struct bgp_ls_attr *a2)
++{
++ return bgp_ls_attr_cmp(a1, a2);
++}
++
++/*
++ * ===========================================================================
++ * BGP-LS NLRI Hash Table Management Functions
++ * ===========================================================================
++ */
++
++/* Insert or lookup NLRI in hash table */
++struct bgp_ls_nlri *bgp_ls_nlri_get(struct bgp_ls_nlri_hash_head *hash, struct bgp *bgp,
++ struct bgp_ls_nlri *nlri)
++{
++ struct bgp_ls_nlri *ls_nlri;
++
++ ls_nlri = bgp_ls_nlri_lookup(hash, nlri);
++ if (!ls_nlri) {
++ ls_nlri = bgp_ls_nlri_copy(nlri);
++ ls_nlri->id = idalloc_allocate(bgp->ls_info->allocator);
++
++ bgp_ls_nlri_hash_add(hash, ls_nlri);
++ }
++
++ ls_nlri->refcnt++;
++ return ls_nlri;
++}
++
++/* Lookup NLRI in BGP-LS NLRI hash table */
++struct bgp_ls_nlri *bgp_ls_nlri_lookup(struct bgp_ls_nlri_hash_head *hash, struct bgp_ls_nlri *nlri)
++{
++ struct bgp_ls_nlri lookup;
++
++ if (!hash)
++ return NULL;
++
++ memset(&lookup, 0, sizeof(lookup));
++ lookup.nlri_type = nlri->nlri_type;
++ lookup.nlri_data = nlri->nlri_data;
++
++ return bgp_ls_nlri_hash_find(hash, &lookup);
++}
++
++/* Insert or lookup BGP-LS attribute in hash table */
++struct bgp_ls_attr *bgp_ls_attr_get(struct bgp_ls_attr_hash_head *hash, struct bgp *bgp,
++ struct bgp_ls_attr *attr)
++{
++ struct bgp_ls_attr *ls_attr;
++
++ ls_attr = bgp_ls_attr_lookup(hash, attr);
++ if (!ls_attr) {
++ ls_attr = bgp_ls_attr_copy(attr);
++
++ bgp_ls_attr_hash_add(hash, ls_attr);
++ }
++
++ ls_attr->refcnt++;
++ return ls_attr;
++}
++
++/*
++ * Lookup BGP-LS attribute in hash table
++ *
++ * Parameters:
++ * hash - Hash table head
++ * attr - Attribute to look up
++ *
++ * Returns:
++ * Pointer to matching attribute if found, NULL otherwise
++ */
++struct bgp_ls_attr *bgp_ls_attr_lookup(struct bgp_ls_attr_hash_head *hash,
++ const struct bgp_ls_attr *attr)
++{
++ struct bgp_ls_attr lookup;
++
++ if (!hash || !attr)
++ return NULL;
++
++ /* Set up lookup key - copy the attribute for comparison */
++ memcpy(&lookup, attr, sizeof(lookup));
++
++ return bgp_ls_attr_hash_find(hash, &lookup);
++}
++
++/*
++ * ===========================================================================
++ * NLRI Interning (Lookup + Lock/Unlock)
++ * ===========================================================================
++ */
++
++/*
++ * Intern BGP-LS NLRI (lookup and lock)
++ *
++ * Looks up an existing NLRI in the hash table and increments its reference count.
++ * Returns the NLRI if found, NULL otherwise.
++ */
++struct bgp_ls_nlri *bgp_ls_nlri_intern(struct bgp_ls_nlri *ls_nlri)
++{
++ struct bgp *bgp = bgp_get_default();
++
++ if (!bgp || !bgp->ls_info || !ls_nlri)
++ return NULL;
++
++ return bgp_ls_nlri_get(&bgp->ls_info->nlri_hash, bgp, ls_nlri);
++}
++
++/*
++ * Unintern BGP-LS NLRI (unlock and potentially free)
++ *
++ * Decrements the reference count and frees the NLRI if it reaches zero.
++ */
++void bgp_ls_nlri_unintern(struct bgp_ls_nlri **pls_nlri)
++{
++ struct bgp_ls_nlri *ls_nlri = *pls_nlri;
++ struct bgp *bgp = bgp_get_default();
++
++ if (!bgp || !bgp->ls_info || !ls_nlri)
++ return;
++
++ ls_nlri->refcnt--;
++
++ if (ls_nlri->refcnt == 0) {
++ bgp_ls_nlri_hash_del(&bgp->ls_info->nlri_hash, ls_nlri);
++ bgp_ls_nlri_free(ls_nlri);
++ *pls_nlri = NULL;
++ }
++}
++
++/*
++ * Intern BGP-LS attribute (lookup and lock)
++ *
++ * Looks up an existing BGP-LS attribute in the hash table and increments its reference count.
++ * Returns the BGP-LS attribute if found, NULL otherwise.
++ */
++struct bgp_ls_attr *bgp_ls_attr_intern(struct bgp_ls_attr *ls_attr)
++{
++ struct bgp *bgp = bgp_get_default();
++
++ if (!bgp || !bgp->ls_info || !ls_attr)
++ return NULL;
++
++ return bgp_ls_attr_get(&bgp->ls_info->ls_attr_hash, bgp, ls_attr);
++}
++
++/*
++ * Unintern BGP-LS attribute (unlock and potentially free)
++ *
++ * Decrements the reference count and frees the BGP-LS attribute if it reaches zero.
++ */
++void bgp_ls_attr_unintern(struct bgp_ls_attr **pls_attr)
++{
++ struct bgp_ls_attr *ls_attr = *pls_attr;
++ struct bgp *bgp = bgp_get_default();
++
++ if (!bgp || !bgp->ls_info || !ls_attr)
++ return;
++
++ ls_attr->refcnt--;
++
++ if (ls_attr->refcnt == 0) {
++ bgp_ls_attr_hash_del(&bgp->ls_info->ls_attr_hash, ls_attr);
++ bgp_ls_attr_free(ls_attr);
++ *pls_attr = NULL;
++ }
++}
++
++/*
++ * ===========================================================================
++ * NLRI Encoding Functions
++ * ===========================================================================
++ */
++
++/*
++ * Write TLV header (Type + Length) to stream
++ *
++ * Wire format (RFC 9552 Section 3.1):
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Type | Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Returns number of bytes written, or -1 on error
++ */
++static inline int stream_put_tlv_hdr(struct stream *s, uint16_t type, uint16_t length)
++{
++ if (STREAM_WRITEABLE(s) < BGP_LS_TLV_HDR_SIZE)
++ return -1;
++
++ stream_putw(s, type);
++ stream_putw(s, length);
++ return BGP_LS_TLV_HDR_SIZE;
++}
++
++/*
++ * Put a complete TLV (Type + Length + Value) into stream
++ * Returns 0 on success, -1 on error
++ */
++static inline int stream_put_tlv(struct stream *s, uint16_t type, uint16_t length,
++ const void *value)
++{
++ if (stream_put_tlv_hdr(s, type, length) < 0)
++ return -1;
++
++ if (length > 0) {
++ if (STREAM_WRITEABLE(s) < length)
++ return -1;
++ stream_put(s, value, length);
++ }
++
++ return 0;
++}
++
++static inline int stream_putf_tlv(struct stream *s, uint16_t type, const float value)
++{
++ if (stream_put_tlv_hdr(s, type, 4) < 0)
++ return -1;
++
++ if (STREAM_WRITEABLE(s) < 4)
++ return -1;
++
++ stream_putf(s, value);
++
++ return 0;
++}
++
++/*
++ * Encode Node Descriptor to wire format (RFC 9552 Section 5.2.1)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Descriptor Type | Descriptor Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Node Descriptor Sub-TLVs (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Node Descriptor Sub-TLVs (RFC 9552 Section 5.2.1.4):
++ * - TLV 512: Autonomous System (4 bytes)
++ * - TLV 513: BGP-LS Identifier (4 bytes, deprecated)
++ * - TLV 514: OSPF Area-ID (4 bytes)
++ * - TLV 515: IGP Router-ID (4-16 bytes, MANDATORY)
++ *
++ * tlv_type: BGP_LS_TLV_LOCAL_NODE_DESC or BGP_LS_TLV_REMOTE_NODE_DESC
++ *
++ * Returns: Number of bytes written, or -1 on error
++ */
++int bgp_ls_encode_node_descriptor(struct stream *s, const struct bgp_ls_node_descriptor *desc,
++ uint16_t tlv_type)
++{
++ size_t len_pos, sub_tlv_start;
++ int written = 0;
++ int ret;
++
++ if (!s || !desc)
++ return -1;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_TLV_HDR_SIZE)
++ return -1;
++
++ /* Write TLV type and reserve space for length */
++ stream_putw(s, tlv_type);
++ len_pos = stream_get_endp(s);
++ stream_putw(s, 0); /* Placeholder for length */
++ written += BGP_LS_TLV_HDR_SIZE;
++
++ sub_tlv_start = stream_get_endp(s);
++
++ /* AS Number (TLV 512) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_AS_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_AS_NUMBER, BGP_LS_AS_NUMBER_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_AS_NUMBER_SIZE)
++ return -1;
++ stream_putl(s, desc->asn);
++ written += BGP_LS_AS_NUMBER_SIZE;
++ }
++
++ /* BGP-LS Identifier (TLV 513) - deprecated but may be present */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_BGP_LS_ID, BGP_LS_BGP_LS_ID_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_BGP_LS_ID_SIZE)
++ return -1;
++ stream_putl(s, desc->bgp_ls_id);
++ written += BGP_LS_BGP_LS_ID_SIZE;
++ }
++
++ /* OSPF Area ID (TLV 514) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_OSPF_AREA_ID, BGP_LS_OSPF_AREA_ID_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_OSPF_AREA_ID_SIZE)
++ return -1;
++ stream_putl(s, desc->ospf_area_id);
++ written += BGP_LS_OSPF_AREA_ID_SIZE;
++ }
++
++ /* IGP Router ID (TLV 515) - MANDATORY */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_IGP_ROUTER_ID, desc->igp_router_id_len);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < desc->igp_router_id_len)
++ return -1;
++ stream_put(s, desc->igp_router_id, desc->igp_router_id_len);
++ written += desc->igp_router_id_len;
++ }
++
++ /* Update length field */
++ uint16_t sub_tlv_len = stream_get_endp(s) - sub_tlv_start;
++
++ stream_putw_at(s, len_pos, sub_tlv_len);
++
++ return written;
++}
++
++/*
++ * Encode Link Descriptor to wire format (RFC 9552 Section 5.2.2)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Link Descriptor TLVs (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Link Descriptor TLVs (RFC 9552 Section 5.2.2):
++ * - TLV 258: Link Local/Remote Identifiers (8 bytes)
++ * - TLV 259: IPv4 Interface Address (4 bytes)
++ * - TLV 260: IPv4 Neighbor Address (4 bytes)
++ * - TLV 261: IPv6 Interface Address (16 bytes)
++ * - TLV 262: IPv6 Neighbor Address (16 bytes)
++ * - TLV 263: Multi-Topology ID (2 bytes per MT-ID)
++ *
++ * Returns: Number of bytes written, or -1 on error
++ */
++int bgp_ls_encode_link_descriptor(struct stream *s, const struct bgp_ls_link_descriptor *desc)
++{
++ int written = 0;
++ int ret;
++ uint16_t i;
++
++ if (!s || !desc)
++ return -1;
++
++ /* Link Local/Remote Identifiers (TLV 258) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_LINK_ID, BGP_LS_LINK_ID_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_LINK_ID_SIZE)
++ return -1;
++ stream_putl(s, desc->link_local_id);
++ stream_putl(s, desc->link_remote_id);
++ written += BGP_LS_LINK_ID_SIZE;
++ }
++
++ /* IPv4 Interface Address (TLV 259) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_IPV4_INTF_ADDR, BGP_LS_IPV4_ADDR_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_IPV4_ADDR_SIZE)
++ return -1;
++ stream_put_ipv4(s, desc->ipv4_intf_addr.s_addr);
++ written += BGP_LS_IPV4_ADDR_SIZE;
++ }
++
++ /* IPv4 Neighbor Address (TLV 260) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_IPV4_NEIGH_ADDR, BGP_LS_IPV4_ADDR_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_IPV4_ADDR_SIZE)
++ return -1;
++ stream_put_ipv4(s, desc->ipv4_neigh_addr.s_addr);
++ written += BGP_LS_IPV4_ADDR_SIZE;
++ }
++
++ /* IPv6 Interface Address (TLV 261) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_IPV6_INTF_ADDR, BGP_LS_IPV6_ADDR_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_IPV6_ADDR_SIZE)
++ return -1;
++ stream_put(s, &desc->ipv6_intf_addr, BGP_LS_IPV6_ADDR_SIZE);
++ written += BGP_LS_IPV6_ADDR_SIZE;
++ }
++
++ /* IPv6 Neighbor Address (TLV 262) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_IPV6_NEIGH_ADDR, BGP_LS_IPV6_ADDR_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_IPV6_ADDR_SIZE)
++ return -1;
++ stream_put(s, &desc->ipv6_neigh_addr, BGP_LS_IPV6_ADDR_SIZE);
++ written += BGP_LS_IPV6_ADDR_SIZE;
++ }
++
++ /* Multi-Topology ID (TLV 263) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_MT_ID_BIT) &&
++ desc->mt_id_count > 0) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_MT_ID,
++ desc->mt_id_count * BGP_LS_MT_ID_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < (size_t)desc->mt_id_count * BGP_LS_MT_ID_SIZE)
++ return -1;
++ for (i = 0; i < desc->mt_id_count; i++)
++ stream_putw(s, desc->mt_id[i]);
++ written += desc->mt_id_count * BGP_LS_MT_ID_SIZE;
++ }
++
++ /* Remote AS Number (TLV 264) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_REMOTE_AS_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_REMOTE_AS_NUMBER, 4);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < 4)
++ return -1;
++ stream_putl(s, desc->remote_asn);
++ written += 4;
++ }
++
++ return written;
++}
++
++/*
++ * Encode Prefix Descriptor to wire format (RFC 9552 Section 5.2.3)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Prefix Descriptor TLVs (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Prefix Descriptor TLVs (RFC 9552 Section 5.2.3):
++ * - TLV 263: Multi-Topology ID (2 bytes per MT-ID)
++ * - TLV 264: OSPF Route Type (1 byte)
++ * - TLV 265: IP Reachability Information (1 + prefix bytes, MANDATORY)
++ *
++ * Returns: Number of bytes written, or -1 on error
++ */
++int bgp_ls_encode_prefix_descriptor(struct stream *s, const struct bgp_ls_prefix_descriptor *desc)
++{
++ int written = 0;
++ int ret;
++ uint16_t i;
++ uint8_t prefix_len_bytes;
++
++ if (!s || !desc)
++ return -1;
++
++ /* Multi-Topology ID (TLV 263) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_PREFIX_DESC_MT_ID_BIT) &&
++ desc->mt_id_count > 0) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_MT_ID,
++ desc->mt_id_count * BGP_LS_MT_ID_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < (size_t)desc->mt_id_count * BGP_LS_MT_ID_SIZE)
++ return -1;
++ for (i = 0; i < desc->mt_id_count; i++)
++ stream_putw(s, desc->mt_id[i]);
++ written += desc->mt_id_count * BGP_LS_MT_ID_SIZE;
++ }
++
++ /* OSPF Route Type (TLV 264) */
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT)) {
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_OSPF_ROUTE_TYPE,
++ BGP_LS_OSPF_ROUTE_TYPE_SIZE);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < BGP_LS_OSPF_ROUTE_TYPE_SIZE)
++ return -1;
++ stream_putc(s, desc->ospf_route_type);
++ written += BGP_LS_OSPF_ROUTE_TYPE_SIZE;
++ }
++
++ /* IP Reachability Information (TLV 265) - MANDATORY */
++ if (desc->prefix.family == AF_INET) {
++ prefix_len_bytes = (desc->prefix.prefixlen + 7) / 8;
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_IP_REACH_INFO,
++ BGP_LS_PREFIX_LEN_SIZE + prefix_len_bytes);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < (size_t)BGP_LS_PREFIX_LEN_SIZE + prefix_len_bytes)
++ return -1;
++ stream_putc(s, desc->prefix.prefixlen);
++ stream_put(s, &desc->prefix.u.prefix4, prefix_len_bytes);
++ written += BGP_LS_PREFIX_LEN_SIZE + prefix_len_bytes;
++ } else if (desc->prefix.family == AF_INET6) {
++ prefix_len_bytes = (desc->prefix.prefixlen + 7) / 8;
++ ret = stream_put_tlv_hdr(s, BGP_LS_TLV_IP_REACH_INFO,
++ BGP_LS_PREFIX_LEN_SIZE + prefix_len_bytes);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ if (STREAM_WRITEABLE(s) < (size_t)BGP_LS_PREFIX_LEN_SIZE + prefix_len_bytes)
++ return -1;
++ stream_putc(s, desc->prefix.prefixlen);
++ stream_put(s, &desc->prefix.u.prefix6, prefix_len_bytes);
++ written += BGP_LS_PREFIX_LEN_SIZE + prefix_len_bytes;
++ }
++
++ return written;
++}
++
++/*
++ * Encode Node NLRI to wire format (RFC 9552 Section 5.2)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (64 bits) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Returns: Number of bytes written, or -1 on error
++ */
++int bgp_ls_encode_node_nlri(struct stream *s, const struct bgp_ls_node_nlri *nlri)
++{
++ int written = 0;
++ int ret;
++
++ if (!s || !nlri)
++ return -1;
++
++ /* Protocol-ID (1 byte) */
++ if (STREAM_WRITEABLE(s) < BGP_LS_PROTOCOL_ID_SIZE)
++ return -1;
++ stream_putc(s, nlri->protocol_id);
++ written += BGP_LS_PROTOCOL_ID_SIZE;
++
++ /* Identifier (8 bytes) */
++ if (STREAM_WRITEABLE(s) < BGP_LS_IDENTIFIER_SIZE)
++ return -1;
++ stream_putq(s, nlri->identifier);
++ written += BGP_LS_IDENTIFIER_SIZE;
++
++ /* Local Node Descriptors */
++ ret = bgp_ls_encode_node_descriptor(s, &nlri->local_node, BGP_LS_TLV_LOCAL_NODE_DESC);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ return written;
++}
++
++/*
++ * Encode Link NLRI to wire format (RFC 9552 Section 5.2)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (64 bits) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Remote Node Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Link Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Returns: Number of bytes written, or -1 on error
++ */
++int bgp_ls_encode_link_nlri(struct stream *s, const struct bgp_ls_link_nlri *nlri)
++{
++ int written = 0;
++ int ret;
++
++ if (!s || !nlri)
++ return -1;
++
++ /* Protocol-ID (1 byte) */
++ if (STREAM_WRITEABLE(s) < BGP_LS_PROTOCOL_ID_SIZE)
++ return -1;
++ stream_putc(s, nlri->protocol_id);
++ written += BGP_LS_PROTOCOL_ID_SIZE;
++
++ /* Identifier (8 bytes) */
++ if (STREAM_WRITEABLE(s) < BGP_LS_IDENTIFIER_SIZE)
++ return -1;
++ stream_putq(s, nlri->identifier);
++ written += BGP_LS_IDENTIFIER_SIZE;
++
++ /* Local Node Descriptors */
++ ret = bgp_ls_encode_node_descriptor(s, &nlri->local_node, BGP_LS_TLV_LOCAL_NODE_DESC);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ /* Remote Node Descriptors */
++ ret = bgp_ls_encode_node_descriptor(s, &nlri->remote_node, BGP_LS_TLV_REMOTE_NODE_DESC);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ /* Link Descriptors */
++ ret = bgp_ls_encode_link_descriptor(s, &nlri->link_desc);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ return written;
++}
++
++/*
++ * Encode Prefix NLRI to wire format (RFC 9552 Section 5.2)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (64 bits) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Prefix Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Note: Same structure for both IPv4 (Type 3) and IPv6 (Type 4)
++ *
++ * nlri_type: BGP_LS_NLRI_TYPE_IPV4_PREFIX or BGP_LS_NLRI_TYPE_IPV6_PREFIX
++ *
++ * Returns: Number of bytes written, or -1 on error
++ */
++int bgp_ls_encode_prefix_nlri(struct stream *s, const struct bgp_ls_prefix_nlri *nlri,
++ enum bgp_ls_nlri_type nlri_type)
++{
++ int written = 0;
++ int ret;
++
++ if (!s || !nlri)
++ return -1;
++
++ /* Validate prefix family matches NLRI type */
++ if ((nlri_type == BGP_LS_NLRI_TYPE_IPV4_PREFIX &&
++ nlri->prefix_desc.prefix.family != AF_INET) ||
++ (nlri_type == BGP_LS_NLRI_TYPE_IPV6_PREFIX &&
++ nlri->prefix_desc.prefix.family != AF_INET6))
++ return -1;
++
++ /* Protocol-ID (1 byte) */
++ if (STREAM_WRITEABLE(s) < BGP_LS_PROTOCOL_ID_SIZE)
++ return -1;
++ stream_putc(s, nlri->protocol_id);
++ written += BGP_LS_PROTOCOL_ID_SIZE;
++
++ /* Identifier (8 bytes) */
++ if (STREAM_WRITEABLE(s) < BGP_LS_IDENTIFIER_SIZE)
++ return -1;
++ stream_putq(s, nlri->identifier);
++ written += BGP_LS_IDENTIFIER_SIZE;
++
++ /* Local Node Descriptors */
++ ret = bgp_ls_encode_node_descriptor(s, &nlri->local_node, BGP_LS_TLV_LOCAL_NODE_DESC);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ /* Prefix Descriptors */
++ ret = bgp_ls_encode_prefix_descriptor(s, &nlri->prefix_desc);
++ if (ret < 0)
++ return -1;
++ written += ret;
++
++ return written;
++}
++
++/*
++ * Encode complete NLRI with Type-Length-Value header
++ *
++ * Wire format (RFC 9552 Section 5.2):
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Type | Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | |
++ * // NLRI Value (variable) //
++ * | |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Where:
++ * Type = NLRI Type (1=Node, 2=Link, 3=IPv4 Prefix, 4=IPv6 Prefix)
++ * Length = Length of NLRI Value in octets
++ * Value = Type-specific NLRI data (Protocol-ID + Identifier + Descriptors)
++ *
++ * This is the top-level encoding used in MP_REACH/MP_UNREACH attributes.
++ *
++ * Returns: Number of bytes written, or -1 on error
++ */
++int bgp_ls_encode_nlri(struct stream *s, const struct bgp_ls_nlri *nlri)
++{
++ size_t len_pos, value_start;
++ int written = 0;
++ int ret = 0;
++
++ if (!s || !nlri)
++ return -1;
++
++ /* Validate NLRI */
++ if (!bgp_ls_nlri_validate(nlri))
++ return -1;
++
++ /* NLRI Type (2 bytes) */
++ if (STREAM_WRITEABLE(s) < BGP_LS_NLRI_TYPE_SIZE)
++ return -1;
++ stream_putw(s, nlri->nlri_type);
++ written += BGP_LS_NLRI_TYPE_SIZE;
++
++ /* Reserve space for NLRI Length */
++ if (STREAM_WRITEABLE(s) < BGP_LS_NLRI_LENGTH_SIZE)
++ return -1;
++ len_pos = stream_get_endp(s);
++ stream_putw(s, 0); /* Placeholder */
++ written += BGP_LS_NLRI_LENGTH_SIZE;
++
++ value_start = stream_get_endp(s);
++
++ /* Encode NLRI Value based on type */
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE:
++ ret = bgp_ls_encode_node_nlri(s, &nlri->nlri_data.node);
++ break;
++
++ case BGP_LS_NLRI_TYPE_LINK:
++ ret = bgp_ls_encode_link_nlri(s, &nlri->nlri_data.link);
++ break;
++
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ ret = bgp_ls_encode_prefix_nlri(s, &nlri->nlri_data.prefix, nlri->nlri_type);
++ break;
++
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ return -1;
++ }
++
++ if (ret < 0)
++ return -1;
++
++ written += ret;
++
++ /* Update NLRI Length field */
++ uint16_t nlri_len = stream_get_endp(s) - value_start;
++
++ stream_putw_at(s, len_pos, nlri_len);
++
++ return written;
++}
++
++/*
++ * Encode BGP-LS Attributes (BGP-LS Attribute Type 29)
++ * RFC 9552 Section 4.3.1
++ *
++ * Returns number of bytes written, or -1 on error
++ */
++int bgp_ls_encode_attr(struct stream *s, const struct bgp_ls_attr *attr,
++ enum bgp_ls_protocol_id protocol_id)
++{
++ size_t start_pos;
++
++ if (!s || !attr)
++ return -1;
++
++ start_pos = stream_get_endp(s);
++
++ /* Node Flag Bits (TLV 1024) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_FLAGS_BIT)) {
++ if (stream_put_tlv(s, BGP_LS_ATTR_NODE_FLAG_BITS, 1, &attr->node_flags) < 0)
++ return -1;
++ }
++
++ /* Node Name (TLV 1026) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_NAME_BIT)) {
++ if (attr->node_name) {
++ uint16_t len = strlen(attr->node_name);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_NODE_NAME, len, attr->node_name) < 0)
++ return -1;
++ }
++ }
++
++ /* IS-IS Area Identifier (TLV 1027) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ISIS_AREA_BIT)) {
++ if (attr->isis_area_id && attr->isis_area_id_len > 0) {
++ if (stream_put_tlv(s, BGP_LS_ATTR_ISIS_AREA_ID, attr->isis_area_id_len,
++ attr->isis_area_id) < 0)
++ return -1;
++ }
++ }
++
++ /* SR Capabilities (TLV 1034) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_SR_CAPABILITIES_BIT)) {
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_SR_CAPABILITIES, 12) < 0)
++ return -1;
++ if (STREAM_WRITEABLE(s) < 12)
++ return -1;
++ stream_putc(s, attr->srgb.flag);
++ stream_putc(s, 0);
++ stream_put3(s, attr->srgb.range_size);
++ stream_put_tlv_hdr(s, BGP_LS_ATTR_SID_LABEL, 3);
++ stream_put3(s, attr->srgb.lower_bound);
++ }
++
++ /* Node MSD (TLV 266) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_MSD_BIT)) {
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_NODE_MSD, 2) < 0)
++ return -1;
++ if (STREAM_WRITEABLE(s) < 2)
++ return -1;
++ stream_putc(s, BGP_LS_IGP_MSD_TYPE_BASE_MPLS);
++ stream_putc(s, attr->msd);
++ }
++
++ /* IPv4 Router-ID of Local Node (TLV 1028) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT)) {
++ if (stream_put_tlv(s, BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL, 4,
++ &attr->ipv4_router_id_local) < 0)
++ return -1;
++ }
++
++ /* IPv6 Router-ID of Local Node (TLV 1029) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT)) {
++ if (stream_put_tlv(s, BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL, 16,
++ &attr->ipv6_router_id_local) < 0)
++ return -1;
++ }
++
++ /* IPv4 Router-ID of Remote Node (TLV 1030) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE_BIT)) {
++ if (stream_put_tlv(s, BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE, 4,
++ &attr->ipv4_router_id_remote) < 0)
++ return -1;
++ }
++
++ /* IPv6 Router-ID of Remote Node (TLV 1031) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE_BIT)) {
++ if (stream_put_tlv(s, BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE, 16,
++ &attr->ipv6_router_id_remote) < 0)
++ return -1;
++ }
++
++ /* Administrative Group (TLV 1088) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ADMIN_GROUP_BIT)) {
++ uint32_t admin_group_be = htonl(attr->admin_group);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_ADMIN_GROUP, 4, &admin_group_be) < 0)
++ return -1;
++ }
++
++ /* Maximum Link Bandwidth (TLV 1089) - IEEE 754 floating point */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MAX_LINK_BW_BIT)) {
++ if (stream_putf_tlv(s, BGP_LS_ATTR_MAX_LINK_BW, attr->max_link_bw) < 0)
++ return -1;
++ }
++
++ /* Maximum Reservable Bandwidth (TLV 1090) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MAX_RESV_BW_BIT)) {
++ if (stream_putf_tlv(s, BGP_LS_ATTR_MAX_RESV_BW, attr->max_resv_bw) < 0)
++ return -1;
++ }
++
++ /* Unreserved Bandwidth (TLV 1091) - 8 priority levels */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_UNRESV_BW_BIT)) {
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_UNRESV_BW, 32) < 0)
++ return -1;
++ for (int i = 0; i < BGP_LS_MAX_UNRESV_BW; i++) {
++ if (STREAM_WRITEABLE(s) < 4)
++ return -1;
++ stream_putf(s, attr->unreserved_bw[i]);
++ }
++ }
++
++ /* TE Default Metric (TLV 1092) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_TE_METRIC_BIT)) {
++ uint32_t te_metric_be = htonl(attr->te_metric);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_TE_DEFAULT_METRIC, 4, &te_metric_be) < 0)
++ return -1;
++ }
++
++ /* Link Protection Type (TLV 1093) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_LINK_PROTECTION_BIT)) {
++ uint16_t protection_be = htons(attr->link_protection);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_LINK_PROTECTION_TYPE, 2, &protection_be) < 0)
++ return -1;
++ }
++
++ /* MPLS Protocol Mask (TLV 1094) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MPLS_PROTOCOL_BIT)) {
++ if (stream_put_tlv(s, BGP_LS_ATTR_MPLS_PROTOCOL_MASK, 1,
++ &attr->mpls_protocol_mask) < 0)
++ return -1;
++ }
++
++ /* IGP Metric (TLV 1095) - Variable length 1-3 bytes */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IGP_METRIC_BIT)) {
++ uint8_t metric_buf[3];
++ uint8_t len = attr->igp_metric_len;
++
++ if (len > 0 && len <= BGP_LS_IGP_METRIC_MAX_LEN) {
++ /* Encode metric in big-endian */
++ if (len == 1) {
++ metric_buf[0] = attr->igp_metric & 0xFF;
++ } else if (len == 2) {
++ metric_buf[0] = (attr->igp_metric >> 8) & 0xFF;
++ metric_buf[1] = attr->igp_metric & 0xFF;
++ } else { /* len == 3 */
++ metric_buf[0] = (attr->igp_metric >> 16) & 0xFF;
++ metric_buf[1] = (attr->igp_metric >> 8) & 0xFF;
++ metric_buf[2] = attr->igp_metric & 0xFF;
++ }
++ if (stream_put_tlv(s, BGP_LS_ATTR_IGP_METRIC, len, metric_buf) < 0)
++ return -1;
++ }
++ }
++
++ /* Shared Risk Link Group (TLV 1096) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_SRLG_BIT)) {
++ if (attr->srlg_values && attr->srlg_count > 0) {
++ uint16_t srlg_len = attr->srlg_count * 4;
++
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_SRLG, srlg_len) < 0)
++ return -1;
++ for (int i = 0; i < attr->srlg_count; i++) {
++ uint32_t srlg_be = htonl(attr->srlg_values[i]);
++
++ if (STREAM_WRITEABLE(s) < 4)
++ return -1;
++ stream_put(s, &srlg_be, 4);
++ }
++ }
++ }
++
++ /* Link Name (TLV 1098) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_LINK_NAME_BIT)) {
++ if (attr->link_name) {
++ uint16_t len = strlen(attr->link_name);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_LINK_NAME, len, attr->link_name) < 0)
++ return -1;
++ }
++ }
++
++ /* Adjacency SID (TLV 1099) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ADJ_SID_BIT))
++ for (int i = 0; i < attr->adj_sid_count; i++) {
++ int sid_len = bgp_ls_attr_adjacency_sid_len(attr->adj_sid[i].flags,
++ protocol_id);
++
++ if (sid_len == -1) {
++ /* Should be impossible here */
++ zlog_warn("BGP-LS: %s wrong combination of V-Flag and L-Flag for Adjacency SID",
++ __func__);
++ } else {
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_ADJ_SID, sid_len + 4) < 0)
++ return -1;
++ if (STREAM_WRITEABLE(s) < (size_t)sid_len + 4)
++ return -1;
++ stream_putc(s, attr->adj_sid[i].flags);
++ stream_putc(s, attr->adj_sid[i].weight);
++ stream_putw(s, 0);
++ if (sid_len == 3)
++ stream_put3(s, attr->adj_sid[i].sid);
++ else
++ stream_putl(s, attr->adj_sid[i].sid);
++ }
++ }
++
++ /* Extended Admin Group (TLV 1173) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_EXT_ADMIN_GROUP_BIT)) {
++ size_t nb_words = admin_group_nb_words(&attr->ext_admin_group);
++
++ if (nb_words > 0) {
++ uint16_t ag_len = nb_words * 4; /* Each word is 4 bytes */
++
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_EXTENDED_ADMIN_GROUP, ag_len) < 0)
++ return -1;
++
++ /* Encode each 32-bit word */
++ for (size_t i = 0; i < nb_words; i++) {
++ uint32_t word = admin_group_get_offset(&attr->ext_admin_group, i);
++ uint32_t word_be = htonl(word);
++
++ if (STREAM_WRITEABLE(s) < 4)
++ return -1;
++ stream_put(s, &word_be, 4);
++ }
++ }
++ }
++
++ /* Unidirectional Link Delay (TLV 1114) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_DELAY_BIT)) {
++ uint32_t delay_be = htonl(attr->delay);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_UNIDIRECTIONAL_LINK_DELAY, 4, &delay_be) < 0)
++ return -1;
++ }
++
++ /* Min/Max Unidirectional Link Delay (TLV 1115) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MIN_MAX_DELAY_BIT)) {
++ uint32_t min_delay_be = htonl(attr->min_delay);
++ uint32_t max_delay_be = htonl(attr->max_delay);
++
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_MIN_MAX_UNIDIRECTIONAL_LINK_DELAY, 8) < 0)
++ return -1;
++
++ if (STREAM_WRITEABLE(s) < 8)
++ return -1;
++ stream_put(s, &min_delay_be, 4);
++ stream_put(s, &max_delay_be, 4);
++ }
++
++ /* Unidirectional Delay Variation (TLV 1116) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_JITTER_BIT)) {
++ uint32_t jitter_be = htonl(attr->jitter);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_UNIDIRECTIONAL_DELAY_VARIATION, 4, &jitter_be) <
++ 0)
++ return -1;
++ }
++
++ /* Unidirectional Packet Loss (TLV 1117) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_PKT_LOSS_BIT)) {
++ uint32_t pkt_loss_be = htonl(attr->pkt_loss);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_UNIDIRECTIONAL_LINK_LOSS, 4, &pkt_loss_be) < 0)
++ return -1;
++ }
++
++ /* Unidirectional Residual Bandwidth (TLV 1118) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_RESIDUAL_BW_BIT)) {
++ if (stream_putf_tlv(s, BGP_LS_ATTR_UNIDIRECTIONAL_RESIDUAL_BANDWIDTH,
++ attr->residual_bw) < 0)
++ return -1;
++ }
++
++ /* Unidirectional Available Bandwidth (TLV 1119) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_AVAILABLE_BW_BIT)) {
++ if (stream_putf_tlv(s, BGP_LS_ATTR_UNIDIRECTIONAL_AVAILABLE_BANDWIDTH,
++ attr->available_bw) < 0)
++ return -1;
++ }
++
++ /* Unidirectional Utilized Bandwidth (TLV 1120) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_UTILIZED_BW_BIT)) {
++ if (stream_putf_tlv(s, BGP_LS_ATTR_UNIDIRECTIONAL_UTILIZED_BANDWIDTH,
++ attr->utilized_bw) < 0)
++ return -1;
++ }
++
++ /* IGP Flags (TLV 1152) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IGP_FLAGS_BIT)) {
++ if (stream_put_tlv(s, BGP_LS_ATTR_IGP_FLAGS, 1, &attr->igp_flags) < 0)
++ return -1;
++ }
++
++ /* Route Tags (TLV 1153) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ROUTE_TAG_BIT)) {
++ if (attr->route_tags && attr->route_tag_count > 0) {
++ uint16_t tag_len = attr->route_tag_count * 4;
++
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_ROUTE_TAG, tag_len) < 0)
++ return -1;
++ for (int i = 0; i < attr->route_tag_count; i++) {
++ uint32_t tag_be = htonl(attr->route_tags[i]);
++
++ if (STREAM_WRITEABLE(s) < 4)
++ return -1;
++ stream_put(s, &tag_be, 4);
++ }
++ }
++ }
++
++ /* Extended Tags (TLV 1154) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_EXTENDED_TAG_BIT)) {
++ if (attr->extended_tags && attr->extended_tag_count > 0) {
++ uint16_t tag_len = attr->extended_tag_count * 8;
++
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_EXTENDED_TAG, tag_len) < 0)
++ return -1;
++ for (int i = 0; i < attr->extended_tag_count; i++) {
++ uint64_t tag_be = htonll(attr->extended_tags[i]);
++
++ if (STREAM_WRITEABLE(s) < 8)
++ return -1;
++ stream_put(s, &tag_be, 8);
++ }
++ }
++ }
++
++ /* Prefix Metric (TLV 1155) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_PREFIX_METRIC_BIT)) {
++ uint32_t metric_be = htonl(attr->prefix_metric);
++
++ if (stream_put_tlv(s, BGP_LS_ATTR_PREFIX_METRIC, 4, &metric_be) < 0)
++ return -1;
++ }
++
++ /* OSPF Forwarding Address (TLV 1156) - IPv4 or IPv6 */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_OSPF_FWD_ADDR_BIT)) {
++ /* Check which address family is present */
++ if (attr->ospf_fwd_addr.s_addr != 0) {
++ /* IPv4 forwarding address */
++ if (stream_put_tlv(s, BGP_LS_ATTR_OSPF_FWD_ADDR, 4, &attr->ospf_fwd_addr) <
++ 0)
++ return -1;
++ } else if (!IN6_IS_ADDR_UNSPECIFIED(&attr->ospf_fwd_addr6)) {
++ /* IPv6 forwarding address */
++ if (stream_put_tlv(s, BGP_LS_ATTR_OSPF_FWD_ADDR, 16,
++ &attr->ospf_fwd_addr6) < 0)
++ return -1;
++ }
++ }
++
++ /* Prefix SID (TLV 1158) */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_PREFIX_SID_BIT)) {
++ int sid_len = bgp_ls_attr_prefix_sid_len(attr->prefix_sid.sid_flag);
++
++ if (sid_len == -1) {
++ /* Should be impossible here */
++ zlog_warn("BGP-LS: %s wrong combination of V-Flag and L-Flag for Prefix SID",
++ __func__);
++ } else {
++ if (stream_put_tlv_hdr(s, BGP_LS_ATTR_PREFIX_SID, 4 + sid_len) < 0)
++ return -1;
++ if (STREAM_WRITEABLE(s) < (size_t)4 + sid_len)
++ return -1;
++ stream_putc(s, attr->prefix_sid.sid_flag);
++ stream_putc(s, attr->prefix_sid.algo);
++ stream_putw(s, 0); /* Reserved = 0 */
++ /* SID Label/Index - three or four bytes */
++ if (sid_len == 3)
++ stream_put3(s, attr->prefix_sid.sid);
++ else if (sid_len == 4)
++ stream_putl(s, attr->prefix_sid.sid);
++ }
++ }
++
++ return stream_get_endp(s) - start_pos;
++}
++
++static int bgp_ls_attr_sid_len_by_lv_flags(uint8_t flags, uint8_t v_flag_mask, uint8_t l_flag_mask)
++{
++ /*
++ * IS-IS: RFC 8667, 2.1.1; OSPFv2: RFC8665, 5; OSPFv3: RFC8666, 6:
++ *
++ * All other combinations of V-Flag and L-Flag [all other = V-Flag!=L-Flag]
++ * are invalid and any SID Advertisement received with an invalid setting
++ * for V- and L-Flags MUST be ignored.
++ */
++ uint8_t vl_mask = v_flag_mask | l_flag_mask;
++ uint8_t vl_val = CHECK_FLAG(flags, vl_mask);
++
++ if (vl_val != 0 && vl_val != vl_mask)
++ return -1;
++
++ return vl_val ? 3 : 4;
++}
++
++/*
++ * Get Prefix-SID attribute SID length by flags
++ *
++ * @return 3 or 4 in normal case, -1 in error case
++ */
++int bgp_ls_attr_prefix_sid_len(uint8_t flags)
++{
++ return bgp_ls_attr_sid_len_by_lv_flags(flags, BGP_LS_PREFIX_SID_FLAG_VALUE,
++ BGP_LS_PREFIX_SID_FLAG_LOCAL);
++}
++
++/*
++ * Get Adjacency SID attribute Label/Index SID length by flags
++ *
++ * @return 3 or 4 in normal case, -1 in error case
++ */
++int bgp_ls_attr_adjacency_sid_len(uint8_t flags, enum bgp_ls_protocol_id protocol_id)
++{
++ switch (protocol_id) {
++ case BGP_LS_PROTO_ISIS_L1:
++ case BGP_LS_PROTO_ISIS_L2:
++ return bgp_ls_attr_sid_len_by_lv_flags(flags, ISIS_EXT_SUBTLV_LINK_ADJ_SID_VFLG,
++ ISIS_EXT_SUBTLV_LINK_ADJ_SID_LFLG);
++ case BGP_LS_PROTO_OSPFV2:
++ case BGP_LS_PROTO_OSPFV3:
++ return bgp_ls_attr_sid_len_by_lv_flags(flags, OSPF_EXT_SUBTLV_LINK_ADJ_SID_VFLG,
++ OSPF_EXT_SUBTLV_LINK_ADJ_SID_LFLG);
++ default:
++ case BGP_LS_PROTO_RESERVED:
++ case BGP_LS_PROTO_DIRECT:
++ case BGP_LS_PROTO_STATIC:
++ case BGP_LS_PROTO_BGP:
++ zlog_warn("BGP-LS: %s unsupported protocol %d", __func__, protocol_id);
++ return -1;
++ }
++}
++
++/*
++ * ===========================================================================
++ * NLRI Decoding Functions
++ * ===========================================================================
++ */
++
++/*
++ * Read TLV header (Type + Length) from stream
++ *
++ * Wire format (RFC 9552 Section 5.1):
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Type | Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Returns 0 on success, -1 on error
++ */
++static inline int stream_get_tlv_hdr(struct stream *s, uint16_t *type, uint16_t *length)
++{
++ if (STREAM_READABLE(s) < BGP_LS_TLV_HDR_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Not enough data for TLV header");
++ return -1;
++ }
++
++ *type = stream_getw(s);
++ *length = stream_getw(s);
++
++ /* Check if stream has enough data for TLV value */
++ if (STREAM_READABLE(s) < *length) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: TLV type=%u length=%u exceeds available data",
++ *type, *length);
++ return -1;
++ }
++
++ return 0;
++}
++
++/*
++ * Skip unknown TLV in stream (RFC 9552 Section 5.1)
++ * Unknown TLVs must be preserved and propagated
++ */
++static inline void stream_skip_tlv(struct stream *s, uint16_t length)
++{
++ stream_forward_getp(s, length);
++}
++
++/*
++ * Decode Node Descriptor from wire format (RFC 9552 Section 5.2.1)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Descriptor Type | Descriptor Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Node Descriptor Sub-TLVs (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Node Descriptor Sub-TLVs (RFC 9552 Section 5.2.1.4):
++ * - TLV 512: Autonomous System (4 bytes, OPTIONAL)
++ * - TLV 513: BGP-LS Identifier (4 bytes, deprecated, OPTIONAL)
++ * - TLV 514: OSPF Area-ID (4 bytes, CONDITIONAL)
++ * - TLV 515: IGP Router-ID (variable, MANDATORY)
++ *
++ * Returns: 0 on success, -1 on error
++ */
++int bgp_ls_decode_node_descriptor(struct stream *s, struct bgp_ls_node_descriptor *desc,
++ uint16_t desc_length)
++{
++ size_t end_pos;
++ uint16_t sub_type, sub_len;
++ bool has_igp_router_id = false;
++
++ if (!s || !desc)
++ return -1;
++
++ /* Clear descriptor */
++ memset(desc, 0, sizeof(*desc));
++
++ /* Save end position for bounds checking */
++ end_pos = stream_get_getp(s) + desc_length;
++
++ /* Parse sub-TLVs */
++ while (stream_get_getp(s) < end_pos) {
++ if (stream_get_tlv_hdr(s, &sub_type, &sub_len) < 0)
++ return -1;
++
++ switch (sub_type) {
++ case BGP_LS_TLV_AS_NUMBER:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_AS_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate AS Number TLV in node descriptor");
++ return -1;
++ }
++ if (sub_len != BGP_LS_AS_NUMBER_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid AS Number TLV length %u (expected %u)",
++ sub_len, BGP_LS_AS_NUMBER_SIZE);
++ return -1;
++ }
++ desc->asn = stream_getl(s);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_NODE_DESC_AS_BIT);
++ break;
++
++ case BGP_LS_TLV_BGP_LS_ID:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate BGP-LS ID TLV in node descriptor");
++ return -1;
++ }
++ if (sub_len != BGP_LS_BGP_LS_ID_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid BGP-LS ID TLV length %u (expected %u)",
++ sub_len, BGP_LS_BGP_LS_ID_SIZE);
++ return -1;
++ }
++ desc->bgp_ls_id = stream_getl(s);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT);
++ break;
++
++ case BGP_LS_TLV_OSPF_AREA_ID:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate OSPF Area ID TLV in node descriptor");
++ return -1;
++ }
++ if (sub_len != BGP_LS_OSPF_AREA_ID_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid OSPF Area ID TLV length %u (expected %u)",
++ sub_len, BGP_LS_OSPF_AREA_ID_SIZE);
++ return -1;
++ }
++ desc->ospf_area_id = stream_getl(s);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ break;
++
++ case BGP_LS_TLV_IGP_ROUTER_ID:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate IGP Router-ID TLV in node descriptor");
++ return -1;
++ }
++ /* Variable length: 4 to 16 bytes */
++ if (sub_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ sub_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid IGP Router-ID TLV length %u", sub_len);
++ return -1;
++ }
++ desc->igp_router_id_len = sub_len;
++ stream_get(desc->igp_router_id, s, sub_len);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++ has_igp_router_id = true;
++ break;
++
++ default:
++ /* Unknown TLV - skip but preserve (RFC 9552 Section 5.1) */
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Unknown Node Descriptor sub-TLV %u, length %u (skipping)",
++ sub_type, sub_len);
++ stream_skip_tlv(s, sub_len);
++ break;
++ }
++ }
++
++ /* Verify mandatory TLV present */
++ if (!has_igp_router_id) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Mandatory IGP Router-ID TLV missing from Node Descriptor");
++ return -1;
++ }
++
++ /* Check we consumed exactly desc_length bytes */
++ if (stream_get_getp(s) != end_pos) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Node Descriptor length mismatch (consumed %zu, expected %u)",
++ stream_get_getp(s) - (end_pos - desc_length), desc_length);
++ return -1;
++ }
++
++ return 0;
++}
++
++/*
++ * Decode Link Descriptor from wire format (RFC 9552 Section 5.2.2)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Link Descriptor TLVs (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Link Descriptor TLVs (RFC 9552 Section 5.2.2):
++ * - TLV 258: Link Local/Remote Identifiers (8 bytes)
++ * - TLV 259: IPv4 Interface Address (4 bytes)
++ * - TLV 260: IPv4 Neighbor Address (4 bytes)
++ * - TLV 261: IPv6 Interface Address (16 bytes)
++ * - TLV 262: IPv6 Neighbor Address (16 bytes)
++ * - TLV 263: Multi-Topology ID (variable, 2 bytes per MT-ID)
++ *
++ * Returns: 0 on success, -1 on error
++ */
++int bgp_ls_decode_link_descriptor(struct stream *s, struct bgp_ls_link_descriptor *desc,
++ size_t total_length)
++{
++ size_t end_pos;
++ uint16_t tlv_type, tlv_len;
++
++ if (!s || !desc)
++ return -1;
++
++ /* Clear descriptor */
++ memset(desc, 0, sizeof(*desc));
++
++ /* Save end position for bounds checking */
++ end_pos = stream_get_getp(s) + total_length;
++
++ /* Parse TLVs */
++ while (stream_get_getp(s) < end_pos) {
++ if (stream_get_tlv_hdr(s, &tlv_type, &tlv_len) < 0)
++ goto error;
++
++ switch (tlv_type) {
++ case BGP_LS_TLV_LINK_ID:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate Link ID TLV in link descriptor");
++ goto error;
++ }
++ if (tlv_len != BGP_LS_LINK_ID_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid Link ID TLV length %u (expected %u)",
++ tlv_len, BGP_LS_LINK_ID_SIZE);
++ goto error;
++ }
++ desc->link_local_id = stream_getl(s);
++ desc->link_remote_id = stream_getl(s);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT);
++ break;
++
++ case BGP_LS_TLV_IPV4_INTF_ADDR:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate IPv4 Interface Address TLV in link descriptor");
++ goto error;
++ }
++ if (tlv_len != BGP_LS_IPV4_ADDR_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid IPv4 Interface Address TLV length %u",
++ tlv_len);
++ goto error;
++ }
++ stream_get(&desc->ipv4_intf_addr.s_addr, s, BGP_LS_IPV4_ADDR_SIZE);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT);
++ break;
++
++ case BGP_LS_TLV_IPV4_NEIGH_ADDR:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate IPv4 Neighbor Address TLV in link descriptor");
++ goto error;
++ }
++ if (tlv_len != BGP_LS_IPV4_ADDR_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid IPv4 Neighbor Address TLV length %u",
++ tlv_len);
++ goto error;
++ }
++ stream_get(&desc->ipv4_neigh_addr.s_addr, s, BGP_LS_IPV4_ADDR_SIZE);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT);
++ break;
++
++ case BGP_LS_TLV_IPV6_INTF_ADDR:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate IPv6 Interface Address TLV in link descriptor");
++ goto error;
++ }
++ if (tlv_len != BGP_LS_IPV6_ADDR_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid IPv6 Interface Address TLV length %u",
++ tlv_len);
++ goto error;
++ }
++ stream_get(&desc->ipv6_intf_addr.s6_addr, s, BGP_LS_IPV6_ADDR_SIZE);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT);
++ break;
++
++ case BGP_LS_TLV_IPV6_NEIGH_ADDR:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate IPv6 Neighbor Address TLV in link descriptor");
++ goto error;
++ }
++ if (tlv_len != BGP_LS_IPV6_ADDR_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid IPv6 Neighbor Address TLV length %u",
++ tlv_len);
++ goto error;
++ }
++ stream_get(&desc->ipv6_neigh_addr.s6_addr, s, BGP_LS_IPV6_ADDR_SIZE);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT);
++ break;
++
++ case BGP_LS_TLV_MT_ID:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_MT_ID_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate MT-ID TLV in link descriptor");
++ goto error;
++ }
++ /* Variable length: 2*n bytes where n is number of MT-IDs */
++ if (tlv_len % 2 != 0 || tlv_len > BGP_LS_MAX_MT_ID * 2) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Invalid MT-ID TLV length %u",
++ tlv_len);
++ goto error;
++ }
++ desc->mt_id_count = tlv_len / 2;
++ desc->mt_id = XCALLOC(MTYPE_BGP_LS_NLRI, tlv_len);
++ for (uint16_t i = 0; i < desc->mt_id_count; i++)
++ desc->mt_id[i] = stream_getw(s);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_LINK_DESC_MT_ID_BIT);
++ break;
++
++ case BGP_LS_TLV_REMOTE_AS_NUMBER:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_LINK_DESC_REMOTE_AS_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate Remote AS Number TLV in link descriptor");
++ goto error;
++ }
++ if (tlv_len != 4) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid Remote AS Number TLV length %u (expected 4)",
++ tlv_len);
++ goto error;
++ }
++ desc->remote_asn = stream_getl(s);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_LINK_DESC_REMOTE_AS_BIT);
++ break;
++
++ default:
++ /* Unknown TLV - skip but preserve (RFC 9552 Section 5.1) */
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Unknown Link Descriptor TLV %u, length %u (skipping)",
++ tlv_type, tlv_len);
++ stream_skip_tlv(s, tlv_len);
++ break;
++ }
++ }
++
++ /* Check we consumed exactly total_length bytes */
++ if (stream_get_getp(s) != end_pos) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Link Descriptor length mismatch");
++ goto error;
++ }
++
++ return 0;
++
++error:
++ XFREE(MTYPE_BGP_LS_NLRI, desc->mt_id);
++ desc->mt_id = NULL;
++ desc->mt_id_count = 0;
++ return -1;
++}
++
++/*
++ * Decode Prefix Descriptor from wire format (RFC 9552 Section 5.2.3)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Prefix Descriptor TLVs (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Prefix Descriptor TLVs (RFC 9552 Section 5.2.3):
++ * - TLV 263: Multi-Topology ID (2 bytes, CONDITIONAL)
++ * - TLV 264: OSPF Route Type (1 byte, CONDITIONAL)
++ * - TLV 265: IP Reachability Information (variable, MANDATORY)
++ *
++ * Returns: 0 on success, -1 on error
++ */
++int bgp_ls_decode_prefix_descriptor(struct stream *s, struct bgp_ls_prefix_descriptor *desc,
++ size_t total_length, bool is_ipv6)
++{
++ size_t end_pos;
++ uint16_t tlv_type, tlv_len;
++ bool has_ip_reach = false;
++
++ if (!s || !desc)
++ return -1;
++
++ /* Clear descriptor */
++ memset(desc, 0, sizeof(*desc));
++
++ /* Save end position for bounds checking */
++ end_pos = stream_get_getp(s) + total_length;
++
++ /* Parse TLVs */
++ while (stream_get_getp(s) < end_pos) {
++ if (stream_get_tlv_hdr(s, &tlv_type, &tlv_len) < 0)
++ goto error;
++
++ switch (tlv_type) {
++ case BGP_LS_TLV_MT_ID:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_PREFIX_DESC_MT_ID_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate MT-ID TLV in prefix descriptor");
++ goto error;
++ }
++ /* Variable length: 2*n bytes where n is number of MT-IDs */
++ if (tlv_len % 2 != 0 || tlv_len > BGP_LS_MAX_MT_ID * 2) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Invalid MT-ID TLV length %u",
++ tlv_len);
++ goto error;
++ }
++ desc->mt_id_count = tlv_len / 2;
++ desc->mt_id = XCALLOC(MTYPE_BGP_LS_NLRI, tlv_len);
++ for (uint16_t i = 0; i < desc->mt_id_count; i++)
++ desc->mt_id[i] = stream_getw(s);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_PREFIX_DESC_MT_ID_BIT);
++ break;
++
++ case BGP_LS_TLV_OSPF_ROUTE_TYPE:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs,
++ BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate OSPF Route Type TLV in prefix descriptor");
++ goto error;
++ }
++ if (tlv_len != BGP_LS_OSPF_ROUTE_TYPE_SIZE) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid OSPF Route Type TLV length %u", tlv_len);
++ goto error;
++ }
++ desc->ospf_route_type = stream_getc(s);
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT);
++ break;
++
++ case BGP_LS_TLV_IP_REACH_INFO:
++ if (BGP_LS_TLV_CHECK(desc->present_tlvs, BGP_LS_PREFIX_DESC_IP_REACH_BIT)) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: duplicate IP Reachability Info TLV in prefix descriptor");
++ goto error;
++ }
++ /* Variable length: prefix_len + ceil(prefix_len/8) */
++ if (tlv_len < 1) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid IP Reach Info TLV length %u", tlv_len);
++ goto error;
++ }
++ /* First byte is prefix length */
++ desc->prefix.prefixlen = stream_getc(s);
++
++ /* Calculate expected bytes for this prefix length */
++ uint16_t expected_bytes = (desc->prefix.prefixlen + 7) / 8;
++
++ if (tlv_len != 1 + expected_bytes) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: IP Reach Info TLV length %u mismatch (prefix len %u requires %u bytes)",
++ tlv_len, desc->prefix.prefixlen, 1 + expected_bytes);
++ goto error;
++ }
++
++ /*
++ * Read prefix bytes
++ */
++ if (is_ipv6) {
++ desc->prefix.family = AF_INET6;
++ /* Validate prefix length is valid for IPv6 */
++ if (desc->prefix.prefixlen > IPV6_MAX_BITLEN) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid IPv6 prefix length %u (max 128)",
++ desc->prefix.prefixlen);
++ goto error;
++ }
++ if (expected_bytes > 0)
++ stream_get(&desc->prefix.u.prefix6.s6_addr, s,
++ expected_bytes);
++ } else {
++ desc->prefix.family = AF_INET;
++ /* Validate prefix length is valid for IPv4 */
++ if (desc->prefix.prefixlen > IPV4_MAX_BITLEN) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Invalid IPv4 prefix length %u (max 32)",
++ desc->prefix.prefixlen);
++ goto error;
++ }
++ if (expected_bytes > 0)
++ stream_get(&desc->prefix.u.prefix4.s_addr, s,
++ expected_bytes);
++ }
++
++ BGP_LS_TLV_SET(desc->present_tlvs, BGP_LS_PREFIX_DESC_IP_REACH_BIT);
++ has_ip_reach = true;
++ break;
++
++ default:
++ /* Unknown TLV - skip but preserve (RFC 9552 Section 5.1) */
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Unknown Prefix Descriptor TLV %u, length %u (skipping)",
++ tlv_type, tlv_len);
++ stream_skip_tlv(s, tlv_len);
++ break;
++ }
++ }
++
++ /* Verify mandatory TLV present */
++ if (!has_ip_reach) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Mandatory IP Reachability Info TLV missing from Prefix Descriptor");
++ goto error;
++ }
++
++ /* Check we consumed exactly total_length bytes */
++ if (stream_get_getp(s) != end_pos) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Prefix Descriptor length mismatch");
++ goto error;
++ }
++
++ return 0;
++
++error:
++ XFREE(MTYPE_BGP_LS_NLRI, desc->mt_id);
++ desc->mt_id = NULL;
++ desc->mt_id_count = 0;
++ return -1;
++}
++
++/*
++ * Decode Node NLRI from wire format (RFC 9552 Section 5.2)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | NLRI Type = 1 | Total NLRI Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+
++ *
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (8 octets) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Local Node Descriptors TLV Type | Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptor Sub-TLVs //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Returns: 0 on success, -1 on error
++ */
++int bgp_ls_decode_node_nlri(struct stream *s, struct bgp_ls_nlri *nlri, uint16_t nlri_length)
++{
++ uint16_t desc_type, desc_len;
++ size_t start_pos;
++
++ if (!s || !nlri)
++ return -1;
++
++ /* Save start position for length validation */
++ start_pos = stream_get_getp(s);
++
++ /* Check minimum length */
++ if (nlri_length < BGP_LS_NLRI_MIN_LENGTH) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Node NLRI length %u too short (minimum %u)",
++ nlri_length, BGP_LS_NLRI_MIN_LENGTH);
++ return -1;
++ }
++
++ /* Allocate NLRI structure */
++ nlri->nlri_type = BGP_LS_NLRI_TYPE_NODE;
++
++ /* Read Protocol-ID (1 byte) */
++ nlri->nlri_data.node.protocol_id = stream_getc(s);
++
++ /* Validate Protocol-ID */
++ if (nlri->nlri_data.node.protocol_id == BGP_LS_PROTO_RESERVED ||
++ nlri->nlri_data.node.protocol_id > BGP_LS_PROTO_BGP) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Invalid Protocol-ID %u",
++ nlri->nlri_data.node.protocol_id);
++ return -1;
++ }
++
++ /* Read Identifier (8 bytes) */
++ nlri->nlri_data.node.identifier = stream_getq(s);
++
++ /* Read Local Node Descriptor TLV */
++ if (stream_get_tlv_hdr(s, &desc_type, &desc_len) < 0)
++ return -1;
++
++ if (desc_type != BGP_LS_TLV_LOCAL_NODE_DESC) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Expected Local Node Descriptor TLV %u, got %u",
++ BGP_LS_TLV_LOCAL_NODE_DESC, desc_type);
++ return -1;
++ }
++
++ if (bgp_ls_decode_node_descriptor(s, &nlri->nlri_data.node.local_node, desc_len) < 0)
++ return -1;
++
++ /* Verify we consumed exactly nlri_length bytes */
++ if (stream_get_getp(s) - start_pos != nlri_length) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Node NLRI length mismatch (consumed %zu, expected %u)",
++ stream_get_getp(s) - start_pos, nlri_length);
++ return -1;
++ }
++
++ return 0;
++}
++
++/*
++ * Decode Link NLRI from wire format (RFC 9552 Section 5.2)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | NLRI Type = 2 | Total NLRI Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+
++ *
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (8 octets) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Local Node Descriptors TLV Type | Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptor Sub-TLVs //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Remote Node Descriptors TLV Type | Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Remote Node Descriptor Sub-TLVs //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Link Descriptor TLVs //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Returns: 0 on success, -1 on error
++ */
++int bgp_ls_decode_link_nlri(struct stream *s, struct bgp_ls_nlri *nlri, uint16_t nlri_length)
++{
++ uint16_t desc_type, desc_len;
++ size_t start_pos, link_desc_start;
++
++ if (!s || !nlri)
++ return -1;
++
++ /* Save start position for length validation */
++ start_pos = stream_get_getp(s);
++
++ /* Check minimum length */
++ if (nlri_length < BGP_LS_NLRI_MIN_LENGTH) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Link NLRI length %u too short (minimum %u)",
++ nlri_length, BGP_LS_NLRI_MIN_LENGTH);
++ return -1;
++ }
++
++ /* Allocate NLRI structure */
++ nlri->nlri_type = BGP_LS_NLRI_TYPE_LINK;
++
++ /* Read Protocol-ID (1 byte) */
++ nlri->nlri_data.link.protocol_id = stream_getc(s);
++
++ /* Validate Protocol-ID */
++ if (nlri->nlri_data.link.protocol_id == BGP_LS_PROTO_RESERVED ||
++ nlri->nlri_data.link.protocol_id > BGP_LS_PROTO_BGP) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Invalid Protocol-ID %u",
++ nlri->nlri_data.link.protocol_id);
++ return -1;
++ }
++
++ /* Read Identifier (8 bytes) */
++ nlri->nlri_data.link.identifier = stream_getq(s);
++
++ /* Read Local Node Descriptor TLV */
++ if (stream_get_tlv_hdr(s, &desc_type, &desc_len) < 0)
++ return -1;
++
++ if (desc_type != BGP_LS_TLV_LOCAL_NODE_DESC) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Expected Local Node Descriptor TLV %u, got %u",
++ BGP_LS_TLV_LOCAL_NODE_DESC, desc_type);
++ return -1;
++ }
++
++ if (bgp_ls_decode_node_descriptor(s, &nlri->nlri_data.link.local_node, desc_len) < 0)
++ return -1;
++
++ /* Read Remote Node Descriptor TLV */
++ if (stream_get_tlv_hdr(s, &desc_type, &desc_len) < 0)
++ return -1;
++
++ if (desc_type != BGP_LS_TLV_REMOTE_NODE_DESC) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Expected Remote Node Descriptor TLV %u, got %u",
++ BGP_LS_TLV_REMOTE_NODE_DESC, desc_type);
++ return -1;
++ }
++
++ if (bgp_ls_decode_node_descriptor(s, &nlri->nlri_data.link.remote_node, desc_len) < 0)
++ return -1;
++
++ /* Link Descriptors are remaining bytes */
++ link_desc_start = stream_get_getp(s);
++ size_t link_desc_len = nlri_length - (link_desc_start - start_pos);
++
++ if (link_desc_len > 0) {
++ if (bgp_ls_decode_link_descriptor(s, &nlri->nlri_data.link.link_desc,
++ link_desc_len) < 0)
++ return -1;
++ }
++
++ /* Verify we consumed exactly nlri_length bytes */
++ if (stream_get_getp(s) - start_pos != nlri_length) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Link NLRI length mismatch (consumed %zu, expected %u)",
++ stream_get_getp(s) - start_pos, nlri_length);
++ return -1;
++ }
++
++ return 0;
++}
++
++/*
++ * Decode Prefix NLRI from wire format (RFC 9552 Section 5.2)
++ *
++ * Wire format:
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | NLRI Type = 3/4 | Total NLRI Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+
++ *
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (8 octets) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Local Node Descriptors TLV Type | Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptor Sub-TLVs //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Prefix Descriptor TLVs //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Returns: 0 on success, -1 on error
++ */
++int bgp_ls_decode_prefix_nlri(struct stream *s, struct bgp_ls_nlri *nlri, uint16_t nlri_type,
++ uint16_t nlri_length)
++{
++ uint16_t desc_type, desc_len;
++ size_t start_pos, prefix_desc_start;
++
++ if (!s || !nlri)
++ return -1;
++
++ /* Validate NLRI type */
++ if (nlri_type != BGP_LS_NLRI_TYPE_IPV4_PREFIX && nlri_type != BGP_LS_NLRI_TYPE_IPV6_PREFIX) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Invalid Prefix NLRI type %u", nlri_type);
++ return -1;
++ }
++
++ /* Save start position for length validation */
++ start_pos = stream_get_getp(s);
++
++ /* Check minimum length */
++ if (nlri_length < BGP_LS_NLRI_MIN_LENGTH) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Prefix NLRI length %u too short (minimum %u)",
++ nlri_length, BGP_LS_NLRI_MIN_LENGTH);
++ return -1;
++ }
++
++ /* Allocate NLRI structure */
++ nlri->nlri_type = nlri_type;
++
++ /* Read Protocol-ID (1 byte) */
++ nlri->nlri_data.prefix.protocol_id = stream_getc(s);
++
++ /* Validate Protocol-ID */
++ if (nlri->nlri_data.prefix.protocol_id == BGP_LS_PROTO_RESERVED ||
++ nlri->nlri_data.prefix.protocol_id > BGP_LS_PROTO_BGP) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Invalid Protocol-ID %u",
++ nlri->nlri_data.prefix.protocol_id);
++ return -1;
++ }
++
++ /* Read Identifier (8 bytes) */
++ nlri->nlri_data.prefix.identifier = stream_getq(s);
++
++ /* Read Local Node Descriptor TLV */
++ if (stream_get_tlv_hdr(s, &desc_type, &desc_len) < 0)
++ return -1;
++
++ if (desc_type != BGP_LS_TLV_LOCAL_NODE_DESC) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Expected Local Node Descriptor TLV %u, got %u",
++ BGP_LS_TLV_LOCAL_NODE_DESC, desc_type);
++ return -1;
++ }
++
++ if (bgp_ls_decode_node_descriptor(s, &nlri->nlri_data.prefix.local_node, desc_len) < 0)
++ return -1;
++
++ /* Prefix Descriptors are remaining bytes */
++ prefix_desc_start = stream_get_getp(s);
++ size_t prefix_desc_len = nlri_length - (prefix_desc_start - start_pos);
++
++ if (prefix_desc_len > 0) {
++ /* Determine if IPv6 based on NLRI type (RFC 9552 Section 3.2) */
++ bool is_ipv6 = (nlri_type == BGP_LS_NLRI_TYPE_IPV6_PREFIX);
++
++ if (bgp_ls_decode_prefix_descriptor(s, &nlri->nlri_data.prefix.prefix_desc,
++ prefix_desc_len, is_ipv6) < 0)
++ return -1;
++ } else {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Prefix NLRI has no Prefix Descriptor TLVs");
++ return -1;
++ }
++
++ /* Verify we consumed exactly nlri_length bytes */
++ if (stream_get_getp(s) - start_pos != nlri_length) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: Prefix NLRI length mismatch (consumed %zu, expected %u)",
++ stream_get_getp(s) - start_pos, nlri_length);
++ return -1;
++ }
++
++ return 0;
++}
++
++/*
++ * Decode complete BGP-LS NLRI from UPDATE message (RFC 9552 Section 5.2)
++ *
++ * This is the main entry point for decoding NLRIs from MP_REACH_NLRI
++ * or MP_UNREACH_NLRI attributes.
++ *
++ * Returns: 0 on success, -1 on error
++ */
++int bgp_ls_decode_nlri(struct stream *s, struct bgp_ls_nlri *nlri)
++{
++ uint16_t nlri_type, nlri_length;
++
++ if (!s || !nlri)
++ return -1;
++
++ /* Read NLRI Type (2 bytes) */
++ if (STREAM_READABLE(s) < 2) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Not enough data for NLRI type");
++ return -1;
++ }
++ nlri_type = stream_getw(s);
++
++ /* Read NLRI Length (2 bytes) */
++ if (STREAM_READABLE(s) < 2) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Not enough data for NLRI length");
++ return -1;
++ }
++ nlri_length = stream_getw(s);
++
++ /* Check if stream has enough data for NLRI */
++ if (STREAM_READABLE(s) < nlri_length) {
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: NLRI type=%u length=%u exceeds available data",
++ nlri_type, nlri_length);
++ return -1;
++ }
++
++ /* Decode based on NLRI type */
++ switch (nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE:
++ return bgp_ls_decode_node_nlri(s, nlri, nlri_length);
++
++ case BGP_LS_NLRI_TYPE_LINK:
++ return bgp_ls_decode_link_nlri(s, nlri, nlri_length);
++
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ return bgp_ls_decode_prefix_nlri(s, nlri, nlri_type, nlri_length);
++
++ default:
++ /* Unknown NLRI type - preserve and propagate (RFC 9552 Section 5.2) */
++ flog_warn(EC_BGP_LS_PACKET, "BGP-LS: Unknown NLRI type %u, length %u (skipping)",
++ nlri_type, nlri_length);
++ stream_forward_getp(s, nlri_length);
++ return -1;
++ }
++}
++
++/*
++ * Parse Node Flag Bits TLV (TLV 1024)
++ * RFC 9552 Section 5.3.1.1
++ */
++static int parse_node_flags(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length < 1) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Node Flags TLV too short (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_FLAGS_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Node Flags TLV");
++ return -1;
++ }
++
++ attr->node_flags = stream_getc(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_NODE_FLAGS_BIT);
++
++ /* Skip any extra bytes */
++ if (length > 1)
++ stream_forward_getp(s, length - 1);
++
++ return 0;
++}
++
++/*
++ * Parse Node Name TLV (TLV 1026)
++ * RFC 9552 Section 5.3.1.3
++ */
++static int parse_node_name(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length == 0)
++ return 0;
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_NAME_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Node Name TLV");
++ return -1;
++ }
++
++ /* Allocate space for node name (null-terminated) */
++ attr->node_name = XCALLOC(MTYPE_BGP_LS_ATTR, length + 1);
++ stream_get(attr->node_name, s, length);
++ attr->node_name[length] = '\0';
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_NODE_NAME_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse IS-IS Area ID TLV (TLV 1027)
++ * RFC 9552 Section 5.3.1.4
++ */
++static int parse_isis_area_id(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length == 0 || length > 13) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid ISIS Area ID length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ISIS_AREA_BIT)) {
++ /*
++ * RFC 9552 Section 5.3.1.4 allows multiple IS-IS Area-ID TLVs
++ * to encode synonymous area addresses. We currently support
++ * only one IS-IS Area-ID TLV; skip any additional ones.
++ */
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: multiple IS-IS Area-ID TLVs not supported, skipping duplicate");
++ stream_forward_getp(s, length);
++ return 0;
++ }
++
++ attr->isis_area_id = XCALLOC(MTYPE_BGP_LS_ATTR, length);
++ stream_get(attr->isis_area_id, s, length);
++ attr->isis_area_id_len = length;
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_ISIS_AREA_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse SR Capabilities TLV (TLV 1034)
++ * RFC 9085 Section 2.1.2
++ */
++static int parse_sr_capabilities(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ uint32_t t;
++
++ /*
++ * RFC8667 (IS-IS) 3.1:
++ * The SR-Capabilities sub-TLV MAY be advertised in an LSP of any
++ * number, but a router MUST NOT advertise more than one
++ * SR-Capabilities sub-TLV. A router receiving multiple
++ * SR-Capabilities sub-TLVs from the same originator SHOULD select the
++ * first advertisement in the lowest-numbered LSP.
++ *
++ * RFC8665 (OSPF) 3.2:
++ * Multiple occurrences of the SID/Label Range TLV MAY be advertised
++ * in order to advertise multiple ranges. In such a case:
++ *
++ * So multiple TLVs are possible, just taking first one.
++ * XXX Selection of lowest-numbered LSP for IS-IS is not implemented
++ * (it is SHOULD)
++ */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_SR_CAPABILITIES_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Multiple range SR Capabilities TLV not supported, skipping duplicate");
++
++ stream_forward_getp(s, length);
++ return 0;
++ }
++
++ if (length < 12 || (length - 2) % 10 != 0) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid SR Capabilities length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ attr->srgb.flag = stream_getc(s);
++ stream_getc(s); /* Reserved, ignore */
++ attr->srgb.range_size = stream_get3(s);
++ t = stream_getw(s);
++ if (t != BGP_LS_ATTR_SID_LABEL) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Failed parsing SR Capabilities, SubTLV Type is not %d",
++ BGP_LS_ATTR_SID_LABEL);
++ return -1;
++ }
++ t = stream_getw(s);
++ if (t != 3) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Failed parsing SR Capabilities, SubTLV Length is not 3");
++ return -1;
++ }
++ attr->srgb.lower_bound = stream_get3(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_SR_CAPABILITIES_BIT);
++
++ if (length > 12) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Multiple ranges in SR Capabilities not supported, skipping duplicate");
++ stream_forward_getp(s, length - 12);
++ }
++
++ return 0;
++}
++
++/*
++ * Parse Node MSD TLV (TLV 266)
++ * RFC 8814 Section 3
++ */
++static int parse_node_msd(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ uint8_t t;
++ uint8_t v;
++ uint8_t msd = 255;
++ bool had_msd = false;
++
++ /*
++ * RFC8476 (OSPFv2/OSPFv3)
++ * When multiple Node MSD TLVs are received from a given router, the
++ * receiver MUST use the first occurrence of the TLV in the Router
++ * Information (RI) LSA.
++ *
++ * RFC8491 (IS-IS) 2: (not implemented, just taking first TLV here)
++ * If there exist multiple Node MSD advertisements for the same MSD-Type
++ * originated by the same router, the procedures defined in [RFC7981]
++ * apply.
++ */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_NODE_MSD_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Multiple Node MSD TLV received, skipping duplicate");
++ stream_forward_getp(s, length);
++ return 0;
++ }
++
++ if (length < 2 || length % 2 != 0) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Node MSD length (%u bytes)", length);
++ return -1;
++ }
++
++ for (int i = 0; i < length; i += 2) {
++ t = stream_getc(s);
++ v = stream_getc(s);
++ if (t == BGP_LS_IGP_MSD_TYPE_BASE_MPLS) {
++ if (had_msd) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Several MSD Values of Base MPLS Imposition MSD type, choosing minimum");
++ }
++ had_msd = true;
++ msd = MIN(msd, v);
++ }
++ }
++
++ if (!had_msd) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Received Node MSD without value for Base MPLS Imposition MSD type, skipping");
++ return 0;
++ }
++
++ attr->msd = msd;
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_NODE_MSD_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Administrative Group TLV (TLV 1088)
++ * RFC 5305 Section 3.1
++ */
++static int parse_admin_group(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Admin Group length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ADMIN_GROUP_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Admin Group TLV");
++ return -1;
++ }
++
++ attr->admin_group = stream_getl(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_ADMIN_GROUP_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Maximum Link Bandwidth TLV (TLV 1089)
++ * RFC 5305 Section 3.4
++ */
++static int parse_max_link_bw(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ uint32_t bw_bits;
++
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Max Link BW length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MAX_LINK_BW_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Maximum Link Bandwidth TLV");
++ return -1;
++ }
++
++ /* Read as 32-bit IEEE floating point */
++ bw_bits = stream_getl(s);
++ memcpy(&attr->max_link_bw, &bw_bits, 4);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_MAX_LINK_BW_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Maximum Reservable Bandwidth TLV (TLV 1090)
++ * RFC 5305 Section 3.5
++ */
++static int parse_max_resv_bw(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ uint32_t bw_bits;
++
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Max Resv BW length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MAX_RESV_BW_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Maximum Reservable Bandwidth TLV");
++ return -1;
++ }
++
++ bw_bits = stream_getl(s);
++ memcpy(&attr->max_resv_bw, &bw_bits, 4);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_MAX_RESV_BW_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Unreserved Bandwidth TLV (TLV 1091)
++ * RFC 5305 Section 3.6
++ */
++static int parse_unresv_bw(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ uint32_t bw_bits;
++ int i;
++
++ if (length != 32) { /* 8 priorities * 4 bytes each */
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Unreserved BW length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_UNRESV_BW_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Unreserved Bandwidth TLV");
++ return -1;
++ }
++
++ for (i = 0; i < BGP_LS_MAX_UNRESV_BW; i++) {
++ bw_bits = stream_getl(s);
++ memcpy(&attr->unreserved_bw[i], &bw_bits, 4);
++ }
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_UNRESV_BW_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse TE Default Metric TLV (TLV 1092)
++ * RFC 9552 Section 5.3.2.3
++ */
++static int parse_te_metric(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid TE Metric length (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_TE_METRIC_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate TE Default Metric TLV");
++ return -1;
++ }
++
++ attr->te_metric = stream_getl(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_TE_METRIC_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse IGP Metric TLV (TLV 1095)
++ * RFC 9552 Section 5.3.2.4
++ */
++static int parse_igp_metric(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length < 1 || length > BGP_LS_IGP_METRIC_MAX_LEN) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid IGP Metric length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IGP_METRIC_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate IGP Metric TLV");
++ return -1;
++ }
++
++ attr->igp_metric = 0;
++ for (int i = 0; i < length; i++)
++ attr->igp_metric = (attr->igp_metric << 8) | stream_getc(s);
++ attr->igp_metric_len = length;
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_IGP_METRIC_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse SRLG TLV (TLV 1096)
++ * RFC 9552 Section 5.3.2.5
++ */
++static int parse_srlg(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ uint16_t count;
++ int i;
++
++ if (length % 4 != 0) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid SRLG length (%u bytes)", length);
++ return -1;
++ }
++
++ count = length / 4;
++ if (count > BGP_LS_MAX_SRLG) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Too many SRLGs (%u, max %u)", count,
++ BGP_LS_MAX_SRLG);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_SRLG_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate SRLG TLV");
++ return -1;
++ }
++
++ attr->srlg_values = XMALLOC(MTYPE_BGP_LS_ATTR, count * sizeof(uint32_t));
++ for (i = 0; i < count; i++)
++ attr->srlg_values[i] = stream_getl(s);
++ attr->srlg_count = count;
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_SRLG_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Link Name TLV (TLV 1098)
++ * RFC 9552 Section 5.3.2.7
++ */
++static int parse_link_name(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length == 0)
++ return 0;
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_LINK_NAME_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Link Name TLV");
++ return -1;
++ }
++
++ attr->link_name = XCALLOC(MTYPE_BGP_LS_ATTR, length + 1);
++ stream_get(attr->link_name, s, length);
++ attr->link_name[length] = '\0';
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_LINK_NAME_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Adjacency SID (TLV 1099)
++ * RFC 9085 Section 2.2.1
++ */
++static int parse_adjacency_sid(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ int flags;
++ int sid_len;
++
++ if (length != 7 && length != 8) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Adjacency SID length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (attr->adj_sid_count == BGP_LS_ADJ_MAX) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Ignoring Adjacency SID, maximum %d is implemented",
++ BGP_LS_ADJ_MAX);
++ stream_forward_endp(s, length);
++ return 0;
++ }
++
++ /*
++ * We don't know protocol id yet here, but V-Flag must be equal to L-Flag
++ * in all protocols. V-Flag position for IS-IS is same as L-Flag position for OSPF.
++ * So here we check length based on this bit and in
++ * bgp_ls_attr_process_with_protocol_id we check that V-Flag equals L-Flag.
++ */
++ flags = stream_getc(s);
++ sid_len = bgp_ls_attr_sid_len_by_lv_flags(flags, ISIS_EXT_SUBTLV_LINK_ADJ_SID_VFLG,
++ ISIS_EXT_SUBTLV_LINK_ADJ_SID_VFLG);
++
++ if (sid_len != length - 4) {
++ stream_forward_getp(s, length - 1);
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: %s L/V-Flag value contradicts length of Adjacent SID, ignoring",
++ __func__);
++ return 0;
++ }
++
++ attr->adj_sid[attr->adj_sid_count].flags = flags;
++ attr->adj_sid[attr->adj_sid_count].weight = stream_getc(s);
++ stream_getw(s); /* Reserved, ignored */
++ if (sid_len == 3)
++ attr->adj_sid[attr->adj_sid_count].sid = stream_get3(s);
++ else
++ attr->adj_sid[attr->adj_sid_count].sid = stream_getl(s);
++ attr->adj_sid_count++;
++
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_ADJ_SID_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Extended Admin Group TLV (TLV 1173)
++ */
++static int parse_ext_admin_group(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ size_t nb_words;
++
++ /* Length must be multiple of 4 (each word is 32 bits) */
++ if (length % 4 != 0) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid Extended Admin Group TLV length %u (must be multiple of 4)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_EXT_ADMIN_GROUP_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Extended Admin Group TLV");
++ return -1;
++ }
++
++ nb_words = length / 4;
++ if (nb_words > BGP_LS_MAX_EXT_ADMIN_GROUPS) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Extended Admin Group TLV too large (%zu words, max %zu)",
++ nb_words, (size_t)BGP_LS_MAX_EXT_ADMIN_GROUPS);
++ return -1;
++ }
++
++ /* Decode each 32-bit word */
++ for (size_t i = 0; i < nb_words; i++)
++ admin_group_bulk_set(&attr->ext_admin_group, stream_getl(s), i);
++
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_EXT_ADMIN_GROUP_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Unidirectional Link Delay TLV (TLV 1114)
++ */
++static int parse_link_delay(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Link Delay length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_DELAY_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Unidirectional Link Delay TLV");
++ return -1;
++ }
++
++ attr->delay = stream_getl(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_DELAY_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Min/Max Unidirectional Link Delay TLV (TLV 1115)
++ */
++static int parse_min_max_delay(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 8) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Min/Max Delay length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MIN_MAX_DELAY_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: duplicate Min/Max Unidirectional Link Delay TLV");
++ return -1;
++ }
++
++ attr->min_delay = stream_getl(s);
++ attr->max_delay = stream_getl(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_MIN_MAX_DELAY_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Unidirectional Delay Variation TLV (TLV 1116)
++ */
++static int parse_link_jitter(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Link Jitter length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_JITTER_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: duplicate Unidirectional Delay Variation TLV");
++ return -1;
++ }
++
++ attr->jitter = stream_getl(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_JITTER_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Unidirectional Packet Loss TLV (TLV 1117)
++ */
++static int parse_packet_loss(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Packet Loss length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_PKT_LOSS_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Unidirectional Link Loss TLV");
++ return -1;
++ }
++
++ attr->pkt_loss = stream_getl(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_PKT_LOSS_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Unidirectional Residual Bandwidth TLV (TLV 1118)
++ */
++static int parse_residual_bw(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Residual BW length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_RESIDUAL_BW_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Residual Bandwidth TLV");
++ return -1;
++ }
++
++ stream_get(&attr->residual_bw, s, 4);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_RESIDUAL_BW_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Unidirectional Available Bandwidth TLV (TLV 1119)
++ */
++static int parse_available_bw(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Available BW length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_AVAILABLE_BW_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Available Bandwidth TLV");
++ return -1;
++ }
++
++ stream_get(&attr->available_bw, s, 4);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_AVAILABLE_BW_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Unidirectional Utilized Bandwidth TLV (TLV 1120)
++ */
++static int parse_utilized_bw(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Utilized BW length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_UTILIZED_BW_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Utilized Bandwidth TLV");
++ return -1;
++ }
++
++ stream_get(&attr->utilized_bw, s, 4);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_UTILIZED_BW_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse IGP Flags TLV (TLV 1152)
++ * RFC 9552 Section 5.3.3.1
++ */
++static int parse_igp_flags(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length < 1) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: IGP Flags TLV too short (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IGP_FLAGS_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate IGP Flags TLV");
++ return -1;
++ }
++
++ attr->igp_flags = stream_getc(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_IGP_FLAGS_BIT);
++
++ if (length > 1)
++ stream_forward_getp(s, length - 1);
++
++ return 0;
++}
++
++/*
++ * Parse Route Tag TLV (TLV 1153)
++ * RFC 9552 Section 5.3.3.2
++ */
++static int parse_route_tag(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ uint16_t count;
++ int i;
++
++ if (length % 4 != 0) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Route Tag length (%u bytes)", length);
++ return -1;
++ }
++
++ count = length / 4;
++ if (count > BGP_LS_MAX_ROUTE_TAGS) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Too many Route Tags (%u, max %u)", count,
++ BGP_LS_MAX_ROUTE_TAGS);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ROUTE_TAG_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Route Tag TLV");
++ return -1;
++ }
++
++ attr->route_tags = XMALLOC(MTYPE_BGP_LS_ATTR, count * sizeof(uint32_t));
++ for (i = 0; i < count; i++)
++ attr->route_tags[i] = stream_getl(s);
++ attr->route_tag_count = count;
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_ROUTE_TAG_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse Prefix Metric TLV (TLV 1155)
++ * RFC 9552 Section 5.3.3.4
++ */
++static int parse_prefix_metric(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid Prefix Metric length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_PREFIX_METRIC_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Prefix Metric TLV");
++ return -1;
++ }
++
++ attr->prefix_metric = stream_getl(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_PREFIX_METRIC_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse OSPF Forwarding Address TLV (TLV 1156)
++ * RFC 9552 Section 5.3.3.5
++ */
++static int parse_ospf_fwd_addr(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4 && length != 16) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Invalid OSPF Fwd Addr length (%u bytes)",
++ length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_OSPF_FWD_ADDR_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate OSPF Forwarding Address TLV");
++ return -1;
++ }
++ if (length == 4) {
++ /* IPv4 */
++ stream_get(&attr->ospf_fwd_addr, s, 4);
++ } else {
++ /* IPv6 */
++ stream_get(&attr->ospf_fwd_addr6, s, 16);
++ }
++
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_OSPF_FWD_ADDR_BIT);
++ return 0;
++}
++
++/* Parse IPv4 Router-ID of Local Node (TLV 1028) */
++static int parse_ipv4_router_id_local(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid IPv4 Router-ID Local length (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT)) {
++ /*
++ * RFC 9552 Section 5.3.1.4 allows multiple IPv4 Local Router-ID TLVs
++ * when a node has more than one auxiliary Router-ID. We currently
++ * support only one; skip any additional ones.
++ */
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: multiple IPv4 Local Router-ID TLVs not supported, skipping duplicate");
++ stream_forward_getp(s, length);
++ return 0;
++ }
++
++ stream_get(&attr->ipv4_router_id_local, s, 4);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT);
++ return 0;
++}
++
++/* Parse IPv6 Router-ID of Local Node (TLV 1029) */
++static int parse_ipv6_router_id_local(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 16) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid IPv6 Router-ID Local length (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT)) {
++ /*
++ * RFC 9552 Section 5.3.1.4 allows multiple IPv6 Local Router-ID TLVs
++ * when a node has more than one auxiliary Router-ID. We currently
++ * support only one; skip any additional ones.
++ */
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: multiple IPv6 Local Router-ID TLVs not supported, skipping duplicate");
++ stream_forward_getp(s, length);
++ return 0;
++ }
++
++ stream_get(&attr->ipv6_router_id_local, s, 16);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT);
++ return 0;
++}
++
++/* Parse IPv4 Router-ID of Remote Node (TLV 1030) */
++static int parse_ipv4_router_id_remote(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 4) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid IPv4 Router-ID Remote length (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE_BIT)) {
++ /*
++ * RFC 9552 Section 5.3.2.1 allows multiple IPv4 Remote Router-ID TLVs
++ * when a node has more than one auxiliary Router-ID. We currently
++ * support only one; skip any additional ones.
++ */
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: multiple IPv4 Remote Router-ID TLVs not supported, skipping duplicate");
++ stream_forward_getp(s, length);
++ return 0;
++ }
++
++ stream_get(&attr->ipv4_router_id_remote, s, 4);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE_BIT);
++ return 0;
++}
++
++/* Parse IPv6 Router-ID of Remote Node (TLV 1031) */
++static int parse_ipv6_router_id_remote(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 16) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid IPv6 Router-ID Remote length (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE_BIT)) {
++ /*
++ * RFC 9552 Section 5.3.2.1 allows multiple IPv6 Remote Router-ID TLVs
++ * when a node has more than one auxiliary Router-ID. We currently
++ * support only one; skip any additional ones.
++ */
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: multiple IPv6 Remote Router-ID TLVs not supported, skipping duplicate");
++ stream_forward_getp(s, length);
++ return 0;
++ }
++
++ stream_get(&attr->ipv6_router_id_remote, s, 16);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE_BIT);
++ return 0;
++}
++
++/* Parse Link Protection Type (TLV 1093) */
++static int parse_link_protection(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 2) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid Link Protection Type length (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_LINK_PROTECTION_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Link Protection Type TLV");
++ return -1;
++ }
++
++ attr->link_protection = stream_getw(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_LINK_PROTECTION_BIT);
++ return 0;
++}
++
++/* Parse MPLS Protocol Mask (TLV 1094) */
++static int parse_mpls_protocol_mask(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ if (length != 1) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid MPLS Protocol Mask length (%u bytes)", length);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_MPLS_PROTOCOL_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate MPLS Protocol Mask TLV");
++ return -1;
++ }
++
++ attr->mpls_protocol_mask = stream_getc(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_MPLS_PROTOCOL_BIT);
++ return 0;
++}
++
++/* Parse Extended Tags (TLV 1154) */
++static int parse_extended_tag(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ uint16_t count;
++
++ if (length % 8 != 0) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid Extended Tag length (%u bytes, must be multiple of 8)",
++ length);
++ return -1;
++ }
++
++ count = length / 8;
++ if (count > BGP_LS_MAX_ROUTE_TAGS) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: Too many Extended Tags (%u, max %u)", count,
++ BGP_LS_MAX_ROUTE_TAGS);
++ return -1;
++ }
++
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_EXTENDED_TAG_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV, "BGP-LS: duplicate Extended Route Tag TLV");
++ return -1;
++ }
++
++ attr->extended_tags = XCALLOC(MTYPE_BGP_LS_ATTR, count * sizeof(uint64_t));
++ attr->extended_tag_count = count;
++
++ for (uint16_t i = 0; i < count; i++)
++ attr->extended_tags[i] = stream_getq(s);
++
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_EXTENDED_TAG_BIT);
++ return 0;
++}
++
++/*
++ * Parse Prefix SID (TLV 1158)
++ * RFC 9085 Section 2.3.1
++ */
++static int parse_prefix_sid(struct stream *s, uint16_t length, struct bgp_ls_attr *attr)
++{
++ int flags;
++ int sid_len;
++
++ if (length != 7 && length != 8) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Invalid Prefix SID length (%u bytes, expected 7 or 8)", length);
++ return -1;
++ }
++
++ /*
++ * IS-IS in RFC8667 2.1 references RFC8402, in section 3.1:
++ * Multiple SIDs MAY be allocated to the same prefix so long
++ * as the tuple <prefix, topology, algorithm> is unique.
++ *
++ * RFC8665 (OSPFv2), 5; RFC8666 (OSPFv3), 6:
++ * It MAY appear more than once in the parent TLV
++ */
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_PREFIX_SID_BIT)) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Only one Prefix SID per prefix is supported, ignoring another one");
++ stream_forward_getp(s, length);
++ return 0;
++ }
++
++ flags = stream_getc(s);
++ sid_len = bgp_ls_attr_prefix_sid_len(flags);
++
++ if (sid_len == -1) {
++ stream_forward_getp(s, length - 1);
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: %s wrong combination of V-Flag and L-Flag for Prefix SID, ignoring",
++ __func__);
++ return 0;
++ }
++
++ if (sid_len + 4 != length) {
++ stream_forward_getp(s, length - 1);
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: %s V-Flag value contradicts length of Prefix SID, ignoring",
++ __func__);
++ return 0;
++ }
++
++ attr->prefix_sid.sid_flag = flags;
++ attr->prefix_sid.algo = stream_getc(s);
++ stream_getw(s); /* Reserved, ignore two octets */
++ attr->prefix_sid.sid = 0;
++ for (int i = 0; i < sid_len; i++)
++ attr->prefix_sid.sid = (attr->prefix_sid.sid << 8) | stream_getc(s);
++ BGP_LS_TLV_SET(attr->present_tlvs, BGP_LS_ATTR_PREFIX_SID_BIT);
++
++ return 0;
++}
++
++/*
++ * Parse BGP-LS Attribute TLVs
++ * RFC 9552 Section 5.3.1
++ */
++int bgp_ls_parse_attr(struct stream *s, uint16_t total_length, struct bgp_ls_attr *attr)
++{
++ uint16_t type, length;
++ size_t end_pos = stream_get_getp(s) + total_length;
++
++ if (!attr)
++ return -1;
++
++ while (stream_get_getp(s) < end_pos) {
++ if (stream_get_tlv_hdr(s, &type, &length) < 0) {
++ flog_warn(EC_BGP_UPDATE_RCV,
++ "BGP-LS: Error parsing Node Attribute TLV header");
++ return -1;
++ }
++
++ switch (type) {
++ case BGP_LS_ATTR_NODE_FLAG_BITS:
++ if (parse_node_flags(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_NODE_NAME:
++ if (parse_node_name(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_ISIS_AREA_ID:
++ if (parse_isis_area_id(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_SR_CAPABILITIES:
++ if (parse_sr_capabilities(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_NODE_MSD:
++ if (parse_node_msd(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL:
++ if (parse_ipv4_router_id_local(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL:
++ if (parse_ipv6_router_id_local(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE:
++ if (parse_ipv4_router_id_remote(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE:
++ if (parse_ipv6_router_id_remote(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_ADMIN_GROUP:
++ if (parse_admin_group(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_MAX_LINK_BW:
++ if (parse_max_link_bw(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_MAX_RESV_BW:
++ if (parse_max_resv_bw(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_UNRESV_BW:
++ if (parse_unresv_bw(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_TE_DEFAULT_METRIC:
++ if (parse_te_metric(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_LINK_PROTECTION_TYPE:
++ if (parse_link_protection(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_MPLS_PROTOCOL_MASK:
++ if (parse_mpls_protocol_mask(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_IGP_METRIC:
++ if (parse_igp_metric(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_SRLG:
++ if (parse_srlg(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_LINK_NAME:
++ if (parse_link_name(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_ADJ_SID:
++ if (parse_adjacency_sid(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_UNIDIRECTIONAL_LINK_DELAY:
++ if (parse_link_delay(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_MIN_MAX_UNIDIRECTIONAL_LINK_DELAY:
++ if (parse_min_max_delay(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_UNIDIRECTIONAL_DELAY_VARIATION:
++ if (parse_link_jitter(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_UNIDIRECTIONAL_LINK_LOSS:
++ if (parse_packet_loss(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_UNIDIRECTIONAL_RESIDUAL_BANDWIDTH:
++ if (parse_residual_bw(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_UNIDIRECTIONAL_AVAILABLE_BANDWIDTH:
++ if (parse_available_bw(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_UNIDIRECTIONAL_UTILIZED_BANDWIDTH:
++ if (parse_utilized_bw(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_EXTENDED_ADMIN_GROUP:
++ if (parse_ext_admin_group(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_IGP_FLAGS:
++ if (parse_igp_flags(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_ROUTE_TAG:
++ if (parse_route_tag(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_EXTENDED_TAG:
++ if (parse_extended_tag(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_PREFIX_METRIC:
++ if (parse_prefix_metric(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_OSPF_FWD_ADDR:
++ if (parse_ospf_fwd_addr(s, length, attr) < 0)
++ return -1;
++ break;
++
++ case BGP_LS_ATTR_PREFIX_SID:
++ if (parse_prefix_sid(s, length, attr) < 0)
++ return -1;
++ break;
++
++ default:
++ if (BGP_DEBUG(update, UPDATE_IN))
++ zlog_debug("BGP-LS: Skipping unrecognized BGP-LS Attribute TLV %u",
++ type);
++ stream_skip_tlv(s, length);
++ break;
++ }
++ }
++
++ return 0;
++}
++
++/*
++ * Process BGP-LS Attributes when protocol id is known
++ */
++int bgp_ls_attr_process_with_protocol_id(struct bgp_ls_attr *attr,
++ enum bgp_ls_protocol_id protocol_id)
++{
++ if (BGP_LS_TLV_CHECK(attr->present_tlvs, BGP_LS_ATTR_ADJ_SID_BIT)) {
++ for (int i = attr->adj_sid_count - 1; i > 0; i--) {
++ int sid_len = bgp_ls_attr_adjacency_sid_len(attr->adj_sid[i].flags,
++ protocol_id);
++
++ if (sid_len == -1) {
++ flog_warn(EC_BGP_LS_PACKET,
++ "BGP-LS: %s wrong combination of V-Flag and L-Flag for Adjacency SID, ignoring",
++ __func__);
++
++ memmove(&attr->adj_sid[i], &attr->adj_sid[i + 1],
++ (attr->adj_sid_count - i - 1) * sizeof(attr->adj_sid[0]));
++ }
++ }
++ }
++ return 0;
++}
++
++/*
++ * Convert BGP-LS Attributes to JSON object
++ * Used for "show bgp" commands to display link-state topology information in json
++ */
++struct json_object *bgp_ls_attr_to_json(struct bgp_ls_attr *ls_attr)
++{
++ json_object *json_ls_attr = json_object_new_object();
++
++ /* Node Name */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_NODE_NAME_BIT))
++ json_object_string_add(json_ls_attr, "nodeName",
++ ls_attr->node_name ? ls_attr->node_name : "(null)");
++
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_ISIS_AREA_BIT)) {
++ struct iso_address addr;
++
++ addr.addr_len = ls_attr->isis_area_id_len;
++ memcpy(addr.area_addr, ls_attr->isis_area_id, ls_attr->isis_area_id_len);
++ json_object_string_addf(json_ls_attr, "isisAreaId", "%pIS", &addr);
++ }
++
++ /* Local TE Router-ID (IPv4) */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT))
++ json_object_string_addf(json_ls_attr, "routerIdLocal", "%pI4",
++ &ls_attr->ipv4_router_id_local);
++
++ /* Local TE Router-ID (IPv6) */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT))
++ json_object_string_addf(json_ls_attr, "routerIdLocalV6", "%pI6",
++ &ls_attr->ipv6_router_id_local);
++
++ /* Link bandwidth */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_MAX_LINK_BW_BIT))
++ json_object_double_add(json_ls_attr, "maxLinkBandwidth", ls_attr->max_link_bw);
++
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_MAX_RESV_BW_BIT))
++ json_object_double_add(json_ls_attr, "maxResvLinkBandwidth",
++ ls_attr->max_resv_bw);
++
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_UNRESV_BW_BIT)) {
++ json_object *jbw = json_object_new_array();
++
++ json_object_object_add(json_ls_attr, "unreservedBandwidth", jbw);
++ for (int i = 0; i < MAX_CLASS_TYPE; i++) {
++ char buf[INET6_BUFSIZ];
++ json_object *jobj = json_object_new_object();
++
++ snprintfrr(buf, sizeof(buf), "classType%d", i);
++ json_object_double_add(jobj, buf, ls_attr->unreserved_bw[i]);
++ json_object_array_add(jbw, jobj);
++ }
++ }
++
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_IGP_METRIC_BIT))
++ json_object_int_add(json_ls_attr, "igpMetric", ls_attr->igp_metric);
++
++ /* TE Default Metric */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_TE_METRIC_BIT))
++ json_object_int_add(json_ls_attr, "teMetric", ls_attr->te_metric);
++
++ /* SR Capabilities */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_SR_CAPABILITIES_BIT)) {
++ json_object *jsrcap = json_object_new_object();
++
++ json_object_object_add(json_ls_attr, "srCapabilities", jsrcap);
++ json_object_string_addf(jsrcap, "flags", "0x%x", ls_attr->srgb.flag);
++ json_object_int_add(jsrcap, "lowerBound", ls_attr->srgb.lower_bound);
++ json_object_int_add(jsrcap, "rangeSize", ls_attr->srgb.range_size);
++ }
++
++ /* Node MSD */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_NODE_MSD_BIT))
++ json_object_int_add(json_ls_attr, "msd", ls_attr->msd);
++
++ /* Administrative Group */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_ADMIN_GROUP_BIT))
++ json_object_int_add(json_ls_attr, "adminGroup", ls_attr->admin_group);
++
++ /* Link Protection Type */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_LINK_PROTECTION_BIT))
++ json_object_int_add(json_ls_attr, "linkProtection", ls_attr->link_protection);
++
++ /* MPLS Protocol Mask */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_MPLS_PROTOCOL_BIT))
++ json_object_int_add(json_ls_attr, "mplsProtocolMask",
++ ls_attr->mpls_protocol_mask);
++
++ /* SRLG */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_SRLG_BIT)) {
++ struct json_object *jsrlg = json_object_new_array();
++
++ json_object_object_add(json_ls_attr, "srlgs", jsrlg);
++ for (int i = 0; i < ls_attr->srlg_count; i++) {
++ json_object *jobj = json_object_new_object();
++
++ json_object_int_add(jobj, "srlg", ls_attr->srlg_values[i]);
++ json_object_array_add(jsrlg, jobj);
++ }
++ }
++
++ /* Link Name */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_LINK_NAME_BIT))
++ json_object_string_add(json_ls_attr, "linkName", ls_attr->link_name);
++
++ /* Adjacency SID */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_ADJ_SID_BIT)) {
++ struct json_object *jadj = json_object_new_array();
++
++ json_object_object_add(json_ls_attr, "adjSids", jadj);
++ for (int i = 0; i < ls_attr->adj_sid_count; i++) {
++ json_object *jobj = json_object_new_object();
++
++ json_object_int_add(jobj, "sid", ls_attr->adj_sid[i].sid);
++ json_object_string_addf(jobj, "flags", "0x%x", ls_attr->adj_sid[i].flags);
++ json_object_int_add(jobj, "weight", ls_attr->adj_sid[i].weight);
++
++ json_object_array_add(jadj, jobj);
++ }
++ }
++
++ /* Performance Metrics - Link Delay */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_DELAY_BIT))
++ json_object_int_add(json_ls_attr, "delay", ls_attr->delay);
++
++ /* Min/Max Delay */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_MIN_MAX_DELAY_BIT)) {
++ json_object_int_add(json_ls_attr, "minDelay", ls_attr->min_delay);
++ json_object_int_add(json_ls_attr, "maxDelay", ls_attr->max_delay);
++ }
++
++ /* Delay Variation */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_JITTER_BIT))
++ json_object_int_add(json_ls_attr, "jitter", ls_attr->jitter);
++
++ /* Packet Loss */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_PKT_LOSS_BIT))
++ json_object_double_add(json_ls_attr, "loss", ls_attr->pkt_loss * LOSS_PRECISION);
++
++ /* Residual Bandwidth */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_RESIDUAL_BW_BIT))
++ json_object_double_add(json_ls_attr, "residualBandwidth", ls_attr->residual_bw);
++
++ /* Available Bandwidth */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_AVAILABLE_BW_BIT))
++ json_object_double_add(json_ls_attr, "availableBandwidth", ls_attr->available_bw);
++
++ /* Utilized Bandwidth */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_UTILIZED_BW_BIT))
++ json_object_double_add(json_ls_attr, "utilizedBandwidth", ls_attr->utilized_bw);
++
++ /* IGP Flags (for prefixes) */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_IGP_FLAGS_BIT))
++ json_object_string_addf(json_ls_attr, "flags", "0x%x", ls_attr->igp_flags);
++
++ /* Route Tags */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_ROUTE_TAG_BIT)) {
++ struct json_object *jtags = json_object_new_array();
++
++ json_object_object_add(json_ls_attr, "tags", jtags);
++ for (int i = 0; i < ls_attr->route_tag_count; i++) {
++ json_object *jobj = json_object_new_object();
++
++ json_object_int_add(jobj, "tag", ls_attr->route_tags[i]);
++ json_object_array_add(jtags, jobj);
++ }
++ }
++
++ /* Extended Tags */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_EXTENDED_TAG_BIT)) {
++ struct json_object *jtags = json_object_new_array();
++
++ json_object_object_add(json_ls_attr, "extendedTags", jtags);
++ for (int i = 0; i < ls_attr->extended_tag_count; i++) {
++ json_object *jobj = json_object_new_object();
++
++ json_object_int_add(jobj, "tag", ls_attr->extended_tags[i]);
++ json_object_array_add(jtags, jobj);
++ }
++ }
++
++ /* Prefix Metric */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_PREFIX_METRIC_BIT))
++ json_object_int_add(json_ls_attr, "prefixMetric", ls_attr->prefix_metric);
++
++ /* OSPF Forwarding Address (IPv4) */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_OSPF_FWD_ADDR_BIT)) {
++ if (ls_attr->ospf_fwd_addr.s_addr != INADDR_ANY)
++ json_object_string_addf(json_ls_attr, "forwardingAddr", "%pI4",
++ &ls_attr->ospf_fwd_addr);
++ else if (!IN6_IS_ADDR_UNSPECIFIED(&ls_attr->ospf_fwd_addr6))
++ json_object_string_addf(json_ls_attr, "forwardingAddrV6", "%pI6",
++ &ls_attr->ospf_fwd_addr6);
++ }
++
++ /* Prefix SID */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_PREFIX_SID_BIT)) {
++ json_object *jpref = json_object_new_object();
++
++ json_object_object_add(json_ls_attr, "prefixSid", jpref);
++ json_object_int_add(jpref, "sid", ls_attr->prefix_sid.sid);
++ json_object_string_addf(jpref, "flags", "0x%x", ls_attr->prefix_sid.sid_flag);
++ json_object_int_add(jpref, "algo", ls_attr->prefix_sid.algo);
++ }
++
++ return json_ls_attr;
++}
++
++/*
++ * Display BGP-LS Attributes to VTY output
++ * Used for "show bgp" commands to display link-state topology information
++ */
++void bgp_ls_attr_display(struct vty *vty, struct bgp_ls_attr *ls_attr)
++{
++ int col = 0;
++ bool first_attr = true;
++
++#define COL_WIDTH 50
++#define INIT_INDENT " Link-state: "
++#define CONT_INDENT " "
++#define CHECK_WRAP() \
++ do { \
++ if (col > COL_WIDTH) { \
++ vty_out(vty, "\n" CONT_INDENT); \
++ col = strlen(CONT_INDENT); \
++ } else if (!first_attr) { \
++ vty_out(vty, ", "); \
++ col += 2; \
++ } \
++ first_attr = false; \
++ } while (0)
++
++ vty_out(vty, INIT_INDENT);
++ col = strlen(INIT_INDENT);
++
++ /* Node Name */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_NODE_NAME_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Node Name: %s",
++ ls_attr->node_name ? ls_attr->node_name : "(null)");
++ }
++
++ /* IS-IS Area Identifier */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_ISIS_AREA_BIT)) {
++ struct iso_address addr;
++
++ CHECK_WRAP();
++ addr.addr_len = ls_attr->isis_area_id_len;
++ memcpy(addr.area_addr, ls_attr->isis_area_id, ls_attr->isis_area_id_len);
++ col += vty_out(vty, "IS-IS Area ID: %pIS", &addr);
++ }
++
++ /* Local TE Router-ID (IPv4) */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Local TE Router-ID: %pI4", &ls_attr->ipv4_router_id_local);
++ }
++
++ /* Local TE Router-ID (IPv6) */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Local TE Router-ID: %pI6", &ls_attr->ipv6_router_id_local);
++ }
++
++ /* Link bandwidth */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_MAX_LINK_BW_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Maximum Link BW (kbits/sec): %.0f",
++ ls_attr->max_link_bw / 1000.0);
++ }
++
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_MAX_RESV_BW_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Maximum Reserv Link BW (kbits/sec): %.0f",
++ ls_attr->max_resv_bw / 1000.0);
++ }
++
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_UNRESV_BW_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Maximum Unreserv Link BW (kbits/sec):");
++ for (int j = 0; j < 8; j++)
++ col += vty_out(vty, " %.0f", ls_attr->unreserved_bw[j] / 1000.0);
++ }
++
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_IGP_METRIC_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "metric: %u", ls_attr->igp_metric);
++ }
++
++ /* TE Default Metric */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_TE_METRIC_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "TE Default Metric: %u", ls_attr->te_metric);
++ }
++
++ /* Administrative Group */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_ADMIN_GROUP_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Administrative Group: 0x%08x", ls_attr->admin_group);
++ }
++
++ /* Link Protection Type */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_LINK_PROTECTION_BIT)) {
++ CHECK_WRAP();
++ vty_out(vty, "Link Protection Type: 0x%04x", ls_attr->link_protection);
++ }
++
++ /* MPLS Protocol Mask */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_MPLS_PROTOCOL_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "MPLS Protocol Mask: 0x%02x", ls_attr->mpls_protocol_mask);
++ }
++
++ /* SR Capabilities */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_SR_CAPABILITIES_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "SR Capabilities: flags 0x%x range %d-%d", ls_attr->srgb.flag,
++ ls_attr->srgb.lower_bound,
++ ls_attr->srgb.lower_bound + ls_attr->srgb.range_size);
++ }
++
++ /* Node MSD */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_NODE_MSD_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "MSD: %d", ls_attr->msd);
++ }
++
++ /* SRLG */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_SRLG_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "SRLG:");
++ col += 5;
++ for (int j = 0; j < ls_attr->srlg_count; j++)
++ col += vty_out(vty, " %u", ls_attr->srlg_values[j]);
++ }
++
++ /* Link Name */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_LINK_NAME_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Link Name: %s", ls_attr->link_name);
++ }
++
++ /* Adjacency SID */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_ADJ_SID_BIT)) {
++ for (int i = 0; i < ls_attr->adj_sid_count; i++) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Adjacency-SID: %u Flags: 0x%x Weight: 0x%x",
++ ls_attr->adj_sid[i].sid, ls_attr->adj_sid[i].flags,
++ ls_attr->adj_sid[i].weight);
++ }
++ }
++
++ /* Performance Metrics - Link Delay */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_DELAY_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Link Delay: %u us", ls_attr->delay);
++ }
++
++ /* Min/Max Delay */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_MIN_MAX_DELAY_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Min Delay: %u us Max Delay: %u us", ls_attr->min_delay,
++ ls_attr->max_delay);
++ }
++
++ /* Delay Variation */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_JITTER_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Delay Variation: %u us", ls_attr->jitter);
++ }
++
++ /* Packet Loss */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_PKT_LOSS_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Packet Loss: %g (%%)",
++ (float)(ls_attr->pkt_loss * LOSS_PRECISION));
++ }
++
++ /* Residual Bandwidth */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_RESIDUAL_BW_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Residual BW (kbits/sec): %.0f", ls_attr->residual_bw / 1000.0);
++ }
++
++ /* Available Bandwidth */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_AVAILABLE_BW_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Available BW (kbits/sec): %.0f",
++ ls_attr->available_bw / 1000.0);
++ }
++
++ /* Utilized Bandwidth */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_UTILIZED_BW_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Utilized BW (kbits/sec): %.0f", ls_attr->utilized_bw / 1000.0);
++ }
++
++ /* IGP Flags (for prefixes) */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_IGP_FLAGS_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "IGP flags: 0x%02x", ls_attr->igp_flags);
++ }
++
++ /* Route Tags */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_ROUTE_TAG_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Route tag:");
++ for (int j = 0; j < ls_attr->route_tag_count; j++)
++ col += vty_out(vty, " %u", ls_attr->route_tags[j]);
++ }
++
++ /* Extended Tags */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_EXTENDED_TAG_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Extended tag:");
++ for (int j = 0; j < ls_attr->extended_tag_count; j++)
++ col += vty_out(vty, " %" PRIu64, ls_attr->extended_tags[j]);
++ }
++
++ /* Prefix Metric */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_PREFIX_METRIC_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Metric: %u", ls_attr->prefix_metric);
++ }
++
++ /* OSPF Forwarding Address (IPv4) */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_OSPF_FWD_ADDR_BIT)) {
++ if (ls_attr->ospf_fwd_addr.s_addr != INADDR_ANY) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Forwarding addr: %pI4", &ls_attr->ospf_fwd_addr);
++ } else if (!IN6_IS_ADDR_UNSPECIFIED(&ls_attr->ospf_fwd_addr6)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Forwarding addr: %pI6", &ls_attr->ospf_fwd_addr6);
++ }
++ }
++
++ /* Prefix SID */
++ if (BGP_LS_TLV_CHECK(ls_attr->present_tlvs, BGP_LS_ATTR_PREFIX_SID_BIT)) {
++ CHECK_WRAP();
++ col += vty_out(vty, "Prefix-SID: %u Flags 0x%x algo %hhu", ls_attr->prefix_sid.sid,
++ ls_attr->prefix_sid.sid_flag, ls_attr->prefix_sid.algo);
++ }
++
++ (void)col; /* Don't complain about last 'col +=' */
++#undef CHECK_WRAP
++#undef COL_WIDTH
++#undef INIT_INDENT
++#undef CONT_INDENT
++
++ vty_out(vty, "\n");
++}
++
++/*
++ * Display BGP-LS NLRI to VTY output
++ * Used for "show bgp" commands to display link-state NLRI details
++ */
++void bgp_ls_nlri_display(struct vty *vty, struct bgp_ls_nlri *nlri)
++{
++ char ipaddr_str[INET6_ADDRSTRLEN];
++ const char *nlri_type_str = NULL;
++ const char *protocol_str = NULL;
++
++ if (!nlri)
++ return;
++
++ /* Determine NLRI type string */
++ switch (nlri->nlri_type) {
++ case BGP_LS_NLRI_TYPE_NODE:
++ nlri_type_str = "Node";
++ break;
++ case BGP_LS_NLRI_TYPE_LINK:
++ nlri_type_str = "Link";
++ break;
++ case BGP_LS_NLRI_TYPE_IPV4_PREFIX:
++ nlri_type_str = "Prefix";
++ break;
++ case BGP_LS_NLRI_TYPE_IPV6_PREFIX:
++ nlri_type_str = "Prefix";
++ break;
++ case BGP_LS_NLRI_TYPE_RESERVED:
++ nlri_type_str = "Unknown";
++ break;
++ }
++
++ /* Determine protocol string */
++ switch (nlri->nlri_data.node.protocol_id) {
++ case BGP_LS_PROTO_ISIS_L1:
++ protocol_str = "ISIS L1";
++ break;
++ case BGP_LS_PROTO_ISIS_L2:
++ protocol_str = "ISIS L2";
++ break;
++ case BGP_LS_PROTO_OSPFV2:
++ protocol_str = "OSPFv2";
++ break;
++ case BGP_LS_PROTO_OSPFV3:
++ protocol_str = "OSPFv3";
++ break;
++ case BGP_LS_PROTO_BGP:
++ protocol_str = "BGP";
++ break;
++ case BGP_LS_PROTO_DIRECT:
++ protocol_str = "Direct";
++ break;
++ case BGP_LS_PROTO_STATIC:
++ protocol_str = "Static";
++ break;
++ case BGP_LS_PROTO_RESERVED:
++ protocol_str = "Unknown";
++ break;
++ }
++
++ vty_out(vty, "NLRI Type: %s\n", nlri_type_str);
++ vty_out(vty, "Protocol: %s\n", protocol_str);
++ vty_out(vty, "Identifier: 0x%" PRIx64 "\n", nlri->nlri_data.node.identifier);
++
++ /* Display Local Node Descriptor */
++ vty_out(vty, "Local Node Descriptor:\n");
++ struct bgp_ls_node_descriptor *local_node = NULL;
++
++ if (nlri->nlri_type == BGP_LS_NLRI_TYPE_NODE) {
++ local_node = &nlri->nlri_data.node.local_node;
++ } else if (nlri->nlri_type == BGP_LS_NLRI_TYPE_LINK) {
++ local_node = &nlri->nlri_data.link.local_node;
++ } else if (nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV4_PREFIX ||
++ nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV6_PREFIX) {
++ local_node = &nlri->nlri_data.prefix.local_node;
++ }
++
++ if (local_node) {
++ if (BGP_LS_TLV_CHECK(local_node->present_tlvs, BGP_LS_NODE_DESC_AS_BIT))
++ vty_out(vty, "\tAS Number: %u\n", local_node->asn);
++ if (BGP_LS_TLV_CHECK(local_node->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT))
++ vty_out(vty, "\tArea ID: %pI4\n", (in_addr_t *)&local_node->ospf_area_id);
++ if (BGP_LS_TLV_CHECK(local_node->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT))
++ vty_out(vty, "\tBGP Identifier: %u\n", local_node->bgp_ls_id);
++ if (BGP_LS_TLV_CHECK(local_node->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT)) {
++ if (local_node->igp_router_id_len == 4) {
++ vty_out(vty, "\tRouter ID IPv4: %pI4\n",
++ (struct in_addr *)local_node->igp_router_id);
++ } else if (local_node->igp_router_id_len == 6 ||
++ local_node->igp_router_id_len == 7) {
++ vty_out(vty, "\tISO Node ID: %02x%02x.%02x%02x.%02x%02x.%02x\n",
++ local_node->igp_router_id[0], local_node->igp_router_id[1],
++ local_node->igp_router_id[2], local_node->igp_router_id[3],
++ local_node->igp_router_id[4], local_node->igp_router_id[5],
++ local_node->igp_router_id[6] >> 1);
++ } else if (local_node->igp_router_id_len == 16) {
++ inet_ntop(AF_INET6, local_node->igp_router_id, ipaddr_str,
++ sizeof(ipaddr_str));
++ vty_out(vty, "\tRouter ID IPv6: %s\n", ipaddr_str);
++ }
++ }
++ }
++
++ /* Display Remote Node Descriptor for Link NLRI */
++ if (nlri->nlri_type == BGP_LS_NLRI_TYPE_LINK) {
++ struct bgp_ls_node_descriptor *remote_node = &nlri->nlri_data.link.remote_node;
++
++ vty_out(vty, "Remote Node Descriptor:\n");
++
++ if (BGP_LS_TLV_CHECK(remote_node->present_tlvs, BGP_LS_NODE_DESC_AS_BIT))
++ vty_out(vty, "\tAS Number: %u\n", remote_node->asn);
++ if (BGP_LS_TLV_CHECK(remote_node->present_tlvs, BGP_LS_NODE_DESC_OSPF_AREA_BIT))
++ vty_out(vty, "\tArea ID: %pI4\n", (in_addr_t *)&remote_node->ospf_area_id);
++ if (BGP_LS_TLV_CHECK(remote_node->present_tlvs, BGP_LS_NODE_DESC_BGP_LS_ID_BIT))
++ vty_out(vty, "\tBGP Identifier: %u\n", remote_node->bgp_ls_id);
++ if (BGP_LS_TLV_CHECK(remote_node->present_tlvs, BGP_LS_NODE_DESC_IGP_ROUTER_BIT)) {
++ if (remote_node->igp_router_id_len == 4) {
++ vty_out(vty, "\tRouter ID IPv4: %pI4\n",
++ (struct in_addr *)remote_node->igp_router_id);
++ } else if (remote_node->igp_router_id_len == 6 ||
++ remote_node->igp_router_id_len == 7) {
++ vty_out(vty, "\tISO Node ID: %02x%02x.%02x%02x.%02x%02x.%02x\n",
++ remote_node->igp_router_id[0],
++ remote_node->igp_router_id[1],
++ remote_node->igp_router_id[2],
++ remote_node->igp_router_id[3],
++ remote_node->igp_router_id[4],
++ remote_node->igp_router_id[5],
++ remote_node->igp_router_id[6] >> 1);
++ } else if (remote_node->igp_router_id_len == 16) {
++ inet_ntop(AF_INET6, remote_node->igp_router_id, ipaddr_str,
++ sizeof(ipaddr_str));
++ vty_out(vty, "\tRouter ID IPv6: %s\n", ipaddr_str);
++ }
++ }
++
++ /* Display Link Descriptor */
++ struct bgp_ls_link_descriptor *link_desc = &nlri->nlri_data.link.link_desc;
++
++ vty_out(vty, "Link Descriptor:\n");
++
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_LINK_ID_BIT)) {
++ vty_out(vty, "\tLink ID: %u.%u\n", link_desc->link_local_id,
++ link_desc->link_remote_id);
++ }
++
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_INTF_BIT)) {
++ inet_ntop(AF_INET, &link_desc->ipv4_intf_addr, ipaddr_str,
++ sizeof(ipaddr_str));
++ vty_out(vty, "\tLocal Interface Address IPv4: %s\n", ipaddr_str);
++ }
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV4_NEIGH_BIT)) {
++ inet_ntop(AF_INET, &link_desc->ipv4_neigh_addr, ipaddr_str,
++ sizeof(ipaddr_str));
++ vty_out(vty, "\tNeighbor Interface Address IPv4: %s\n", ipaddr_str);
++ }
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_INTF_BIT)) {
++ inet_ntop(AF_INET6, &link_desc->ipv6_intf_addr, ipaddr_str,
++ sizeof(ipaddr_str));
++ vty_out(vty, "\tLocal Interface Address IPv6: %s\n", ipaddr_str);
++ }
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_IPV6_NEIGH_BIT)) {
++ inet_ntop(AF_INET6, &link_desc->ipv6_neigh_addr, ipaddr_str,
++ sizeof(ipaddr_str));
++ vty_out(vty, "\tNeighbor Interface Address IPv6: %s\n", ipaddr_str);
++ }
++
++ /* Display Link Descriptor Multi-Topology info */
++ if (BGP_LS_TLV_CHECK(link_desc->present_tlvs, BGP_LS_LINK_DESC_MT_ID_BIT)) {
++ vty_out(vty, "Multi-Topology:\n");
++ for (uint8_t i = 0; i < link_desc->mt_id_count; i++)
++ vty_out(vty, "\tMT-ID: %u\n", link_desc->mt_id[i]);
++ }
++ }
++
++ /* Display Prefix Descriptor for Prefix NLRI */
++ if (nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV4_PREFIX ||
++ nlri->nlri_type == BGP_LS_NLRI_TYPE_IPV6_PREFIX) {
++ struct bgp_ls_prefix_descriptor *prefix_desc = &nlri->nlri_data.prefix.prefix_desc;
++
++ vty_out(vty, "Prefix Descriptor:\n");
++
++ char prefix_str[BUFSIZ];
++
++ prefix2str(&prefix_desc->prefix, prefix_str, sizeof(prefix_str));
++ vty_out(vty, "\tPrefix: %s\n", prefix_str);
++
++ /* OSPF Route Type */
++ if (BGP_LS_TLV_CHECK(prefix_desc->present_tlvs, BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT)) {
++ const char *ospf_rt_str = NULL;
++
++ switch (prefix_desc->ospf_route_type) {
++ case BGP_LS_OSPF_RT_INTRA_AREA:
++ ospf_rt_str = "Intra-Area";
++ break;
++ case BGP_LS_OSPF_RT_INTER_AREA:
++ ospf_rt_str = "Inter-Area";
++ break;
++ case BGP_LS_OSPF_RT_EXTERNAL_1:
++ ospf_rt_str = "External Type 1";
++ break;
++ case BGP_LS_OSPF_RT_EXTERNAL_2:
++ ospf_rt_str = "External Type 2";
++ break;
++ case BGP_LS_OSPF_RT_NSSA_1:
++ ospf_rt_str = "NSSA Type 1";
++ break;
++ case BGP_LS_OSPF_RT_NSSA_2:
++ ospf_rt_str = "NSSA Type 2";
++ break;
++ default:
++ ospf_rt_str = "Unknown";
++ break;
++ }
++ vty_out(vty, "\tOSPF Route Type: %s\n", ospf_rt_str);
++ }
++
++ /* Multi-Topology */
++ if (BGP_LS_TLV_CHECK(prefix_desc->present_tlvs, BGP_LS_PREFIX_DESC_MT_ID_BIT)) {
++ vty_out(vty, "Multi-Topology:\n");
++ for (uint8_t i = 0; i < prefix_desc->mt_id_count; i++)
++ vty_out(vty, "\tMT-ID: %u\n", prefix_desc->mt_id[i]);
++ }
++ }
++}
+diff --git a/bgpd/bgp_ls_nlri.h b/bgpd/bgp_ls_nlri.h
+new file mode 100644
+index 0000000000..b491c78734
+--- /dev/null
++++ b/bgpd/bgp_ls_nlri.h
+@@ -0,0 +1,895 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * BGP Link-State NLRI (RFC 9552)
++ * Copyright (C) 2025 Carmine Scarpitta
++ */
++
++#ifndef _FRR_BGP_LS_NLRI_H
++#define _FRR_BGP_LS_NLRI_H
++
++#include "prefix.h"
++#include "bgpd/bgpd.h"
++
++/* Memory types */
++DECLARE_MTYPE(BGP_LS_ATTR);
++
++/*
++ * ===========================================================================
++ * Protocol and NLRI Type Definitions
++ * ===========================================================================
++ */
++
++/*
++ * BGP-LS Protocol-ID values
++ * IANA: https://www.iana.org/assignments/bgp-ls-parameters/bgp-ls-parameters.xhtml#protocol-ids
++ */
++enum bgp_ls_protocol_id {
++ BGP_LS_PROTO_RESERVED = 0, /* Reserved - RFC 9552 */
++ BGP_LS_PROTO_ISIS_L1 = 1, /* IS-IS Level 1 - RFC 9552 */
++ BGP_LS_PROTO_ISIS_L2 = 2, /* IS-IS Level 2 - RFC 9552 */
++ BGP_LS_PROTO_OSPFV2 = 3, /* OSPFv2 - RFC 9552 */
++ BGP_LS_PROTO_DIRECT = 4, /* Direct - RFC 9552 */
++ BGP_LS_PROTO_STATIC = 5, /* Static configuration - RFC 9552 */
++ BGP_LS_PROTO_OSPFV3 = 6, /* OSPFv3 - RFC 9552 */
++ BGP_LS_PROTO_BGP = 7, /* BGP - RFC 9086 */
++};
++
++/*
++ * BGP-LS NLRI Types
++ * IANA: https://www.iana.org/assignments/bgp-ls-parameters/bgp-ls-parameters.xhtml#nlri-types
++ */
++enum bgp_ls_nlri_type {
++ BGP_LS_NLRI_TYPE_RESERVED = 0, /* Reserved - RFC 9552 */
++ BGP_LS_NLRI_TYPE_NODE = 1, /* Node NLRI - RFC 9552 */
++ BGP_LS_NLRI_TYPE_LINK = 2, /* Link NLRI - RFC 9552 */
++ BGP_LS_NLRI_TYPE_IPV4_PREFIX = 3, /* IPv4 Topology Prefix NLRI - RFC 9552 */
++ BGP_LS_NLRI_TYPE_IPV6_PREFIX = 4, /* IPv6 Topology Prefix NLRI - RFC 9552 */
++};
++
++/*
++ * ===========================================================================
++ * TLV Type Definitions
++ * ===========================================================================
++ */
++
++/*
++ * Node Descriptor TLV Types
++ * IANA: https://www.iana.org/assignments/bgp-ls-parameters/bgp-ls-parameters.xhtml#node-descriptor-link-descriptor-prefix-descriptor-attribute-tlv
++ */
++enum bgp_ls_node_descriptor_tlv {
++ BGP_LS_TLV_LOCAL_NODE_DESC = 256, /* Local Node Descriptors - RFC 9552, Section 5.2.1.2 */
++ BGP_LS_TLV_REMOTE_NODE_DESC = 257, /* Remote Node Descriptors - RFC 9552, Section 5.2.1.3 */
++ BGP_LS_TLV_AS_NUMBER = 512, /* Autonomous System - RFC 9552, Section 5.2.1.4 */
++ BGP_LS_TLV_BGP_LS_ID = 513, /* BGP-LS Identifier (deprecated) - RFC 9552, Section 5.2.1.4 */
++ BGP_LS_TLV_OSPF_AREA_ID = 514, /* OSPF Area-ID - RFC 9552, Section 5.2.1.4 */
++ BGP_LS_TLV_IGP_ROUTER_ID = 515, /* IGP Router-ID - RFC 9552, Section 5.2.1.4 */
++};
++
++/*
++ * Link Descriptor TLV Types
++ * IANA: https://www.iana.org/assignments/bgp-ls-parameters/bgp-ls-parameters.xhtml#node-descriptor-link-descriptor-prefix-descriptor-attribute-tlv
++ */
++enum bgp_ls_link_descriptor_tlv {
++ BGP_LS_TLV_LINK_ID = 258, /* Link Local/Remote Identifiers - RFC 9552, Section 5.2.2 */
++ BGP_LS_TLV_IPV4_INTF_ADDR = 259, /* IPv4 interface address - RFC 9552, Section 5.2.2 */
++ BGP_LS_TLV_IPV4_NEIGH_ADDR = 260, /* IPv4 neighbor address - RFC 9552, Section 5.2.2 */
++ BGP_LS_TLV_IPV6_INTF_ADDR = 261, /* IPv6 interface address - RFC 9552, Section 5.2.2 */
++ BGP_LS_TLV_IPV6_NEIGH_ADDR = 262, /* IPv6 neighbor address - RFC 9552, Section 5.2.2 */
++ BGP_LS_TLV_MT_ID = 263, /* Multi-Topology Identifier - RFC 9552, Section 5.2.2.1 */
++ BGP_LS_TLV_REMOTE_AS_NUMBER = 270, /* Remote AS Number - draft-ietf-idr-bgpls-inter-as-topology-ext */
++};
++
++/*
++ * Prefix Descriptor TLV Types
++ * IANA: https://www.iana.org/assignments/bgp-ls-parameters/bgp-ls-parameters.xhtml#node-descriptor-link-descriptor-prefix-descriptor-attribute-tlv
++ */
++enum bgp_ls_prefix_descriptor_tlv {
++ BGP_LS_TLV_OSPF_ROUTE_TYPE = 264, /* OSPF Route Type - RFC 9552, Section 5.2.3.1 */
++ BGP_LS_TLV_IP_REACH_INFO = 265, /* IP Reachability Information - RFC 9552, Section 5.2.3.2 */
++};
++
++/*
++ * OSPF Route Type Values (for TLV 264)
++ * RFC 9552, Section 5.2.3.1
++ */
++enum bgp_ls_ospf_route_type {
++ BGP_LS_OSPF_RT_INTRA_AREA = 1, /* Intra-Area */
++ BGP_LS_OSPF_RT_INTER_AREA = 2, /* Inter-Area */
++ BGP_LS_OSPF_RT_EXTERNAL_1 = 3, /* External Type 1 */
++ BGP_LS_OSPF_RT_EXTERNAL_2 = 4, /* External Type 2 */
++ BGP_LS_OSPF_RT_NSSA_1 = 5, /* NSSA Type 1 */
++ BGP_LS_OSPF_RT_NSSA_2 = 6, /* NSSA Type 2 */
++};
++
++/*
++ * BGP-LS Attribute TLV Types
++ * IANA: https://www.iana.org/assignments/bgp-ls-parameters/bgp-ls-parameters.xhtml#node-descriptor-link-descriptor-prefix-descriptor-attribute-tlv
++ */
++
++enum bgp_ls_attr_tlv {
++ /* Node Attribute TLVs (RFC 9552 Section 5.3.1) */
++ BGP_LS_ATTR_NODE_FLAG_BITS = 1024, /* Node Flag Bits */
++ BGP_LS_ATTR_NODE_NAME = 1026, /* Node Name */
++ BGP_LS_ATTR_ISIS_AREA_ID = 1027, /* IS-IS Area Identifier */
++
++ /* Node Attribute TLVs (RFC 9085 Section 2.1) */
++ BGP_LS_ATTR_SR_CAPABILITIES = 1034, /* SR Capabilities */
++ BGP_LS_ATTR_SR_ALGORITHM = 1035, /* SR Algorithm */
++ BGP_LS_ATTR_SR_LOCAL_BLOCK = 1036, /* SR Local Block */
++ BGP_LS_ATTR_SRMS_PREFERENCE = 1037, /* SRMS Preference */
++
++ /* Node Attribute TLVs (RFC 8814) */
++ BGP_LS_ATTR_NODE_MSD = 266, /* Node MSD */
++
++ /* Link Attribute TLVs (RFC 9552 Section 4.3) */
++ BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL = 1028, /* IPv4 Router-ID of Local Node */
++ BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL = 1029, /* IPv6 Router-ID of Local Node */
++ BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE = 1030, /* IPv4 Router-ID of Remote Node */
++ BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE = 1031, /* IPv6 Router-ID of Remote Node */
++ BGP_LS_ATTR_ADMIN_GROUP = 1088, /* Administrative Group (Color) */
++ BGP_LS_ATTR_MAX_LINK_BW = 1089, /* Maximum Link Bandwidth */
++ BGP_LS_ATTR_MAX_RESV_BW = 1090, /* Maximum Reservable Link Bandwidth */
++ BGP_LS_ATTR_UNRESV_BW = 1091, /* Unreserved Bandwidth */
++ BGP_LS_ATTR_TE_DEFAULT_METRIC = 1092, /* TE Default Metric */
++ BGP_LS_ATTR_LINK_PROTECTION_TYPE = 1093, /* Link Protection Type */
++ BGP_LS_ATTR_MPLS_PROTOCOL_MASK = 1094, /* MPLS Protocol Mask */
++ BGP_LS_ATTR_IGP_METRIC = 1095, /* IGP Metric */
++ BGP_LS_ATTR_SRLG = 1096, /* Shared Risk Link Group */
++ BGP_LS_ATTR_OPAQUE_LINK_ATTR = 1097, /* Opaque Link Attribute */
++ BGP_LS_ATTR_LINK_NAME = 1098, /* Link Name */
++ BGP_LS_ATTR_ADJ_SID = 1099, /* Adjacency SID */
++ BGP_LS_ATTR_LAN_ADJ_SID = 1100, /* LAN Adjacency SID */
++ BGP_LS_ATTR_PEER_NODE_SID = 1101, /* PeerNode SID */
++ BGP_LS_ATTR_PEER_ADJ_SID = 1102, /* PeerAdj SID */
++ BGP_LS_ATTR_PEER_SET_SID = 1103, /* PeerSet SID */
++ BGP_LS_ATTR_LINK_MSD = 1104, /* Link MSD */
++ BGP_LS_ATTR_UNIDIRECTIONAL_LINK_DELAY = 1114, /* Unidirectional Link Delay */
++ BGP_LS_ATTR_MIN_MAX_UNIDIRECTIONAL_LINK_DELAY = 1115, /* Min/Max Unidirectional Link Delay */
++ BGP_LS_ATTR_UNIDIRECTIONAL_DELAY_VARIATION = 1116, /* Unidirectional Delay Variation */
++ BGP_LS_ATTR_UNIDIRECTIONAL_LINK_LOSS = 1117, /* Unidirectional Link Loss */
++ BGP_LS_ATTR_UNIDIRECTIONAL_RESIDUAL_BANDWIDTH = 1118, /* Unidirectional Residual Bandwidth */
++ BGP_LS_ATTR_UNIDIRECTIONAL_AVAILABLE_BANDWIDTH = 1119, /* Unidirectional Available Bandwidth */
++ BGP_LS_ATTR_UNIDIRECTIONAL_UTILIZED_BANDWIDTH = 1120, /* Unidirectional Utilized Bandwidth */
++ BGP_LS_ATTR_EXTENDED_ADMIN_GROUP = 1173, /* Extended Administrative Group */
++
++ /* Prefix Attribute TLVs (RFC 9552 Section 4.3) */
++ BGP_LS_ATTR_IGP_FLAGS = 1152, /* IGP Flags */
++ BGP_LS_ATTR_ROUTE_TAG = 1153, /* Route Tag */
++ BGP_LS_ATTR_EXTENDED_TAG = 1154, /* Extended Tag */
++ BGP_LS_ATTR_PREFIX_METRIC = 1155, /* Prefix Metric */
++ BGP_LS_ATTR_OSPF_FWD_ADDR = 1156, /* OSPF Forwarding Address */
++ BGP_LS_ATTR_OPAQUE_PREFIX_ATTR = 1157, /* Opaque Prefix Attribute */
++ BGP_LS_ATTR_PREFIX_SID = 1158, /* Prefix SID */
++ BGP_LS_ATTR_RANGE = 1159, /* Range */
++ BGP_LS_ATTR_SID_LABEL = 1161, /* SID/Label */
++ BGP_LS_ATTR_PREFIX_ATTR_FLAGS = 1170, /* Prefix Attribute Flags */
++ BGP_LS_ATTR_SRV6_LOCATOR = 1162, /* SRv6 Locator */
++};
++
++/*
++ * ===========================================================================
++ * Constants and Macros
++ * ===========================================================================
++ */
++
++/*
++ * TLV Presence Bitmask Macros
++ * Used to track which optional TLVs are present in descriptors
++ */
++#define BGP_LS_TLV_SET(bitmap, bit) ((bitmap) |= (1ULL << (bit)))
++#define BGP_LS_TLV_CHECK(bitmap, bit) ((bitmap) & (1ULL << (bit)))
++#define BGP_LS_TLV_UNSET(bitmap, bit) ((bitmap) &= ~(1ULL << (bit)))
++#define BGP_LS_TLV_RESET(bitmap) ((bitmap) = 0)
++
++/* Bit positions for Node Descriptor TLVs */
++#define BGP_LS_NODE_DESC_AS_BIT 0
++#define BGP_LS_NODE_DESC_BGP_LS_ID_BIT 1
++#define BGP_LS_NODE_DESC_OSPF_AREA_BIT 2
++#define BGP_LS_NODE_DESC_IGP_ROUTER_BIT 3
++
++/* Bit positions for Link Descriptor TLVs */
++#define BGP_LS_LINK_DESC_LINK_ID_BIT 0
++#define BGP_LS_LINK_DESC_IPV4_INTF_BIT 1
++#define BGP_LS_LINK_DESC_IPV4_NEIGH_BIT 2
++#define BGP_LS_LINK_DESC_IPV6_INTF_BIT 3
++#define BGP_LS_LINK_DESC_IPV6_NEIGH_BIT 4
++#define BGP_LS_LINK_DESC_MT_ID_BIT 5
++#define BGP_LS_LINK_DESC_REMOTE_AS_BIT 6
++
++/* Bit positions for Prefix Descriptor TLVs */
++#define BGP_LS_PREFIX_DESC_MT_ID_BIT 0
++#define BGP_LS_PREFIX_DESC_OSPF_ROUTE_BIT 1
++#define BGP_LS_PREFIX_DESC_IP_REACH_BIT 2
++
++/* Maximum number of MT-IDs per descriptor */
++#define BGP_LS_MAX_MT_ID 16
++
++/*
++ * IGP Router-ID Length Constants (RFC 9552 Section 5.2.1.4)
++ */
++#define BGP_LS_IGP_ROUTER_ID_OSPF_LEN 4 /* OSPFv2/v3 non-pseudonode: Router-ID */
++#define BGP_LS_IGP_ROUTER_ID_ISIS_LEN 6 /* IS-IS non-pseudonode: ISO System-ID */
++#define BGP_LS_IGP_ROUTER_ID_ISIS_PSEUDO_LEN 7 /* IS-IS pseudonode: System-ID + PSN */
++#define BGP_LS_IGP_ROUTER_ID_OSPF_PSEUDO_LEN 8 /* OSPFv2/v3 pseudonode: Router-ID + Interface ID */
++#define BGP_LS_IGP_ROUTER_ID_DIRECT_IPV4_LEN IPV4_MAX_BYTELEN /* Direct/Static: IPv4 address */
++#define BGP_LS_IGP_ROUTER_ID_DIRECT_IPV6_LEN IPV6_MAX_BYTELEN /* Direct/Static: IPv6 address */
++#define BGP_LS_IGP_ROUTER_ID_MIN_SIZE BGP_LS_IGP_ROUTER_ID_OSPF_LEN /* Minimum size (4 bytes) */
++#define BGP_LS_IGP_ROUTER_ID_MAX_SIZE \
++ BGP_LS_IGP_ROUTER_ID_DIRECT_IPV6_LEN /* Maximum size (16 bytes) */
++
++/*
++ * TLV Size Constants (for wire format size calculation)
++ */
++#define BGP_LS_TLV_HDR_SIZE 4 /* TLV Type (2) + Length (2) */
++#define BGP_LS_PROTOCOL_ID_SIZE 1 /* Protocol-ID field */
++#define BGP_LS_IDENTIFIER_SIZE 8 /* Identifier field (Instance ID) */
++#define BGP_LS_NLRI_HDR_SIZE \
++ (BGP_LS_PROTOCOL_ID_SIZE + BGP_LS_IDENTIFIER_SIZE) /* Protocol-ID + Identifier */
++#define BGP_LS_NLRI_TYPE_SIZE 2 /* NLRI Type field */
++#define BGP_LS_NLRI_LENGTH_SIZE 2 /* NLRI Length field */
++/* Minimum NLRI length: Protocol-ID + Identifier + Node Descriptor TLV header + min descriptor */
++#define BGP_LS_NLRI_MIN_LENGTH \
++ (BGP_LS_PROTOCOL_ID_SIZE + BGP_LS_IDENTIFIER_SIZE + BGP_LS_TLV_HDR_SIZE)
++#define BGP_LS_AS_NUMBER_SIZE 4 /* AS Number value */
++#define BGP_LS_BGP_LS_ID_SIZE 4 /* BGP-LS Identifier value */
++#define BGP_LS_OSPF_AREA_ID_SIZE 4 /* OSPF Area-ID value */
++#define BGP_LS_LINK_ID_SIZE 8 /* Link Local ID (4) + Remote ID (4) */
++#define BGP_LS_IPV4_ADDR_SIZE IPV4_MAX_BYTELEN /* IPv4 address */
++#define BGP_LS_IPV6_ADDR_SIZE IPV6_MAX_BYTELEN /* IPv6 address */
++#define BGP_LS_OSPF_ROUTE_TYPE_SIZE 1 /* OSPF Route Type value */
++#define BGP_LS_MT_ID_SIZE 2 /* Multi-Topology ID (per entry) */
++#define BGP_LS_PREFIX_LEN_SIZE 1 /* IP prefix length field */
++
++/*
++ * IGP Metric can be 1, 2, or 3 bytes
++ */
++#define BGP_LS_IGP_METRIC_MAX_LEN 3
++
++/*
++ * IGP MSD Type
++ * RFC 8491 Section 6, Figure 6
++ */
++#define BGP_LS_IGP_MSD_TYPE_BASE_MPLS 1
++
++/*
++ * Maximum values for arrays
++ */
++#define BGP_LS_MAX_SRLG 64 /* Maximum SRLGs per link */
++#define BGP_LS_MAX_UNRESV_BW 8 /* 8 priority classes */
++#define BGP_LS_MAX_ROUTE_TAGS 16 /* Maximum route tags */
++#define BGP_LS_MAX_EXT_ADMIN_GROUPS 256 /* Maximum number of admin groups in Extended Admin Group TLV */
++
++/*
++ * Bit positions for attribute presence bitmasks
++ */
++#define BGP_LS_ATTR_NODE_FLAGS_BIT 0
++#define BGP_LS_ATTR_NODE_NAME_BIT 1
++#define BGP_LS_ATTR_ISIS_AREA_BIT 2
++#define BGP_LS_ATTR_SR_CAPABILITIES_BIT 3
++#define BGP_LS_ATTR_SR_ALGORITHM_BIT 4
++#define BGP_LS_ATTR_SR_LOCAL_BLOCK_BIT 5
++#define BGP_LS_ATTR_NODE_MSD_BIT 6
++#define BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT 7
++#define BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT 8
++#define BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE_BIT 9
++#define BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE_BIT 10
++#define BGP_LS_ATTR_ADMIN_GROUP_BIT 11
++#define BGP_LS_ATTR_MAX_LINK_BW_BIT 12
++#define BGP_LS_ATTR_MAX_RESV_BW_BIT 13
++#define BGP_LS_ATTR_UNRESV_BW_BIT 14
++#define BGP_LS_ATTR_TE_METRIC_BIT 15
++#define BGP_LS_ATTR_LINK_PROTECTION_BIT 16
++#define BGP_LS_ATTR_MPLS_PROTOCOL_BIT 17
++#define BGP_LS_ATTR_IGP_METRIC_BIT 18
++#define BGP_LS_ATTR_SRLG_BIT 19
++#define BGP_LS_ATTR_LINK_NAME_BIT 20
++#define BGP_LS_ATTR_ADJ_SID_BIT 21
++#define BGP_LS_ATTR_LINK_MSD_BIT 22
++#define BGP_LS_ATTR_EXT_ADMIN_GROUP_BIT 23
++#define BGP_LS_ATTR_DELAY_BIT 24
++#define BGP_LS_ATTR_MIN_MAX_DELAY_BIT 25
++#define BGP_LS_ATTR_JITTER_BIT 26
++#define BGP_LS_ATTR_PKT_LOSS_BIT 27
++#define BGP_LS_ATTR_RESIDUAL_BW_BIT 28
++#define BGP_LS_ATTR_AVAILABLE_BW_BIT 29
++#define BGP_LS_ATTR_UTILIZED_BW_BIT 30
++#define BGP_LS_ATTR_IGP_FLAGS_BIT 31
++#define BGP_LS_ATTR_ROUTE_TAG_BIT 32
++#define BGP_LS_ATTR_EXTENDED_TAG_BIT 33
++#define BGP_LS_ATTR_PREFIX_METRIC_BIT 34
++#define BGP_LS_ATTR_OSPF_FWD_ADDR_BIT 35
++#define BGP_LS_ATTR_PREFIX_SID_BIT 36
++#define BGP_LS_ATTR_RANGE_BIT 37
++#define BGP_LS_ATTR_SID_LABEL_BIT 38
++#define BGP_LS_ATTR_SRV6_LOCATOR_BIT 39
++
++/*
++ * Node Flag Bits (TLV 1024)
++ * RFC 9552 Section 4.3.1
++ */
++#define BGP_LS_NODE_FLAG_OVERLOAD 0x80 /* Overload Bit */
++#define BGP_LS_NODE_FLAG_ATTACHED 0x40 /* Attached Bit */
++#define BGP_LS_NODE_FLAG_EXTERNAL 0x20 /* External Bit */
++#define BGP_LS_NODE_FLAG_ABR 0x10 /* ABR Bit */
++#define BGP_LS_NODE_FLAG_ROUTER 0x08 /* Router Bit */
++#define BGP_LS_NODE_FLAG_V6 0x04 /* V6 Bit */
++
++/*
++ * IGP Prefix Flags (TLV 1152)
++ * RFC 9552 Section 4.3.4.1
++ */
++#define BGP_LS_PREFIX_FLAG_DOWN 0x80 /* IS-IS Down Bit */
++#define BGP_LS_PREFIX_FLAG_NO_UNICAST 0x40 /* OSPF No-Unicast Bit */
++#define BGP_LS_PREFIX_FLAG_LOCAL 0x20 /* OSPF Local Address Bit */
++#define BGP_LS_PREFIX_FLAG_PROPAGATE 0x10 /* OSPF Propagate NSSA Bit */
++#define BGP_LS_PREFIX_FLAG_NODE 0x08 /* Node Prefix Attached Flag */
++
++/*
++ * IGP Prefix SID Flags (TLV 1158)
++ * RFC 9085 Section 2.3.1
++ */
++#define BGP_LS_PREFIX_SID_FLAG_VALUE 0x08 /* Same for IS-IS, OSPFv2, OSPFv3 */
++#define BGP_LS_PREFIX_SID_FLAG_LOCAL 0x04 /* Same for IS-IS, OSPFv2, OSPFv3 */
++
++/*
++ * IS-IS RFC 8667, 2.2.1
++ * OSPFv2 RFC 8665, 6.1
++ * OSPFv3 RFC 8666, 7.1
++ */
++#define ISIS_EXT_SUBTLV_LINK_ADJ_SID_VFLG 0x20
++#define ISIS_EXT_SUBTLV_LINK_ADJ_SID_LFLG 0x10
++#define OSPF_EXT_SUBTLV_LINK_ADJ_SID_VFLG 0x40
++#define OSPF_EXT_SUBTLV_LINK_ADJ_SID_LFLG 0x20
++
++
++/*
++ * ===========================================================================
++ * Descriptor Structures (RFC 9552 Section 5.2)
++ * ===========================================================================
++ *
++ * These structures identify topology elements (nodes, links, prefixes)
++ * per RFC 9552 Section 5.2
++ */
++
++/*
++ * Node Descriptor - Identifies a router/node in the topology
++ * RFC 9552, Section 5.2.1
++ */
++struct bgp_ls_node_descriptor {
++ uint16_t present_tlvs; /* Bitmask of present TLVs */
++ as_t asn; /* Autonomous System Number */
++ uint32_t bgp_ls_id; /* BGP-LS Identifier (deprecated) */
++ uint32_t ospf_area_id; /* OSPF Area ID */
++ uint8_t igp_router_id_len; /* Length of IGP Router ID (4-16 bytes) */
++ uint8_t igp_router_id[BGP_LS_IGP_ROUTER_ID_MAX_SIZE]; /* IGP Router ID (ISIS, OSPF, Direct, or Static configuration) */
++};
++
++/*
++ * Link Descriptor - Identifies a link between two nodes
++ * RFC 9552, Section 5.2.2
++ */
++struct bgp_ls_link_descriptor {
++ uint32_t present_tlvs; /* Bitmask of present TLVs */
++ uint32_t link_local_id; /* Link Local Identifier */
++ uint32_t link_remote_id; /* Link Remote Identifier */
++ struct in_addr ipv4_intf_addr; /* IPv4 Interface Address */
++ struct in_addr ipv4_neigh_addr; /* IPv4 Neighbor Address */
++ struct in6_addr ipv6_intf_addr; /* IPv6 Interface Address */
++ struct in6_addr ipv6_neigh_addr; /* IPv6 Neighbor Address */
++ as_t remote_asn; /* Remote AS Number */
++ uint8_t mt_id_count; /* Number of Multi-Topology IDs */
++ uint16_t *mt_id; /* Multi-Topology IDs */
++};
++
++/*
++ * Prefix Descriptor - Identifies an IP prefix advertised by a node
++ * RFC 9552, Section 5.2.3
++ */
++struct bgp_ls_prefix_descriptor {
++ uint16_t present_tlvs; /* Bitmask of present TLVs */
++ uint8_t mt_id_count; /* Number of Multi-Topology IDs */
++ uint16_t *mt_id; /* Multi-Topology IDs */
++ enum bgp_ls_ospf_route_type ospf_route_type; /* OSPF Route Type */
++ struct prefix prefix; /* IP prefix (IPv4 or IPv6) */
++};
++
++/*
++ * ===========================================================================
++ * BGP-LS NLRI Structures (RFC 9552 Section 5.2)
++ * ===========================================================================
++ *
++ * These structures represent complete BGP-LS NLRIs as exchanged in UPDATE
++ * messages. Each NLRI consists of:
++ * 1. Protocol-ID - IGP protocol that originated the information
++ * 2. Identifier - 64-bit instance identifier for disambiguation
++ * 3. NLRI-specific descriptors that identify the topology element
++ */
++
++/*
++ * Node NLRI (Type 1) - Identifies a router/node in the topology
++ * RFC 9552, Section 5.2
++ *
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (64 bits) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ */
++struct bgp_ls_node_nlri {
++ enum bgp_ls_protocol_id protocol_id; /* IGP protocol */
++ uint64_t identifier; /* Instance identifier */
++ struct bgp_ls_node_descriptor local_node; /* Node identity */
++};
++
++/*
++ * Link NLRI (Type 2) - Identifies a link between two nodes
++ * RFC 9552, Section 5.2
++ *
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (64 bits) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Remote Node Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Link Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ */
++struct bgp_ls_link_nlri {
++ enum bgp_ls_protocol_id protocol_id; /* IGP protocol */
++ uint64_t identifier; /* Instance identifier */
++ struct bgp_ls_node_descriptor local_node; /* Source node */
++ struct bgp_ls_node_descriptor remote_node; /* Destination node */
++ struct bgp_ls_link_descriptor link_desc; /* Link identity */
++};
++
++/*
++ * Prefix NLRI (Type 3/4) - Identifies an IP prefix advertised by a node
++ * RFC 9552, Section 5.2
++ *
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+
++ * | Protocol-ID |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Identifier |
++ * | (64 bits) |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Local Node Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * // Prefix Descriptors (variable) //
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Note: Same structure used for both IPv4 (Type 3) and IPv6 (Type 4)
++ * The prefix_desc.prefix.family field distinguishes between them
++ */
++struct bgp_ls_prefix_nlri {
++ enum bgp_ls_protocol_id protocol_id; /* IGP protocol */
++ uint64_t identifier; /* Instance identifier */
++ struct bgp_ls_node_descriptor local_node; /* Advertising node */
++ struct bgp_ls_prefix_descriptor prefix_desc; /* Prefix identity */
++};
++
++/* Forward declare the hash table */
++PREDECL_HASH(bgp_ls_nlri_hash);
++
++/*
++ * Top-level BGP-LS NLRI structure (RFC 9552 Section 5.2)
++ *
++ * BGP-LS NLRI encoding in UPDATE message:
++ *
++ * 0 1 2 3
++ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | Type | Length |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ * | |
++ * // NLRI Value (variable) //
++ * | |
++ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
++ *
++ * Where:
++ * Type = NLRI Type (1=Node, 2=Link, 3=IPv4 Prefix, 4=IPv6 Prefix)
++ * Length = Length of NLRI Value in octets
++ * Value = Type-specific NLRI data (see struct bgp_ls_*_nlri)
++ *
++ * This structure represents the decoded NLRI after parsing the wire format.
++ */
++struct bgp_ls_nlri {
++ enum bgp_ls_nlri_type nlri_type; /* Discriminator */
++ union {
++ struct bgp_ls_node_nlri node; /* Node NLRI (Type 1) */
++ struct bgp_ls_link_nlri link; /* Link NLRI (Type 2) */
++ struct bgp_ls_prefix_nlri prefix; /* Prefix NLRI (Type 3/4) */
++ } nlri_data;
++
++ unsigned long refcnt; /* Reference count */
++ uint32_t id;
++
++ /* Hash table linkage */
++ struct bgp_ls_nlri_hash_item hash_item;
++};
++
++/* Forward declare the hash table */
++PREDECL_HASH(bgp_ls_attr_hash);
++
++/* BGP-LS Attribute (Type 29) */
++struct bgp_ls_attr {
++ uint64_t present_tlvs; /* Bitmask of present TLVs */
++
++ /* Node Flag Bits (TLV 1024) */
++ uint8_t node_flags;
++
++ /* Node Name (TLV 1026) */
++ char *node_name;
++
++ /* IS-IS Area Identifier (TLV 1027) */
++ uint8_t isis_area_id_len;
++ uint8_t *isis_area_id;
++
++ /* SR Capabilities (TLV 1034), only one range supported */
++ struct bgp_ls_srgb {
++ uint32_t lower_bound; /* MPLS label lower bound */
++ uint32_t range_size; /* MPLS label range size */
++ uint8_t flag; /* IS-IS SRGB flags */
++ } srgb;
++
++ /* Node MSD (TLV 266) */
++ uint8_t msd;
++
++ /* Multi-Topology IDs (multiple TLVs, same as descriptor) */
++ uint8_t mt_id_count;
++ uint16_t *mt_id;
++
++ /* IPv4/IPv6 Router-IDs (TLVs 1028-1031) */
++ struct in_addr ipv4_router_id_local;
++ struct in6_addr ipv6_router_id_local;
++ struct in_addr ipv4_router_id_remote;
++ struct in6_addr ipv6_router_id_remote;
++
++ /* Administrative Group (TLV 1088) */
++ uint32_t admin_group;
++
++ /* Maximum Link Bandwidth (TLV 1089) - IEEE floating point */
++ float max_link_bw;
++
++ /* Maximum Reservable Bandwidth (TLV 1090) */
++ float max_resv_bw;
++
++ /* Unreserved Bandwidth (TLV 1091) - 8 priority levels */
++ float unreserved_bw[BGP_LS_MAX_UNRESV_BW];
++
++ /* TE Default Metric (TLV 1092) */
++ uint32_t te_metric;
++
++ /* Link Protection Type (TLV 1093) */
++ uint16_t link_protection;
++
++ /* MPLS Protocol Mask (TLV 1094) */
++ uint8_t mpls_protocol_mask;
++
++ /* IGP Metric (TLV 1095) - Variable length 1-3 bytes */
++ uint8_t igp_metric_len;
++ uint32_t igp_metric;
++
++ /* Shared Risk Link Group (TLV 1096) */
++ uint16_t srlg_count;
++ uint32_t *srlg_values;
++
++ /* Link Name (TLV 1098) */
++ char *link_name;
++
++ /* Adjacency SID (TLV 1099) */
++#define BGP_LS_ADJ_MAX 4
++ uint8_t adj_sid_count; /* number of adj_sid elements */
++ struct bgp_ls_adjacency {
++ uint32_t sid; /* SID as MPLS label or index */
++ uint8_t flags; /* Flags */
++ uint8_t weight; /* Administrative weight */
++ } adj_sid[BGP_LS_ADJ_MAX]; /* IPv4/IPv6 & Primary/Backup Adj. SID */
++
++ /* Extended Administrative Group (TLV 1173) */
++ struct admin_group ext_admin_group;
++
++ /* Performance Metrics - Unidirectional Link Delay (TLV 1114) */
++ uint32_t delay;
++
++ /* Min/Max Unidirectional Link Delay (TLV 1115) */
++ uint32_t min_delay;
++ uint32_t max_delay;
++
++ /* Unidirectional Delay Variation (TLV 1116) */
++ uint32_t jitter;
++
++ /* Unidirectional Packet Loss (TLV 1117) */
++ uint32_t pkt_loss;
++
++ /* Unidirectional Residual Bandwidth (TLV 1118) */
++ float residual_bw;
++
++ /* Unidirectional Available Bandwidth (TLV 1119) */
++ float available_bw;
++
++ /* Unidirectional Utilized Bandwidth (TLV 1120) */
++ float utilized_bw;
++
++ /* IGP Flags (TLV 1152) */
++ uint8_t igp_flags;
++
++ /* Route Tags (TLV 1153) */
++ uint16_t route_tag_count;
++ uint32_t *route_tags;
++
++ /* Extended Tags (TLV 1154) */
++ uint8_t extended_tag_count;
++ uint64_t *extended_tags;
++
++ /* Prefix Metric (TLV 1155) */
++ uint32_t prefix_metric;
++
++ /* OSPF Forwarding Address (TLV 1156) */
++ struct in_addr ospf_fwd_addr; /* IPv4 */
++ struct in6_addr ospf_fwd_addr6; /* IPv6 */
++
++ /* Prefix-SID (TLV 1158) */
++ struct prefix_sid {
++ uint8_t sid_flag; /* Segment Routing Flags */
++ uint8_t algo; /* Algorithm for Segment Routing */
++ uint32_t sid; /* Segment Routing ID */
++ } prefix_sid;
++
++ /* Opaque Node Attribute (TLV 1025/1097/1157) */
++ uint16_t opaque_len;
++ uint8_t *opaque_data;
++
++ unsigned long refcnt; /* Reference count */
++
++ /* Hash table linkage */
++ struct bgp_ls_attr_hash_item hash_item;
++};
++
++/* Function prototypes */
++
++/* Descriptor comparison functions */
++extern int bgp_ls_node_descriptor_cmp(const struct bgp_ls_node_descriptor *d1,
++ const struct bgp_ls_node_descriptor *d2);
++extern int bgp_ls_link_descriptor_cmp(const struct bgp_ls_link_descriptor *d1,
++ const struct bgp_ls_link_descriptor *d2);
++extern int bgp_ls_prefix_descriptor_cmp(const struct bgp_ls_prefix_descriptor *d1,
++ const struct bgp_ls_prefix_descriptor *d2);
++
++/* NLRI comparison functions */
++extern int bgp_ls_nlri_cmp(const struct bgp_ls_nlri *nlri1, const struct bgp_ls_nlri *nlri2);
++extern int bgp_ls_attr_cmp(const struct bgp_ls_attr *attr1, const struct bgp_ls_attr *attr2);
++extern bool bgp_ls_attr_same(const struct bgp_ls_attr *attr1, const struct bgp_ls_attr *attr2);
++
++/* NLRI memory management functions */
++extern struct bgp_ls_nlri *bgp_ls_nlri_alloc(void);
++extern void bgp_ls_nlri_free(struct bgp_ls_nlri *nlri);
++extern struct bgp_ls_attr *bgp_ls_attr_alloc(void);
++extern void bgp_ls_attr_free(struct bgp_ls_attr *attr);
++extern struct bgp_ls_nlri *bgp_ls_nlri_copy(const struct bgp_ls_nlri *nlri);
++struct bgp_ls_attr *bgp_ls_attr_copy(const struct bgp_ls_attr *src);
++
++/* NLRI validation functions */
++extern bool bgp_ls_nlri_validate(const struct bgp_ls_nlri *nlri);
++
++/* NLRI size calculation helpers */
++extern size_t bgp_ls_nlri_size(const struct bgp_ls_nlri *nlri);
++
++/* NLRI get protocol_id helper */
++extern enum bgp_ls_protocol_id bgp_ls_nlri_protocol_id(const struct bgp_ls_nlri *nlri);
++
++/* String conversion helpers */
++extern const char *bgp_ls_protocol_id_str(enum bgp_ls_protocol_id proto_id);
++extern const char *bgp_ls_nlri_type_str(enum bgp_ls_nlri_type nlri_type);
++extern const char *bgp_ls_node_descriptor_tlv_str(enum bgp_ls_node_descriptor_tlv tlv_type);
++extern const char *bgp_ls_link_descriptor_tlv_str(enum bgp_ls_link_descriptor_tlv tlv_type);
++extern const char *bgp_ls_prefix_descriptor_tlv_str(enum bgp_ls_prefix_descriptor_tlv tlv_type);
++extern const char *bgp_ls_ospf_route_type_str(enum bgp_ls_ospf_route_type route_type);
++
++/* Json conversion helpers */
++extern const char *bgp_ls_ospf_route_type_str_json(enum bgp_ls_ospf_route_type route_type);
++
++/*
++ * ===========================================================================
++ * Hash Table Management
++ * ===========================================================================
++ */
++
++/* Unified hash functions - dispatch internally based on nlri_type */
++extern unsigned int bgp_ls_nlri_hash_key(const struct bgp_ls_nlri *nlri);
++extern int bgp_ls_nlri_hash_cmp(const struct bgp_ls_nlri *n1, const struct bgp_ls_nlri *n2);
++
++extern unsigned int bgp_ls_attr_hash_key(const struct bgp_ls_attr *ls_attr);
++extern int bgp_ls_attr_hash_cmp(const struct bgp_ls_attr *a1, const struct bgp_ls_attr *a2);
++
++/* Declare the typesafe hash table */
++DECLARE_HASH(bgp_ls_nlri_hash, struct bgp_ls_nlri, hash_item, bgp_ls_nlri_hash_cmp,
++ bgp_ls_nlri_hash_key);
++
++/* Declare the typesafe hash table */
++DECLARE_HASH(bgp_ls_attr_hash, struct bgp_ls_attr, hash_item, bgp_ls_attr_hash_cmp,
++ bgp_ls_attr_hash_key);
++
++/*
++ * ===========================================================================
++ * BGP-LS NLRI Hash Table Management Functions
++ * ===========================================================================
++ */
++
++/* Insert NLRI into unified hash table */
++extern struct bgp_ls_nlri *bgp_ls_nlri_get(struct bgp_ls_nlri_hash_head *hash, struct bgp *bgp,
++ struct bgp_ls_nlri *nlri);
++
++/* Lookup NLRI in unified hash table */
++extern struct bgp_ls_nlri *bgp_ls_nlri_lookup(struct bgp_ls_nlri_hash_head *hash,
++ struct bgp_ls_nlri *nlri);
++
++/* NLRI interning (lookup + lock/unlock) */
++extern struct bgp_ls_nlri *bgp_ls_nlri_intern(struct bgp_ls_nlri *ls_nlri);
++extern void bgp_ls_nlri_unintern(struct bgp_ls_nlri **pls_nlri);
++
++/* Insert BGP-LS attribute into unified hash table */
++extern struct bgp_ls_attr *bgp_ls_attr_get(struct bgp_ls_attr_hash_head *hash, struct bgp *bgp,
++ struct bgp_ls_attr *ls_attr);
++
++/* Lookup BGP-LS attribute in unified hash table */
++extern struct bgp_ls_attr *bgp_ls_attr_lookup(struct bgp_ls_attr_hash_head *hash,
++ const struct bgp_ls_attr *ls_attr);
++
++/* BGP-LS attribute interning (lookup + lock/unlock) */
++extern struct bgp_ls_attr *bgp_ls_attr_intern(struct bgp_ls_attr *ls_attr);
++extern void bgp_ls_attr_unintern(struct bgp_ls_attr **pls_attr);
++
++/*
++ * ===========================================================================
++ * NLRI Encoding Functions
++ * ===========================================================================
++ */
++
++/* Encode Node Descriptor TLVs into stream */
++extern int bgp_ls_encode_node_descriptor(struct stream *s,
++ const struct bgp_ls_node_descriptor *desc,
++ uint16_t tlv_type);
++
++/* Encode Link Descriptor TLVs into stream */
++extern int bgp_ls_encode_link_descriptor(struct stream *s,
++ const struct bgp_ls_link_descriptor *desc);
++
++/* Encode Prefix Descriptor TLVs into stream */
++extern int bgp_ls_encode_prefix_descriptor(struct stream *s,
++ const struct bgp_ls_prefix_descriptor *desc);
++
++/* Encode Node NLRI to wire format */
++extern int bgp_ls_encode_node_nlri(struct stream *s, const struct bgp_ls_node_nlri *nlri);
++
++/* Encode Link NLRI to wire format */
++extern int bgp_ls_encode_link_nlri(struct stream *s, const struct bgp_ls_link_nlri *nlri);
++
++/* Encode Prefix NLRI to wire format */
++extern int bgp_ls_encode_prefix_nlri(struct stream *s, const struct bgp_ls_prefix_nlri *nlri,
++ enum bgp_ls_nlri_type nlri_type);
++
++/* Encode complete NLRI */
++extern int bgp_ls_encode_nlri(struct stream *s, const struct bgp_ls_nlri *nlri);
++
++/* Encode BGP-LS Attributes (Type 29 TLVs) */
++extern int bgp_ls_encode_attr(struct stream *s, const struct bgp_ls_attr *attr,
++ enum bgp_ls_protocol_id protocol_id);
++
++/*
++ * Get Prefix-SID attribute SID length by flags
++ *
++ * @return 3 or 4 in normal case, -1 in error case
++ */
++extern int bgp_ls_attr_prefix_sid_len(uint8_t flags);
++
++/*
++ * Get Adjacency SID attribute Label/Index SID length by flags
++ *
++ * @return 3 or 4 in normal case, -1 in error case
++ */
++extern int bgp_ls_attr_adjacency_sid_len(uint8_t flags, enum bgp_ls_protocol_id protocol_id);
++
++/*
++ * ===========================================================================
++ * NLRI Decoding Functions
++ * ===========================================================================
++ */
++
++/* Decode Node Descriptor TLVs from stream */
++extern int bgp_ls_decode_node_descriptor(struct stream *s, struct bgp_ls_node_descriptor *desc,
++ uint16_t desc_length);
++
++/* Decode Link Descriptor TLVs from stream */
++extern int bgp_ls_decode_link_descriptor(struct stream *s, struct bgp_ls_link_descriptor *desc,
++ size_t total_length);
++
++/* Decode Prefix Descriptor TLVs from stream */
++extern int bgp_ls_decode_prefix_descriptor(struct stream *s, struct bgp_ls_prefix_descriptor *desc,
++ size_t total_length, bool is_ipv6);
++
++/* Decode Node NLRI from wire format */
++extern int bgp_ls_decode_node_nlri(struct stream *s, struct bgp_ls_nlri *nlri,
++ uint16_t nlri_length);
++
++/* Decode Link NLRI from wire format */
++extern int bgp_ls_decode_link_nlri(struct stream *s, struct bgp_ls_nlri *nlri,
++ uint16_t nlri_length);
++
++/* Decode Prefix NLRI from wire format */
++extern int bgp_ls_decode_prefix_nlri(struct stream *s, struct bgp_ls_nlri *nlri,
++ uint16_t nlri_type, uint16_t nlri_length);
++
++/* Decode complete NLRI */
++extern int bgp_ls_decode_nlri(struct stream *s, struct bgp_ls_nlri *nlri);
++
++/*
++ * Parse BGP-LS Attributes (Type 29 TLVs)
++ *
++ * @param s Stream containing the attribute TLVs
++ * @param total_length Total length of all TLVs in bytes
++ * @param attr Pointer to node attribute structure to populate
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_parse_attr(struct stream *s, uint16_t total_length, struct bgp_ls_attr *attr);
++
++/*
++ * Process BGP-LS Attributes when protocol id is known
++ *
++ * Parsing of some attributes require protocol id, but bgp_ls_parse_attr
++ * is called before NLRI is parsed, so at that time protocol id is unknown
++ *
++ * This function is called after bgp_ls_parse_attr when protocol id is known
++ *
++ * @param attr Pointer to node attribute structure to populate
++ * @param protocol_id IGP protocol
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_attr_process_with_protocol_id(struct bgp_ls_attr *attr,
++ enum bgp_ls_protocol_id protocol_id);
++
++/*
++ * Convert BGP-LS Attributes to JSON object
++ *
++ * @param ls_attr Pointer to BGP-LS attribute structure to convert
++ *
++ * Used for "show bgp" commands to display link-state topology information in json
++ *
++ * @return json object
++ */
++extern struct json_object *bgp_ls_attr_to_json(struct bgp_ls_attr *ls_attr);
++
++/*
++ * Display BGP-LS Attributes to VTY output
++ *
++ * @param vty VTY output context
++ * @param ls_attr Pointer to BGP-LS attribute structure to display
++ *
++ * Used for "show bgp" commands to display link-state topology information
++ */
++extern void bgp_ls_attr_display(struct vty *vty, struct bgp_ls_attr *ls_attr);
++
++/*
++ * Display BGP-LS NLRI details to VTY output
++ *
++ * @param vty VTY output context
++ * @param nlri Pointer to BGP-LS NLRI structure to display
++ *
++ * Displays Node/Link/Prefix NLRI information including descriptors,
++ * protocol information, and multi-topology details
++ */
++extern void bgp_ls_nlri_display(struct vty *vty, struct bgp_ls_nlri *nlri);
++
++#endif /* _FRR_BGP_LS_NLRI_H */
+diff --git a/bgpd/bgp_ls_ted.c b/bgpd/bgp_ls_ted.c
+new file mode 100644
+index 0000000000..0750dd690a
+--- /dev/null
++++ b/bgpd/bgp_ls_ted.c
+@@ -0,0 +1,1403 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * BGP Link-State Traffic Engineering Database (RFC 9552)
++ * Copyright (C) 2025 Carmine Scarpitta
++ */
++
++#include <zebra.h>
++
++#include "lib/memory.h"
++#include "lib/log.h"
++#include "lib/prefix.h"
++#include "lib/stream.h"
++#define UNKNOWN LS_UNKNOWN
++#include "lib/link_state.h"
++#undef UNKNOWN
++
++#include "bgpd/bgpd.h"
++#include "bgpd/bgp_debug.h"
++#include "bgpd/bgp_errors.h"
++#include "bgpd/bgp_ls.h"
++#include "bgpd/bgp_ls_nlri.h"
++#include "bgpd/bgp_ls_ted.h"
++#include "bgpd/bgp_route.h"
++
++/*
++ * ===========================================================================
++ * Node Attribute Population
++ * ===========================================================================
++ */
++
++/*
++ * Populate BGP-LS Attributes from Link State Node
++ */
++int bgp_ls_populate_node_attr(struct ls_node *ls_node, struct bgp_ls_attr *attr)
++{
++ if (!ls_node || !attr)
++ return -1;
++
++ /* Node Flag Bits (TLV 1024) */
++ if (CHECK_FLAG(ls_node->flags, LS_NODE_FLAG)) {
++ attr->node_flags = ls_node->node_flag;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_NODE_FLAGS_BIT);
++ }
++
++ /* Node Name (TLV 1026) */
++ if (CHECK_FLAG(ls_node->flags, LS_NODE_NAME) && ls_node->name[0] != '\0') {
++ size_t name_len = strlen(ls_node->name);
++
++ attr->node_name = XCALLOC(MTYPE_BGP_LS_ATTR, name_len + 1);
++ memcpy(attr->node_name, ls_node->name, name_len + 1);
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_NODE_NAME_BIT);
++ }
++
++ /* IS-IS Area Identifier (TLV 1027) */
++ if (CHECK_FLAG(ls_node->flags, LS_NODE_ISIS_AREA_ID)) {
++ attr->isis_area_id_len = ls_node->isis_area_id_len;
++ attr->isis_area_id = XCALLOC(MTYPE_BGP_LS_ATTR, attr->isis_area_id_len);
++ memcpy(attr->isis_area_id, ls_node->isis_area_id, attr->isis_area_id_len);
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_ISIS_AREA_BIT);
++ }
++
++ /* SR Capabilities (TLV 1034) */
++ if (CHECK_FLAG(ls_node->flags, LS_NODE_SR)) {
++ attr->srgb.flag = ls_node->srgb.flag;
++ attr->srgb.lower_bound = ls_node->srgb.lower_bound;
++ attr->srgb.range_size = ls_node->srgb.range_size;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_SR_CAPABILITIES_BIT);
++ }
++
++ /* Node MSD (TLV 266) */
++ if (CHECK_FLAG(ls_node->flags, LS_NODE_MSD)) {
++ attr->msd = ls_node->msd;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_NODE_MSD_BIT);
++ }
++
++ /* IPv4 Router-ID (TLV 1028) */
++ if (CHECK_FLAG(ls_node->flags, LS_NODE_ROUTER_ID)) {
++ attr->ipv4_router_id_local = ls_node->router_id;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_IPV4_ROUTER_ID_LOCAL_BIT);
++ }
++
++ /* IPv6 Router-ID (TLV 1029) */
++ if (CHECK_FLAG(ls_node->flags, LS_NODE_ROUTER_ID6)) {
++ attr->ipv6_router_id_local = ls_node->router_id6;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_IPV6_ROUTER_ID_LOCAL_BIT);
++ }
++
++ return 0;
++}
++
++/*
++ * ===========================================================================
++ * Link Attribute Population
++ * ===========================================================================
++ */
++
++static void populate_adj_sid(struct ls_attributes *ls_attr, int index, int flag,
++ struct bgp_ls_attr *attr, enum bgp_ls_protocol_id protocol_id)
++{
++ int sid_len;
++
++ if (!CHECK_FLAG(ls_attr->flags, flag))
++ return;
++
++ if (attr->adj_sid_count == BGP_LS_ADJ_MAX) {
++ zlog_warn("BGP-LS: %s maximum supported number of Adjacency SID is %d, ignoring others",
++ __func__, BGP_LS_ADJ_MAX);
++ return;
++ }
++
++ sid_len = bgp_ls_attr_adjacency_sid_len(ls_attr->adj_sid[index].flags, protocol_id);
++ if (sid_len == -1) {
++ zlog_warn("BGP-LS: %s TED contains wrong combination of V-Flag and L-Flag for Adjacent SID",
++ __func__);
++ return;
++ }
++
++ attr->adj_sid[attr->adj_sid_count].sid = ls_attr->adj_sid[index].sid;
++ attr->adj_sid[attr->adj_sid_count].flags = ls_attr->adj_sid[index].flags;
++ attr->adj_sid[attr->adj_sid_count].weight = ls_attr->adj_sid[index].weight;
++ attr->adj_sid_count++;
++
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_ADJ_SID_BIT);
++}
++
++/*
++ * Populate BGP-LS Attributes from Link State Attributes
++ */
++int bgp_ls_populate_link_attr(struct ls_attributes *ls_attr, struct bgp_ls_attr *attr,
++ enum bgp_ls_protocol_id protocol_id)
++{
++ if (!ls_attr || !attr)
++ return -1;
++
++ /* Administrative Group (TLV 1088) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_ADM_GRP)) {
++ attr->admin_group = ls_attr->standard.admin_group;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_ADMIN_GROUP_BIT);
++ }
++
++ /* Maximum Link Bandwidth (TLV 1089) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_MAX_BW)) {
++ attr->max_link_bw = ls_attr->standard.max_bw;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_MAX_LINK_BW_BIT);
++ }
++
++ /* Maximum Reservable Bandwidth (TLV 1090) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_MAX_RSV_BW)) {
++ attr->max_resv_bw = ls_attr->standard.max_rsv_bw;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_MAX_RESV_BW_BIT);
++ }
++
++ /* Unreserved Bandwidth (TLV 1091) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_UNRSV_BW)) {
++ for (int i = 0; i < BGP_LS_MAX_UNRESV_BW; i++)
++ attr->unreserved_bw[i] = ls_attr->standard.unrsv_bw[i];
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_UNRESV_BW_BIT);
++ }
++
++ /* TE Default Metric (TLV 1092) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_TE_METRIC)) {
++ attr->te_metric = ls_attr->standard.te_metric;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_TE_METRIC_BIT);
++ }
++
++ /* IGP Metric (TLV 1095) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_METRIC)) {
++ /* IGP metric can be 1, 2, or 3 bytes */
++ if (ls_attr->metric <= 0xFF)
++ attr->igp_metric_len = 1;
++ else if (ls_attr->metric <= 0xFFFF)
++ attr->igp_metric_len = 2;
++ else
++ attr->igp_metric_len = 3;
++ attr->igp_metric = ls_attr->metric;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_IGP_METRIC_BIT);
++ }
++
++ /* Shared Risk Link Group (TLV 1096) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_SRLG) && ls_attr->srlg_len > 0) {
++ uint8_t count = ls_attr->srlg_len;
++
++ if (count > BGP_LS_MAX_SRLG)
++ count = BGP_LS_MAX_SRLG;
++
++ attr->srlg_count = count;
++ attr->srlg_values = XCALLOC(MTYPE_BGP_LS_ATTR, count * sizeof(uint32_t));
++ for (uint8_t i = 0; i < count; i++)
++ attr->srlg_values[i] = ls_attr->srlgs[i];
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_SRLG_BIT);
++ }
++
++ /* Link Name (TLV 1098) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_NAME) && ls_attr->name[0] != '\0') {
++ size_t name_len = strlen(ls_attr->name);
++
++ attr->link_name = XCALLOC(MTYPE_BGP_LS_ATTR, name_len + 1);
++ memcpy(attr->link_name, ls_attr->name, name_len + 1);
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_LINK_NAME_BIT);
++ }
++
++ /* Adjacency SID (TLV 1099) */
++ populate_adj_sid(ls_attr, ADJ_PRI_IPV4, LS_ATTR_ADJ_SID, attr, protocol_id);
++ populate_adj_sid(ls_attr, ADJ_BCK_IPV4, LS_ATTR_BCK_ADJ_SID, attr, protocol_id);
++ populate_adj_sid(ls_attr, ADJ_PRI_IPV6, LS_ATTR_ADJ_SID6, attr, protocol_id);
++ populate_adj_sid(ls_attr, ADJ_BCK_IPV6, LS_ATTR_BCK_ADJ_SID6, attr, protocol_id);
++
++ /* Remote IPv4 Router-ID (TLV 1030) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_REMOTE_ADDR)) {
++ attr->ipv4_router_id_remote = ls_attr->standard.remote_addr;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_IPV4_ROUTER_ID_REMOTE_BIT);
++ }
++
++ /* Remote IPv6 Router-ID (TLV 1031) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_REMOTE_ADDR6)) {
++ attr->ipv6_router_id_remote = ls_attr->standard.remote_addr6;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_IPV6_ROUTER_ID_REMOTE_BIT);
++ }
++
++ /* Extended Admin Group (TLV 1093) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_EXT_ADM_GRP)) {
++ admin_group_copy(&attr->ext_admin_group, &ls_attr->ext_admin_group);
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_EXT_ADMIN_GROUP_BIT);
++ }
++
++ /* Unidirectional Link Delay (TLV 1114) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_DELAY)) {
++ attr->delay = ls_attr->extended.delay;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_DELAY_BIT);
++ }
++
++ /* Min/Max Unidirectional Link Delay (TLV 1115) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_MIN_MAX_DELAY)) {
++ attr->min_delay = ls_attr->extended.min_delay;
++ attr->max_delay = ls_attr->extended.max_delay;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_MIN_MAX_DELAY_BIT);
++ }
++
++ /* Unidirectional Delay Variation (TLV 1116) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_JITTER)) {
++ attr->jitter = ls_attr->extended.jitter;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_JITTER_BIT);
++ }
++
++ /* Unidirectional Packet Loss (TLV 1117) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_PACKET_LOSS)) {
++ attr->pkt_loss = ls_attr->extended.pkt_loss;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_PKT_LOSS_BIT);
++ }
++
++ /* Unidirectional Residual Bandwidth (TLV 1118) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_RSV_BW)) {
++ attr->residual_bw = ls_attr->extended.rsv_bw;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_RESIDUAL_BW_BIT);
++ }
++
++ /* Unidirectional Available Bandwidth (TLV 1119) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_AVA_BW)) {
++ attr->available_bw = ls_attr->extended.ava_bw;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_AVAILABLE_BW_BIT);
++ }
++
++ /* Unidirectional Utilized Bandwidth (TLV 1120) */
++ if (CHECK_FLAG(ls_attr->flags, LS_ATTR_USE_BW)) {
++ attr->utilized_bw = ls_attr->extended.used_bw;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_UTILIZED_BW_BIT);
++ }
++
++ return 0;
++}
++
++/*
++ * ===========================================================================
++ * Prefix Attribute Population
++ * ===========================================================================
++ */
++
++/*
++ * Populate BGP-LS Attributes from Link State Prefix
++ */
++int bgp_ls_populate_prefix_attr(struct ls_prefix *ls_prefix, struct bgp_ls_attr *attr)
++{
++ if (!ls_prefix || !attr)
++ return -1;
++
++ /* IGP Flags (TLV 1152) */
++ if (CHECK_FLAG(ls_prefix->flags, LS_PREF_IGP_FLAG)) {
++ attr->igp_flags = ls_prefix->igp_flag;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_IGP_FLAGS_BIT);
++ }
++
++ /* Route Tags (TLV 1153) - single tag */
++ if (CHECK_FLAG(ls_prefix->flags, LS_PREF_ROUTE_TAG)) {
++ attr->route_tag_count = 1;
++ attr->route_tags = XCALLOC(MTYPE_BGP_LS_ATTR, sizeof(uint32_t));
++ attr->route_tags[0] = ls_prefix->route_tag;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_ROUTE_TAG_BIT);
++ }
++
++ /* Extended Tags (TLV 1154) - single extended tag */
++ if (CHECK_FLAG(ls_prefix->flags, LS_PREF_EXTENDED_TAG)) {
++ attr->extended_tag_count = 1;
++ attr->extended_tags = XCALLOC(MTYPE_BGP_LS_ATTR, sizeof(uint64_t));
++ attr->extended_tags[0] = ls_prefix->extended_tag;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_EXTENDED_TAG_BIT);
++ }
++
++ /* Prefix Metric (TLV 1155) */
++ if (CHECK_FLAG(ls_prefix->flags, LS_PREF_METRIC)) {
++ attr->prefix_metric = ls_prefix->metric;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_PREFIX_METRIC_BIT);
++ }
++
++ /* Prefix-SID (TLV 1158) */
++ if (CHECK_FLAG(ls_prefix->flags, LS_PREF_SR)) {
++ if (bgp_ls_attr_prefix_sid_len(ls_prefix->sr.sid_flag) == -1) {
++ zlog_warn("BGP-LS: %s TED contains wrong combination of V-Flag and L-Flag for Prefix SID",
++ __func__);
++ } else {
++ attr->prefix_sid.sid = ls_prefix->sr.sid;
++ attr->prefix_sid.sid_flag = ls_prefix->sr.sid_flag;
++ attr->prefix_sid.algo = ls_prefix->sr.algo;
++ attr->present_tlvs |= (1ULL << BGP_LS_ATTR_PREFIX_SID_BIT);
++ }
++ }
++
++ return 0;
++}
++
++/*
++ * ===========================================================================
++ * IGP Origination Functions
++ * ===========================================================================
++ */
++
++static bool bgp_ls_link_valid(struct ls_edge *edge)
++{
++ if ((CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ID) &&
++ CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ID)))
++ return true;
++
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ADDR) &&
++ CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ADDR))
++ return true;
++
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ADDR6) &&
++ CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ADDR6) &&
++ !IN6_IS_ADDR_LINKLOCAL(&edge->attributes->standard.local6) &&
++ !IN6_IS_ADDR_LINKLOCAL(&edge->attributes->standard.remote6))
++ return true;
++
++ return false;
++}
++
++/*
++ * Originate Node NLRI from IGP router information
++ *
++ * This function creates a BGP-LS Node NLRI from IGP router data
++ * and installs it in the RIB for advertisement to BGP-LS peers.
++ */
++int bgp_ls_originate_node(struct bgp *bgp, uint8_t protocol_id, uint8_t *router_id,
++ uint16_t router_id_len, uint32_t area_id, struct ls_vertex *vertex)
++{
++ struct bgp_ls_nlri nlri;
++ struct bgp_ls_attr *ls_attr = NULL;
++ int ret;
++
++ if (!bgp || !router_id)
++ return -1;
++
++ if (!vertex || !vertex->node)
++ return -1;
++
++ /* Validate router ID length */
++ if (router_id_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ router_id_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Invalid router ID length %u", router_id_len);
++ return -1;
++ }
++
++ /* Clear NLRI structure */
++ memset(&nlri, 0, sizeof(nlri));
++
++ /* Build Node NLRI */
++ nlri.nlri_type = BGP_LS_NLRI_TYPE_NODE;
++ nlri.nlri_data.node.protocol_id = protocol_id;
++ nlri.nlri_data.node.identifier = 0; /* Instance ID, use 0 for default */
++
++ /* Set Local Node Descriptor */
++ nlri.nlri_data.node.local_node.igp_router_id_len = router_id_len;
++ memcpy(nlri.nlri_data.node.local_node.igp_router_id, router_id, router_id_len);
++ BGP_LS_TLV_SET(nlri.nlri_data.node.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++
++ /* Set AS Number if available */
++ if (CHECK_FLAG(vertex->node->flags, LS_NODE_AS_NUMBER)) {
++ nlri.nlri_data.node.local_node.asn = vertex->node->as_number;
++ BGP_LS_TLV_SET(nlri.nlri_data.node.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_AS_BIT);
++ }
++
++ /* Set OSPF Area ID if OSPF */
++ if (protocol_id == BGP_LS_PROTO_OSPFV2 || protocol_id == BGP_LS_PROTO_OSPFV3) {
++ nlri.nlri_data.node.local_node.ospf_area_id = area_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.node.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ }
++
++ /* Populate BGP-LS attributes from Link State vertex */
++ ls_attr = bgp_ls_attr_alloc();
++ if (bgp_ls_populate_node_attr(vertex->node, ls_attr) < 0) {
++ zlog_warn("BGP-LS: Failed to populate Node attributes");
++ bgp_ls_attr_free(ls_attr);
++ return -1;
++ }
++
++ /* Install in RIB */
++ ret = bgp_ls_update(bgp, &nlri, ls_attr);
++ if (ret < 0) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Failed to originate Node NLRI");
++ bgp_ls_attr_free(ls_attr);
++ return -1;
++ }
++
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("BGP-LS: Originated Node NLRI for protocol %u", protocol_id);
++
++ bgp_ls_attr_free(ls_attr);
++
++ return 0;
++}
++
++
++int bgp_ls_withdraw_node(struct bgp *bgp, uint8_t protocol_id, uint8_t *router_id,
++ uint16_t router_id_len, uint32_t area_id, struct ls_vertex *vertex)
++{
++ struct bgp_ls_nlri nlri;
++ int ret;
++
++ if (!bgp || !router_id)
++ return -1;
++
++ if (!vertex || !vertex->node)
++ return -1;
++
++ /* Validate router ID length */
++ if (router_id_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ router_id_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Invalid router ID length %u", router_id_len);
++ return -1;
++ }
++
++ /* Clear NLRI structure */
++ memset(&nlri, 0, sizeof(nlri));
++
++ /* Build Node NLRI */
++ nlri.nlri_type = BGP_LS_NLRI_TYPE_NODE;
++ nlri.nlri_data.node.protocol_id = protocol_id;
++ nlri.nlri_data.node.identifier = 0; /* Instance ID, use 0 for default */
++
++ /* Set Local Node Descriptor */
++ nlri.nlri_data.node.local_node.igp_router_id_len = router_id_len;
++ memcpy(nlri.nlri_data.node.local_node.igp_router_id, router_id, router_id_len);
++ BGP_LS_TLV_SET(nlri.nlri_data.node.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++
++ /* Set AS Number if available */
++ if (CHECK_FLAG(vertex->node->flags, LS_NODE_AS_NUMBER)) {
++ nlri.nlri_data.node.local_node.asn = vertex->node->as_number;
++ BGP_LS_TLV_SET(nlri.nlri_data.node.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_AS_BIT);
++ }
++
++ /* Set OSPF Area ID if OSPF */
++ if (protocol_id == BGP_LS_PROTO_OSPFV2 || protocol_id == BGP_LS_PROTO_OSPFV3) {
++ nlri.nlri_data.node.local_node.ospf_area_id = area_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.node.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ }
++
++ /* Withdraw from RIB */
++ ret = bgp_ls_withdraw(bgp, &nlri);
++ if (ret < 0) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Failed to withdraw Node NLRI");
++ return -1;
++ }
++
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("BGP-LS: Withdrawn Node NLRI for protocol %u", protocol_id);
++
++ return 0;
++}
++
++
++/*
++ * Originate Link NLRI from IGP adjacency information
++ *
++ * This function creates a BGP-LS Link NLRI from IGP adjacency data
++ * and installs it in the RIB for advertisement to BGP-LS peers.
++ */
++int bgp_ls_originate_link(struct bgp *bgp, uint8_t protocol_id, uint8_t *local_router_id,
++ uint16_t local_router_id_len, uint8_t *remote_router_id,
++ uint16_t remote_router_id_len, uint32_t area_id, struct ls_edge *edge)
++{
++ struct bgp_ls_nlri nlri;
++ struct bgp_ls_attr *ls_attr = NULL;
++ int ret;
++
++ if (!bgp || !local_router_id || !remote_router_id)
++ return -1;
++
++ if (!edge || !edge->attributes)
++ return -1;
++
++ /* Validate router ID lengths */
++ if (local_router_id_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ local_router_id_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE ||
++ remote_router_id_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ remote_router_id_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Invalid router ID lengths");
++ return -1;
++ }
++
++ /* Clear NLRI structure */
++ memset(&nlri, 0, sizeof(nlri));
++
++ /* Build Link NLRI */
++ nlri.nlri_type = BGP_LS_NLRI_TYPE_LINK;
++ nlri.nlri_data.link.protocol_id = protocol_id;
++ nlri.nlri_data.link.identifier = 0; /* Instance ID, use 0 for default */
++
++ /* Set Local Node Descriptor */
++ nlri.nlri_data.link.local_node.igp_router_id_len = local_router_id_len;
++ memcpy(nlri.nlri_data.link.local_node.igp_router_id, local_router_id, local_router_id_len);
++ BGP_LS_TLV_SET(nlri.nlri_data.link.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++
++ /* Set AS Number for Local Node if available */
++ if (edge->source && edge->source->node &&
++ CHECK_FLAG(edge->source->node->flags, LS_NODE_AS_NUMBER)) {
++ nlri.nlri_data.link.local_node.asn = edge->source->node->as_number;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_AS_BIT);
++ }
++
++ /* Set Remote Node Descriptor */
++ nlri.nlri_data.link.remote_node.igp_router_id_len = remote_router_id_len;
++ memcpy(nlri.nlri_data.link.remote_node.igp_router_id, remote_router_id,
++ remote_router_id_len);
++ BGP_LS_TLV_SET(nlri.nlri_data.link.remote_node.present_tlvs,
++ BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++
++ /* Set AS Number for Remote Node if available */
++ if (edge->destination && edge->destination->node &&
++ CHECK_FLAG(edge->destination->node->flags, LS_NODE_AS_NUMBER)) {
++ nlri.nlri_data.link.remote_node.asn = edge->destination->node->as_number;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.remote_node.present_tlvs,
++ BGP_LS_NODE_DESC_AS_BIT);
++ }
++
++ /* Set OSPF Area ID if OSPF */
++ if (protocol_id == BGP_LS_PROTO_OSPFV2 || protocol_id == BGP_LS_PROTO_OSPFV3) {
++ nlri.nlri_data.link.local_node.ospf_area_id = area_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ nlri.nlri_data.link.remote_node.ospf_area_id = area_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.remote_node.present_tlvs,
++ BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ }
++
++ /* Populate Link Descriptor from Link State edge attributes */
++ /* Link Local/Remote Identifiers (TLV 258) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ID)) {
++ nlri.nlri_data.link.link_desc.link_local_id = edge->attributes->standard.local_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_LINK_ID_BIT);
++ }
++
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ID)) {
++ nlri.nlri_data.link.link_desc.link_remote_id = edge->attributes->standard.remote_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_LINK_ID_BIT);
++ }
++
++ /* IPv4 Interface Address (TLV 259) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ADDR)) {
++ nlri.nlri_data.link.link_desc.ipv4_intf_addr = edge->attributes->standard.local;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_IPV4_INTF_BIT);
++ }
++
++ /* IPv4 Neighbor Address (TLV 260) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ADDR)) {
++ nlri.nlri_data.link.link_desc.ipv4_neigh_addr = edge->attributes->standard.remote;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_IPV4_NEIGH_BIT);
++ }
++
++ /* IPv6 Interface Address (TLV 261) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ADDR6)) {
++ nlri.nlri_data.link.link_desc.ipv6_intf_addr = edge->attributes->standard.local6;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_IPV6_INTF_BIT);
++ }
++
++ /* IPv6 Neighbor Address (TLV 262) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ADDR6)) {
++ nlri.nlri_data.link.link_desc.ipv6_neigh_addr = edge->attributes->standard.remote6;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_IPV6_NEIGH_BIT);
++ }
++
++ /* Remote AS Number (TLV 264) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_REMOTE_AS)) {
++ nlri.nlri_data.link.link_desc.remote_asn = edge->attributes->standard.remote_as;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_REMOTE_AS_BIT);
++ }
++
++ /* Populate BGP-LS attributes from Link State edge */
++ ls_attr = bgp_ls_attr_alloc();
++ if (bgp_ls_populate_link_attr(edge->attributes, ls_attr, protocol_id) < 0) {
++ zlog_warn("BGP-LS: Failed to populate Link attributes");
++ bgp_ls_attr_free(ls_attr);
++ return -1;
++ }
++
++ /* Install in RIB */
++ ret = bgp_ls_update(bgp, &nlri, ls_attr);
++ if (ret < 0) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Failed to originate Link NLRI");
++ bgp_ls_attr_free(ls_attr);
++ return -1;
++ }
++
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("BGP-LS: Originated Link NLRI for protocol %u", protocol_id);
++
++ bgp_ls_attr_free(ls_attr);
++
++ return 0;
++}
++
++
++int bgp_ls_withdraw_link(struct bgp *bgp, uint8_t protocol_id, uint8_t *local_router_id,
++ uint16_t local_router_id_len, uint8_t *remote_router_id,
++ uint16_t remote_router_id_len, uint32_t area_id, struct ls_edge *edge)
++{
++ struct bgp_ls_nlri nlri;
++ int ret;
++
++ if (!bgp || !local_router_id || !remote_router_id)
++ return -1;
++
++ if (!edge || !edge->attributes)
++ return -1;
++
++ /* Validate router ID lengths */
++ if (local_router_id_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ local_router_id_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE ||
++ remote_router_id_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ remote_router_id_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Invalid router ID lengths");
++ return -1;
++ }
++
++ /* Clear NLRI structure */
++ memset(&nlri, 0, sizeof(nlri));
++
++ /* Build Link NLRI */
++ nlri.nlri_type = BGP_LS_NLRI_TYPE_LINK;
++ nlri.nlri_data.link.protocol_id = protocol_id;
++ nlri.nlri_data.link.identifier = 0; /* Instance ID, use 0 for default */
++
++ /* Set Local Node Descriptor */
++ nlri.nlri_data.link.local_node.igp_router_id_len = local_router_id_len;
++ memcpy(nlri.nlri_data.link.local_node.igp_router_id, local_router_id, local_router_id_len);
++ BGP_LS_TLV_SET(nlri.nlri_data.link.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++
++ /* Set AS Number for Local Node if available */
++ if (edge->source && edge->source->node &&
++ CHECK_FLAG(edge->source->node->flags, LS_NODE_AS_NUMBER)) {
++ nlri.nlri_data.link.local_node.asn = edge->source->node->as_number;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_AS_BIT);
++ }
++
++ /* Set Remote Node Descriptor */
++ nlri.nlri_data.link.remote_node.igp_router_id_len = remote_router_id_len;
++ memcpy(nlri.nlri_data.link.remote_node.igp_router_id, remote_router_id,
++ remote_router_id_len);
++ BGP_LS_TLV_SET(nlri.nlri_data.link.remote_node.present_tlvs,
++ BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++
++ /* Set AS Number for Remote Node if available */
++ if (edge->destination && edge->destination->node &&
++ CHECK_FLAG(edge->destination->node->flags, LS_NODE_AS_NUMBER)) {
++ nlri.nlri_data.link.remote_node.asn = edge->destination->node->as_number;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.remote_node.present_tlvs,
++ BGP_LS_NODE_DESC_AS_BIT);
++ }
++
++ /* Set OSPF Area ID if OSPF */
++ if (protocol_id == BGP_LS_PROTO_OSPFV2 || protocol_id == BGP_LS_PROTO_OSPFV3) {
++ nlri.nlri_data.link.local_node.ospf_area_id = area_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ nlri.nlri_data.link.remote_node.ospf_area_id = area_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.remote_node.present_tlvs,
++ BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ }
++
++ /* Populate Link Descriptor from Link State edge attributes */
++ /* Link Local/Remote Identifiers (TLV 258) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ID)) {
++ nlri.nlri_data.link.link_desc.link_local_id = edge->attributes->standard.local_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_LINK_ID_BIT);
++ }
++
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ID)) {
++ nlri.nlri_data.link.link_desc.link_remote_id = edge->attributes->standard.remote_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_LINK_ID_BIT);
++ }
++
++ /* IPv4 Interface Address (TLV 259) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ADDR)) {
++ nlri.nlri_data.link.link_desc.ipv4_intf_addr = edge->attributes->standard.local;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_IPV4_INTF_BIT);
++ }
++
++ /* IPv4 Neighbor Address (TLV 260) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ADDR)) {
++ nlri.nlri_data.link.link_desc.ipv4_neigh_addr = edge->attributes->standard.remote;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_IPV4_NEIGH_BIT);
++ }
++
++ /* IPv6 Interface Address (TLV 261) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_LOCAL_ADDR6)) {
++ nlri.nlri_data.link.link_desc.ipv6_intf_addr = edge->attributes->standard.local6;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_IPV6_INTF_BIT);
++ }
++
++ /* IPv6 Neighbor Address (TLV 262) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_NEIGH_ADDR6)) {
++ nlri.nlri_data.link.link_desc.ipv6_neigh_addr = edge->attributes->standard.remote6;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_IPV6_NEIGH_BIT);
++ }
++
++ /* Remote AS Number (TLV 264) */
++ if (CHECK_FLAG(edge->attributes->flags, LS_ATTR_REMOTE_AS)) {
++ nlri.nlri_data.link.link_desc.remote_asn = edge->attributes->standard.remote_as;
++ BGP_LS_TLV_SET(nlri.nlri_data.link.link_desc.present_tlvs,
++ BGP_LS_LINK_DESC_REMOTE_AS_BIT);
++ }
++
++ /* Withdraw from RIB */
++ ret = bgp_ls_withdraw(bgp, &nlri);
++ if (ret < 0) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Failed to withdraw Link NLRI");
++ return -1;
++ }
++
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("BGP-LS: Withdrawn Link NLRI for protocol %u", protocol_id);
++
++ return 0;
++}
++
++
++/*
++ * Originate Prefix NLRI from IGP prefix information
++ *
++ * This function creates a BGP-LS Prefix NLRI from IGP prefix data
++ * and installs it in the RIB for advertisement to BGP-LS peers.
++ */
++int bgp_ls_originate_prefix(struct bgp *bgp, uint8_t protocol_id, uint8_t *router_id,
++ uint16_t router_id_len, struct prefix *prefix, uint32_t area_id,
++ struct ls_subnet *subnet)
++{
++ struct bgp_ls_nlri nlri;
++ struct bgp_ls_attr *ls_attr = NULL;
++ int ret;
++
++ if (!bgp || !router_id || !prefix)
++ return -1;
++
++ if (!subnet || !subnet->ls_pref)
++ return -1;
++
++ /* Validate router ID length */
++ if (router_id_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ router_id_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Invalid router ID length %u", router_id_len);
++ return -1;
++ }
++
++ /* Clear NLRI structure */
++ memset(&nlri, 0, sizeof(nlri));
++
++ /* Determine NLRI type based on prefix family */
++ if (prefix->family == AF_INET)
++ nlri.nlri_type = BGP_LS_NLRI_TYPE_IPV4_PREFIX;
++ else if (prefix->family == AF_INET6)
++ nlri.nlri_type = BGP_LS_NLRI_TYPE_IPV6_PREFIX;
++ else {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Unsupported prefix family %u", prefix->family);
++ return -1;
++ }
++
++ /* Build Prefix NLRI */
++ nlri.nlri_data.prefix.protocol_id = protocol_id;
++ nlri.nlri_data.prefix.identifier = 0; /* Instance ID, use 0 for default */
++
++ /* Set Local Node Descriptor */
++ nlri.nlri_data.prefix.local_node.igp_router_id_len = router_id_len;
++ memcpy(nlri.nlri_data.prefix.local_node.igp_router_id, router_id, router_id_len);
++ BGP_LS_TLV_SET(nlri.nlri_data.prefix.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++
++ /* Set AS Number if available */
++ if (subnet->vertex && subnet->vertex->node &&
++ CHECK_FLAG(subnet->vertex->node->flags, LS_NODE_AS_NUMBER)) {
++ nlri.nlri_data.prefix.local_node.asn = subnet->vertex->node->as_number;
++ BGP_LS_TLV_SET(nlri.nlri_data.prefix.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_AS_BIT);
++ }
++
++ /* Set OSPF Area ID if OSPF */
++ if (protocol_id == BGP_LS_PROTO_OSPFV2 || protocol_id == BGP_LS_PROTO_OSPFV3) {
++ nlri.nlri_data.prefix.local_node.ospf_area_id = area_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.prefix.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ }
++
++ /* Set Prefix Descriptor */
++ nlri.nlri_data.prefix.prefix_desc.prefix = *prefix;
++ apply_mask(&nlri.nlri_data.prefix.prefix_desc.prefix);
++ BGP_LS_TLV_SET(nlri.nlri_data.prefix.prefix_desc.present_tlvs,
++ BGP_LS_PREFIX_DESC_IP_REACH_BIT);
++
++ /* Populate BGP-LS attributes from Link State subnet */
++ ls_attr = bgp_ls_attr_alloc();
++ if (bgp_ls_populate_prefix_attr(subnet->ls_pref, ls_attr) < 0) {
++ zlog_warn("BGP-LS: Failed to populate Prefix attributes");
++ bgp_ls_attr_free(ls_attr);
++ return -1;
++ }
++
++ /* Install in RIB */
++ ret = bgp_ls_update(bgp, &nlri, ls_attr);
++ if (ret < 0) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Failed to originate Prefix NLRI");
++ bgp_ls_attr_free(ls_attr);
++ return -1;
++ }
++
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("BGP-LS: Originated Prefix NLRI %pFX for protocol %u", prefix,
++ protocol_id);
++
++ bgp_ls_attr_free(ls_attr);
++
++ return 0;
++}
++
++
++int bgp_ls_withdraw_prefix(struct bgp *bgp, uint8_t protocol_id, uint8_t *router_id,
++ uint16_t router_id_len, struct prefix *prefix, uint32_t area_id,
++ struct ls_subnet *subnet)
++{
++ struct bgp_ls_nlri nlri;
++ int ret;
++
++ if (!bgp || !router_id || !prefix)
++ return -1;
++
++ if (!subnet || !subnet->ls_pref)
++ return -1;
++
++ /* Validate router ID length */
++ if (router_id_len < BGP_LS_IGP_ROUTER_ID_MIN_SIZE ||
++ router_id_len > BGP_LS_IGP_ROUTER_ID_MAX_SIZE) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Invalid router ID length %u", router_id_len);
++ return -1;
++ }
++
++ /* Clear NLRI structure */
++ memset(&nlri, 0, sizeof(nlri));
++
++ /* Determine NLRI type based on prefix family */
++ if (prefix->family == AF_INET)
++ nlri.nlri_type = BGP_LS_NLRI_TYPE_IPV4_PREFIX;
++ else if (prefix->family == AF_INET6)
++ nlri.nlri_type = BGP_LS_NLRI_TYPE_IPV6_PREFIX;
++ else {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Unsupported prefix family %u", prefix->family);
++ return -1;
++ }
++
++ /* Build Prefix NLRI */
++ nlri.nlri_data.prefix.protocol_id = protocol_id;
++ nlri.nlri_data.prefix.identifier = 0; /* Instance ID, use 0 for default */
++
++ /* Set Local Node Descriptor */
++ nlri.nlri_data.prefix.local_node.igp_router_id_len = router_id_len;
++ memcpy(nlri.nlri_data.prefix.local_node.igp_router_id, router_id, router_id_len);
++ BGP_LS_TLV_SET(nlri.nlri_data.prefix.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_IGP_ROUTER_BIT);
++
++ /* Set AS Number if available */
++ if (subnet->vertex && subnet->vertex->node &&
++ CHECK_FLAG(subnet->vertex->node->flags, LS_NODE_AS_NUMBER)) {
++ nlri.nlri_data.prefix.local_node.asn = subnet->vertex->node->as_number;
++ BGP_LS_TLV_SET(nlri.nlri_data.prefix.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_AS_BIT);
++ }
++
++ /* Set OSPF Area ID if OSPF */
++ if (protocol_id == BGP_LS_PROTO_OSPFV2 || protocol_id == BGP_LS_PROTO_OSPFV3) {
++ nlri.nlri_data.prefix.local_node.ospf_area_id = area_id;
++ BGP_LS_TLV_SET(nlri.nlri_data.prefix.local_node.present_tlvs,
++ BGP_LS_NODE_DESC_OSPF_AREA_BIT);
++ }
++
++ /* Set Prefix Descriptor */
++ nlri.nlri_data.prefix.prefix_desc.prefix = *prefix;
++ BGP_LS_TLV_SET(nlri.nlri_data.prefix.prefix_desc.present_tlvs,
++ BGP_LS_PREFIX_DESC_IP_REACH_BIT);
++
++ /* Withdraw from RIB */
++ ret = bgp_ls_withdraw(bgp, &nlri);
++ if (ret < 0) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Failed to withdraw Prefix NLRI");
++ return -1;
++ }
++
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("BGP-LS: Withdrawn Prefix NLRI %pFX for protocol %u", prefix,
++ protocol_id);
++
++ return 0;
++}
++
++/*
++ * ===========================================================================
++ * Link State Message Processing
++ * ===========================================================================
++ */
++
++/*
++ * Map Link State origin to BGP-LS Protocol-ID
++ */
++static enum bgp_ls_protocol_id ls_origin_to_protocol_id(enum ls_origin origin)
++{
++ switch (origin) {
++ case ISIS_L1:
++ return BGP_LS_PROTO_ISIS_L1;
++ case ISIS_L2:
++ return BGP_LS_PROTO_ISIS_L2;
++ case OSPFv2:
++ return BGP_LS_PROTO_OSPFV2;
++ case DIRECT:
++ return BGP_LS_PROTO_DIRECT;
++ case STATIC:
++ return BGP_LS_PROTO_STATIC;
++ case LS_UNKNOWN:
++ default:
++ return BGP_LS_PROTO_RESERVED;
++ }
++}
++
++/*
++ * Process Link State vertex and originate/withdraw BGP-LS Node NLRI
++ */
++int bgp_ls_process_vertex(struct bgp *bgp, struct ls_vertex *vertex, uint8_t event)
++{
++ enum bgp_ls_protocol_id protocol_id;
++ uint8_t router_id[BGP_LS_IGP_ROUTER_ID_MAX_SIZE];
++ uint16_t router_id_len = 0;
++ uint32_t area_id = 0;
++
++ if (!bgp || !vertex || !vertex->node)
++ return -1;
++
++ protocol_id = ls_origin_to_protocol_id(vertex->node->adv.origin);
++
++ switch (protocol_id) {
++ case BGP_LS_PROTO_OSPFV2:
++ /* OSPF uses 4-byte IPv4 router ID */
++ memcpy(router_id, &vertex->node->adv.id.ip.addr.s_addr, 4);
++ router_id_len = 4;
++ area_id = ntohl(vertex->node->adv.id.ip.area_id.s_addr);
++ break;
++ case BGP_LS_PROTO_ISIS_L1:
++ case BGP_LS_PROTO_ISIS_L2:
++ /* IS-IS uses 6-byte System ID + 1 byte pseudonode ID = 7 bytes */
++ memcpy(router_id, vertex->node->adv.id.iso.sys_id, ISO_SYS_ID_LEN);
++ // router_id[6] = 0; /* Pseudonode ID = 0 for router */
++ // router_id_len = 7;
++ router_id_len = 6;
++ break;
++ case BGP_LS_PROTO_DIRECT:
++ case BGP_LS_PROTO_STATIC:
++ case BGP_LS_PROTO_OSPFV3:
++ case BGP_LS_PROTO_BGP:
++ case BGP_LS_PROTO_RESERVED:
++ zlog_err("BGP-LS: Unsupported protocol %u", protocol_id);
++ return -1;
++ }
++
++ switch (event) {
++ case LS_MSG_EVENT_SYNC:
++ case LS_MSG_EVENT_ADD:
++ case LS_MSG_EVENT_UPDATE:
++ return bgp_ls_originate_node(bgp, protocol_id, router_id, router_id_len, area_id,
++ vertex);
++
++ case LS_MSG_EVENT_DELETE:
++ return bgp_ls_withdraw_node(bgp, protocol_id, router_id, router_id_len, area_id,
++ vertex);
++
++ default:
++ zlog_warn("BGP-LS: Unknown event type %u for vertex", event);
++ return -1;
++ }
++}
++
++/*
++ * Process Link State edge and originate/withdraw BGP-LS Link NLRI
++ */
++int bgp_ls_process_edge(struct bgp *bgp, struct ls_edge *edge, uint8_t event)
++{
++ uint8_t protocol_id;
++ uint8_t local_router_id[BGP_LS_IGP_ROUTER_ID_MAX_SIZE];
++ uint8_t remote_router_id[BGP_LS_IGP_ROUTER_ID_MAX_SIZE];
++ uint16_t local_router_id_len = 0;
++ uint16_t remote_router_id_len = 0;
++ uint32_t area_id = 0;
++
++ if (!bgp || !edge || !edge->attributes)
++ return -1;
++
++ /* Map origin to protocol ID */
++ protocol_id = ls_origin_to_protocol_id(edge->attributes->adv.origin);
++
++ /* Extract router IDs from source and destination vertices */
++ if (!edge->source || !edge->destination) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Edge missing source or destination vertex");
++ return -1;
++ }
++
++ if (!bgp_ls_link_valid(edge))
++ return -1;
++
++ switch (protocol_id) {
++ case BGP_LS_PROTO_OSPFV2:
++ /* OSPF uses 4-byte IPv4 router ID */
++ memcpy(local_router_id, &edge->source->node->adv.id.ip.addr.s_addr, 4);
++ local_router_id_len = 4;
++
++ memcpy(remote_router_id, &edge->destination->node->adv.id.ip.addr.s_addr, 4);
++ remote_router_id_len = 4;
++
++ area_id = ntohl(edge->attributes->adv.id.ip.area_id.s_addr);
++ break;
++ case BGP_LS_PROTO_ISIS_L1:
++ case BGP_LS_PROTO_ISIS_L2:
++ /* IS-IS non-pseudonode uses 6-byte System ID */
++ memcpy(local_router_id, edge->source->node->adv.id.iso.sys_id, ISO_SYS_ID_LEN);
++ local_router_id_len = 6;
++
++ memcpy(remote_router_id, edge->destination->node->adv.id.iso.sys_id,
++ ISO_SYS_ID_LEN);
++ remote_router_id_len = 6;
++ break;
++ case BGP_LS_PROTO_DIRECT:
++ case BGP_LS_PROTO_STATIC:
++ case BGP_LS_PROTO_OSPFV3:
++ case BGP_LS_PROTO_BGP:
++ case BGP_LS_PROTO_RESERVED:
++ zlog_err("BGP-LS: Unsupported protocol %u", protocol_id);
++ return -1;
++ }
++
++ switch (event) {
++ case LS_MSG_EVENT_SYNC:
++ case LS_MSG_EVENT_ADD:
++ case LS_MSG_EVENT_UPDATE:
++ return bgp_ls_originate_link(bgp, protocol_id, local_router_id,
++ local_router_id_len, remote_router_id,
++ remote_router_id_len, area_id, edge);
++
++ case LS_MSG_EVENT_DELETE:
++ return bgp_ls_withdraw_link(bgp, protocol_id, local_router_id, local_router_id_len,
++ remote_router_id, remote_router_id_len, area_id, edge);
++
++ default:
++ zlog_warn("BGP-LS: Unknown event type %u for edge", event);
++ return -1;
++ }
++}
++
++/*
++ * Process Link State subnet and originate/withdraw BGP-LS Prefix NLRI
++ */
++int bgp_ls_process_subnet(struct bgp *bgp, struct ls_subnet *subnet, uint8_t event)
++{
++ uint8_t protocol_id;
++ uint8_t router_id[BGP_LS_IGP_ROUTER_ID_MAX_SIZE];
++ uint16_t router_id_len = 0;
++ uint32_t area_id = 0;
++
++ if (!bgp || !subnet || !subnet->ls_pref)
++ return -1;
++
++ protocol_id = ls_origin_to_protocol_id(subnet->ls_pref->adv.origin);
++
++ /* Extract router ID from advertising vertex */
++ if (!subnet->vertex) {
++ flog_err(EC_BGP_LS_PACKET, "BGP-LS: Subnet missing advertising vertex");
++ return -1;
++ }
++
++ switch (protocol_id) {
++ case BGP_LS_PROTO_OSPFV2:
++ /* OSPF uses 4-byte IPv4 router ID */
++ memcpy(router_id, &subnet->vertex->node->adv.id.ip.addr.s_addr, 4);
++ router_id_len = 4;
++ area_id = ntohl(subnet->ls_pref->adv.id.ip.area_id.s_addr);
++ break;
++ case BGP_LS_PROTO_ISIS_L1:
++ case BGP_LS_PROTO_ISIS_L2:
++ /* IS-IS uses 6-byte System ID + 1 byte pseudonode ID = 7 bytes */
++ memcpy(router_id, subnet->vertex->node->adv.id.iso.sys_id, ISO_SYS_ID_LEN);
++ // router_id[6] = 0; /* Pseudonode ID = 0 for router */
++ // router_id_len = 7;
++ router_id_len = 6;
++ break;
++ case BGP_LS_PROTO_DIRECT:
++ case BGP_LS_PROTO_STATIC:
++ case BGP_LS_PROTO_OSPFV3:
++ case BGP_LS_PROTO_BGP:
++ case BGP_LS_PROTO_RESERVED:
++ zlog_err("BGP-LS: Unsupported protocol %u", protocol_id);
++ return -1;
++ }
++
++ switch (event) {
++ case LS_MSG_EVENT_SYNC:
++ case LS_MSG_EVENT_ADD:
++ case LS_MSG_EVENT_UPDATE:
++ return bgp_ls_originate_prefix(bgp, protocol_id, router_id, router_id_len,
++ &subnet->key, area_id, subnet);
++
++ case LS_MSG_EVENT_DELETE:
++ return bgp_ls_withdraw_prefix(bgp, protocol_id, router_id, router_id_len,
++ &subnet->key, area_id, subnet);
++
++ default:
++ zlog_warn("BGP-LS: Unknown event type %u for subnet", event);
++ return -1;
++ }
++}
++
++/*
++ * ===========================================================================
++ * Link-State Message Processing from Zebra
++ * ===========================================================================
++ */
++
++/*
++ * Process link-state message and update TED
++ *
++ * Converts message to TED structure (vertex/edge/subnet) and triggers
++ * BGP-LS route origination.
++ */
++int bgp_ls_process_message(struct bgp *bgp, struct ls_message *msg)
++{
++ struct ls_vertex *vertex;
++ struct ls_edge *edge;
++ struct ls_edge *reverse_edge = NULL;
++ bool reverse_edge_dst_updated = false;
++ struct ls_subnet *subnet;
++
++ if (!bgp || !bgp->ls_info || !bgp->ls_info->ted || !msg)
++ return -1;
++
++ if (BGP_DEBUG(zebra, ZEBRA) || BGP_DEBUG(linkstate, LINKSTATE)) {
++ const char *type_str = msg->type == LS_MSG_TYPE_NODE ? "NODE"
++ : msg->type == LS_MSG_TYPE_ATTRIBUTES ? "ATTR"
++ : "PREFIX";
++ const char *event_str = msg->event == LS_MSG_EVENT_ADD ? "ADD"
++ : msg->event == LS_MSG_EVENT_UPDATE ? "UPDATE"
++ : "DELETE";
++ zlog_debug("%s: Processing %s %s", __func__, type_str, event_str);
++ }
++
++ switch (msg->type) {
++ case LS_MSG_TYPE_NODE:
++ vertex = ls_msg2vertex(bgp->ls_info->ted, msg, false);
++ if (!vertex) {
++ zlog_err("%s: Failed to convert message to vertex", __func__);
++ return -1;
++ }
++
++ if (BGP_DEBUG(zebra, ZEBRA) || BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s: Node vertex key=%" PRIu64, __func__, vertex->key);
++
++ bgp_ls_process_vertex(bgp, vertex, msg->event);
++
++ if (msg->event == LS_MSG_EVENT_DELETE)
++ ls_vertex_del_all(bgp->ls_info->ted, vertex);
++
++ break;
++
++ case LS_MSG_TYPE_ATTRIBUTES:
++ edge = ls_msg2edge(bgp->ls_info->ted, msg, false);
++ if (!edge) {
++ zlog_err("%s: Failed to convert message to edge", __func__);
++ return -1;
++ }
++
++ if (BGP_DEBUG(zebra, ZEBRA) || BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s: Link edge", __func__);
++
++ if (msg->event == LS_MSG_EVENT_SYNC || msg->event == LS_MSG_EVENT_ADD ||
++ msg->event == LS_MSG_EVENT_UPDATE) {
++ /* Search for the reverse edge and link both directions. */
++ reverse_edge = ls_find_edge_by_destination(bgp->ls_info->ted,
++ edge->attributes);
++ if (reverse_edge) {
++ /* Attach destination to reverse edge if missing. */
++ if (reverse_edge->destination == NULL && edge->source) {
++ vertex = edge->source;
++ listnode_add_sort_nodup(vertex->incoming_edges,
++ reverse_edge);
++ reverse_edge->destination = vertex;
++ reverse_edge_dst_updated = true;
++ }
++ /* Attach destination to this edge if missing. */
++ if (edge->destination == NULL && reverse_edge->source) {
++ vertex = reverse_edge->source;
++ listnode_add_sort_nodup(vertex->incoming_edges, edge);
++ edge->destination = vertex;
++ }
++ }
++
++ if (!edge->destination) {
++ /*
++ * An ADD for edge A->B may arrive before the reverse edge B->A
++ * exists in TED. In that case edge->destination is NULL, so we
++ * cannot originate the link yet and skip it for now. When B->A is
++ * later added, its ADD/UPDATE processing will also look up and
++ * process A->B.
++ */
++ if (BGP_DEBUG(zebra, ZEBRA) || BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s: Skip edge add/update without destination",
++ __func__);
++ break;
++ }
++
++ bgp_ls_process_edge(bgp, edge, msg->event);
++
++ /*
++ * After we process edge A->B, check whether reverse edge B->A is
++ * already in TED and process it. This originates the Link NLRI for
++ * the direction that was previously skipped when A->B lacked a
++ * destination.
++ */
++ if (reverse_edge &&
++ (msg->event == LS_MSG_EVENT_SYNC || msg->event == LS_MSG_EVENT_ADD ||
++ reverse_edge_dst_updated)) {
++ uint8_t reverse_event = msg->event;
++
++ if (msg->event == LS_MSG_EVENT_UPDATE && reverse_edge_dst_updated)
++ reverse_event = LS_MSG_EVENT_ADD;
++
++ bgp_ls_process_edge(bgp, reverse_edge, reverse_event);
++ } else if (!reverse_edge &&
++ (BGP_DEBUG(zebra, ZEBRA) || BGP_DEBUG(linkstate, LINKSTATE)))
++ zlog_debug("%s: Reverse edge not yet in TED, will be processed on arrival",
++ __func__);
++
++ } else if (msg->event == LS_MSG_EVENT_DELETE) {
++ bgp_ls_process_edge(bgp, edge, msg->event);
++ ls_edge_del_all(bgp->ls_info->ted, edge);
++ } else {
++ if (BGP_DEBUG(zebra, ZEBRA) || BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s: Unknown event type %u for edge", __func__,
++ msg->event);
++ }
++
++ break;
++
++ case LS_MSG_TYPE_PREFIX:
++ subnet = ls_msg2subnet(bgp->ls_info->ted, msg, false);
++ if (!subnet) {
++ zlog_err("%s: Failed to convert message to subnet", __func__);
++ return -1;
++ }
++
++ if (BGP_DEBUG(zebra, ZEBRA) || BGP_DEBUG(linkstate, LINKSTATE)) {
++ char buf[PREFIX2STR_BUFFER];
++
++ prefix2str(&subnet->key, buf, sizeof(buf));
++ zlog_debug("%s: Prefix %s", __func__, buf);
++ }
++
++ bgp_ls_process_subnet(bgp, subnet, msg->event);
++
++ if (msg->event == LS_MSG_EVENT_DELETE)
++ ls_subnet_del_all(bgp->ls_info->ted, subnet);
++
++ break;
++
++ default:
++ zlog_warn("%s: Unknown message type %d", __func__, msg->type);
++ return -1;
++ }
++
++ return 0;
++}
++
++/*
++ * Remove all entries from the TED.
++ */
++static void bgp_ls_ted_clear(struct ls_ted *ted)
++{
++ struct ls_vertex *vertex;
++ struct ls_edge *edge;
++ struct ls_subnet *subnet;
++
++ frr_each_safe (vertices, &ted->vertices, vertex)
++ ls_vertex_del_all(ted, vertex);
++ frr_each_safe (edges, &ted->edges, edge)
++ ls_edge_del_all(ted, edge);
++ frr_each_safe (subnets, &ted->subnets, subnet)
++ ls_subnet_del_all(ted, subnet);
++}
++
++/*
++ * Withdraw all locally originated BGP-LS routes and reset the TED.
++ *
++ * Called when the last BGP-LS peer is deactivated: performs a single bulk
++ * walk of the BGP-LS RIB via bgp_clear_route() to remove all self-originated
++ * paths at once, then clears all TED entries.
++ *
++ * @param bgp - BGP instance
++ */
++void bgp_ls_withdraw_ted(struct bgp *bgp)
++{
++ if (!bgp || !bgp->ls_info || !bgp->ls_info->ted)
++ return;
++
++ if (BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("BGP-LS: Withdrawing all locally originated routes and resetting TED");
++
++ /* Remove all self-originated BGP-LS paths from the RIB */
++ bgp_clear_route(bgp->peer_self, AFI_BGP_LS, SAFI_BGP_LS);
++
++ /* Clear all TED entries */
++ bgp_ls_ted_clear(bgp->ls_info->ted);
++
++ zlog_info("BGP-LS: All locally originated BGP-LS routes withdrawn and TED reset");
++}
++
++/*
++ * Handle link-state SYNC/UPDATE messages from zebra
++ */
++int bgp_ls_process_linkstate_message(struct stream *s, uint8_t msg_type)
++{
++ struct ls_message *msg;
++ struct bgp *bgp;
++ int ret;
++
++ bgp = bgp_get_default();
++ if (!bgp || !bgp->ls_info || !bgp->ls_info->ted) {
++ zlog_warn("%s: TED not initialized, ignoring link-state message", __func__);
++ return 0;
++ }
++
++ msg = ls_parse_msg(s);
++ if (!msg) {
++ zlog_err("%s: Failed to parse link-state message", __func__);
++ return -1;
++ }
++
++ if (BGP_DEBUG(zebra, ZEBRA) || BGP_DEBUG(linkstate, LINKSTATE))
++ zlog_debug("%s: Received link-state %s message", __func__,
++ msg_type == LINK_STATE_UPDATE ? "UPDATE" : "SYNC");
++
++ ret = bgp_ls_process_message(bgp, msg);
++ ls_delete_msg(msg);
++
++ return ret;
++}
+diff --git a/bgpd/bgp_ls_ted.h b/bgpd/bgp_ls_ted.h
+new file mode 100644
+index 0000000000..9404523c13
+--- /dev/null
++++ b/bgpd/bgp_ls_ted.h
+@@ -0,0 +1,226 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * BGP Link-State Traffic Engineering Database (RFC 9552)
++ * Copyright (C) 2025 Carmine Scarpitta
++ *
++ * This file implements the BGP-LS TED management functions for storing
++ * topology information learned from BGP-LS routes. The TED provides a
++ * graph-based view of the network for applications (PCE, SDN controllers).
++ */
++
++#ifndef _FRR_BGP_LS_TED_H
++#define _FRR_BGP_LS_TED_H
++
++#include "bgpd/bgp_ls_nlri.h"
++
++#define UNKNOWN LS_UNKNOWN
++#include "lib/link_state.h"
++#undef UNKNOWN
++
++/* Forward declarations */
++struct bgp;
++struct bgp_ls_nlri;
++
++/*
++ * Populate BGP-LS Attributes from Link State Node
++ *
++ * @param ls_node Link State node from TED
++ * @param attr BGP-LS node attribute structure to populate
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_populate_node_attr(struct ls_node *ls_node, struct bgp_ls_attr *attr);
++
++/*
++ * Populate BGP-LS Attributes from Link State Attributes
++ *
++ * @param ls_attr Link State attributes from TED
++ * @param attr BGP-LS link attribute structure to populate
++ * @param protocol_id - IGP protocol (ISIS/OSPF)
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_populate_link_attr(struct ls_attributes *ls_attr, struct bgp_ls_attr *attr,
++ enum bgp_ls_protocol_id protocol_id);
++
++/*
++ * Populate BGP-LS Attributes from Link State Prefix
++ *
++ * @param ls_prefix Link State prefix from TED
++ * @param attr BGP-LS prefix attribute structure to populate
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_populate_prefix_attr(struct ls_prefix *ls_prefix, struct bgp_ls_attr *attr);
++
++/*
++ * ===========================================================================
++ * IGP Origination Functions
++ * ===========================================================================
++ */
++
++/*
++ * Originate Node NLRI from IGP router information
++ *
++ * Creates a BGP-LS Node NLRI from IGP router advertisements
++ * (ISIS LSP or OSPF Router-LSA) and installs it in the RIB.
++ *
++ * @param bgp - BGP instance
++ * @param protocol_id - IGP protocol (ISIS/OSPF)
++ * @param router_id - IGP router ID
++ * @param router_id_len - Length of router ID
++ * @param area_id - IGP area/level identifier
++ * @param vertex - Link State vertex (for attribute population)
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_originate_node(struct bgp *bgp, uint8_t protocol_id, uint8_t *router_id,
++ uint16_t router_id_len, uint32_t area_id,
++ struct ls_vertex *vertex);
++
++/*
++ * Originate Link NLRI from IGP adjacency information
++ *
++ * Creates a BGP-LS Link NLRI from IGP adjacency advertisements
++ * (ISIS IS Reachability or OSPF Link-LSA) and installs it in the RIB.
++ *
++ * @param bgp - BGP instance
++ * @param protocol_id - IGP protocol (ISIS/OSPF)
++ * @param local_router_id - Local router IGP ID
++ * @param local_router_id_len - Length of local router ID
++ * @param remote_router_id - Remote router IGP ID
++ * @param remote_router_id_len - Length of remote router ID
++ * @param area_id - IGP area/level identifier
++ * @param edge - Link State edge (for attribute population)
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_originate_link(struct bgp *bgp, uint8_t protocol_id, uint8_t *local_router_id,
++ uint16_t local_router_id_len, uint8_t *remote_router_id,
++ uint16_t remote_router_id_len, uint32_t area_id,
++ struct ls_edge *edge);
++
++/*
++ * Originate Prefix NLRI from IGP prefix information
++ *
++ * Creates a BGP-LS Prefix NLRI from IGP prefix advertisements
++ * (ISIS IP Reachability or OSPF Summary-LSA) and installs it in the RIB.
++ *
++ * @param bgp - BGP instance
++ * @param protocol_id - IGP protocol (ISIS/OSPF)
++ * @param router_id - Advertising router IGP ID
++ * @param router_id_len - Length of router ID
++ * @param prefix - IP prefix
++ * @param area_id - IGP area/level identifier
++ * @param subnet - Link State subnet (for attribute population)
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_originate_prefix(struct bgp *bgp, uint8_t protocol_id, uint8_t *router_id,
++ uint16_t router_id_len, struct prefix *prefix, uint32_t area_id,
++ struct ls_subnet *subnet);
++
++/*
++ * ===========================================================================
++ * Link State Message Processing
++ * ===========================================================================
++ */
++
++/*
++ * Process Link State vertex (node) and originate BGP-LS Node NLRI
++ *
++ * Extracts IGP router information from ls_vertex and calls
++ * bgp_ls_originate_node() to create and install BGP-LS route.
++ *
++ * @param bgp - BGP instance
++ * @param vertex - Link State vertex from ls_msg2vertex()
++ * @param event - LS_MSG_EVENT_ADD, UPDATE, or DELETE
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_process_vertex(struct bgp *bgp, struct ls_vertex *vertex, uint8_t event);
++
++/*
++ * Process Link State edge (link) and originate BGP-LS Link NLRI
++ *
++ * Extracts IGP adjacency information from ls_edge and calls
++ * bgp_ls_originate_link() to create and install BGP-LS route.
++ *
++ * @param bgp - BGP instance
++ * @param edge - Link State edge from ls_msg2edge()
++ * @param event - LS_MSG_EVENT_ADD, UPDATE, or DELETE
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_process_edge(struct bgp *bgp, struct ls_edge *edge, uint8_t event);
++
++/*
++ * Process Link State subnet (prefix) and originate BGP-LS Prefix NLRI
++ *
++ * Extracts IGP prefix information from ls_subnet and calls
++ * bgp_ls_originate_prefix() to create and install BGP-LS route.
++ *
++ * @param bgp - BGP instance
++ * @param subnet - Link State subnet from ls_msg2subnet()
++ * @param event - LS_MSG_EVENT_ADD, UPDATE, or DELETE
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_process_subnet(struct bgp *bgp, struct ls_subnet *subnet, uint8_t event);
++
++/*
++ * Withdraw Node NLRI from BGP-LS RIB
++ *
++ * Removes a BGP-LS Node NLRI from the RIB when the IGP router is removed.
++ *
++ * @param bgp - BGP instance
++ * @param protocol_id - IGP protocol (ISIS/OSPF)
++ * @param router_id - IGP router ID
++ * @param router_id_len - Length of router ID
++ * @param area_id - IGP area/level identifier
++ * @return 0 on success, -1 on error
++ */
++int bgp_ls_withdraw_node(struct bgp *bgp, uint8_t protocol_id, uint8_t *router_id,
++ uint16_t router_id_len, uint32_t area_id, struct ls_vertex *vertex);
++
++int bgp_ls_withdraw_link(struct bgp *bgp, uint8_t protocol_id, uint8_t *local_router_id,
++ uint16_t local_router_id_len, uint8_t *remote_router_id,
++ uint16_t remote_router_id_len, uint32_t area_id, struct ls_edge *edge);
++
++int bgp_ls_withdraw_prefix(struct bgp *bgp, uint8_t protocol_id, uint8_t *router_id,
++ uint16_t router_id_len, struct prefix *prefix, uint32_t area_id,
++ struct ls_subnet *subnet);
++
++/*
++ * ===========================================================================
++ * Link-State Message Processing
++ * ===========================================================================
++ */
++
++/*
++ * Process link-state message from zebra
++ *
++ * Handles link-state SYNC/UPDATE messages received from zebra.
++ * Parses the message and delegates to bgp_ls_process_message().
++ *
++ * @param s - Stream containing the message
++ * @param msg_type - Message type (LINK_STATE_UPDATE or LINK_STATE_SYNC)
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_process_linkstate_message(struct stream *s, uint8_t msg_type);
++
++/*
++ * Process link-state message and update TED
++ *
++ * Converts message to TED structure (vertex/edge/subnet) and triggers
++ * BGP-LS route origination.
++ *
++ * @param bgp - BGP instance
++ * @param msg - Link-state message to process
++ * @return 0 on success, -1 on error
++ */
++extern int bgp_ls_process_message(struct bgp *bgp, struct ls_message *msg);
++
++/*
++ * Withdraw all locally originated BGP-LS routes and reset the TED.
++ *
++ * Called when the last BGP-LS peer is deactivated. Withdraws every node,
++ * link, and prefix NLRI that was originated from the TED, then discards
++ * the stale TED.
++ *
++ * @param bgp - BGP instance
++ */
++extern void bgp_ls_withdraw_ted(struct bgp *bgp);
++
++#endif /* _FRR_BGP_LS_TED_H */
+diff --git a/bgpd/bgp_open.c b/bgpd/bgp_open.c
+index 7d6a0b2c24..f9e0a15bfe 100644
+--- a/bgpd/bgp_open.c
++++ b/bgpd/bgp_open.c
+@@ -157,6 +157,11 @@ void bgp_capability_vty_out(struct vty *vty, struct peer *peer, bool use_json,
+ "capabilityErrorMultiProtocolAfi",
+ "L2VPN");
+ break;
++ case AFI_BGP_LS:
++ json_object_string_add(json_cap,
++ "capabilityErrorMultiProtocolAfi",
++ "BGP-LS");
++ break;
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ json_object_int_add(
+@@ -208,6 +213,11 @@ void bgp_capability_vty_out(struct vty *vty, struct peer *peer, bool use_json,
+ "capabilityErrorMultiProtocolSafi",
+ "flowspec");
+ break;
++ case SAFI_BGP_LS:
++ json_object_string_add(json_cap,
++ "capabilityErrorMultiProtocolSafi",
++ "BGP-LS");
++ break;
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+ json_object_int_add(
+@@ -229,6 +239,9 @@ void bgp_capability_vty_out(struct vty *vty, struct peer *peer, bool use_json,
+ case AFI_L2VPN:
+ vty_out(vty, "AFI L2VPN, ");
+ break;
++ case AFI_BGP_LS:
++ vty_out(vty, "AFI BGP-LS, ");
++ break;
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ vty_out(vty, "AFI Unknown %d, ",
+@@ -257,6 +270,9 @@ void bgp_capability_vty_out(struct vty *vty, struct peer *peer, bool use_json,
+ case SAFI_EVPN:
+ vty_out(vty, "SAFI EVPN");
+ break;
++ case SAFI_BGP_LS:
++ vty_out(vty, "SAFI BGP-LS");
++ break;
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+ vty_out(vty, "SAFI Unknown %d ",
+@@ -1511,7 +1527,8 @@ int bgp_open_option_parse(struct peer *peer, uint16_t length,
+ && !peer->afc_nego[AFI_IP6][SAFI_MPLS_VPN]
+ && !peer->afc_nego[AFI_IP6][SAFI_ENCAP]
+ && !peer->afc_nego[AFI_IP6][SAFI_FLOWSPEC]
+- && !peer->afc_nego[AFI_L2VPN][SAFI_EVPN]) {
++ && !peer->afc_nego[AFI_L2VPN][SAFI_EVPN]
++ && !peer->afc_nego[AFI_BGP_LS][SAFI_BGP_LS]) {
+ flog_err(EC_BGP_PKT_OPEN,
+ "%s [Error] Configured AFI/SAFIs do not overlap with received MP capabilities",
+ peer->host);
+diff --git a/bgpd/bgp_packet.c b/bgpd/bgp_packet.c
+index 2157f2a02c..f41890e230 100644
+--- a/bgpd/bgp_packet.c
++++ b/bgpd/bgp_packet.c
+@@ -50,6 +50,7 @@
+ #include "bgpd/bgp_keepalives.h"
+ #include "bgpd/bgp_flowspec.h"
+ #include "bgpd/bgp_trace.h"
++#include "bgpd/bgp_ls.h"
+
+ DEFINE_HOOK(bgp_packet_dump,
+ (struct peer *peer, uint8_t type, bgp_size_t size,
+@@ -322,6 +323,8 @@ int bgp_nlri_parse(struct peer *peer, struct attr *attr,
+ return bgp_nlri_parse_evpn(peer, attr, packet, mp_withdraw);
+ case SAFI_FLOWSPEC:
+ return bgp_nlri_parse_flowspec(peer, attr, packet, mp_withdraw);
++ case SAFI_BGP_LS:
++ return bgp_nlri_parse_ls(peer, mp_withdraw ? NULL : attr, packet);
+ }
+ return BGP_NLRI_PARSE_ERROR;
+ }
+diff --git a/bgpd/bgp_route.c b/bgpd/bgp_route.c
+index 0db68920a0..157f5ec333 100644
+--- a/bgpd/bgp_route.c
++++ b/bgpd/bgp_route.c
+@@ -64,6 +64,9 @@
+ #include "bgpd/bgp_network.h"
+ #include "bgpd/bgp_trace.h"
+ #include "bgpd/bgp_rpki.h"
++#include "bgpd/bgp_bfd.h"
++#include "bgpd/bgp_ls_nlri.h"
++#include "bgpd/bgp_ls.h"
+
+ #ifdef ENABLE_BGP_VNC
+ #include "bgpd/rfapi/rfapi_backend.h"
+@@ -81,6 +84,10 @@
+
+ #include "bgpd/bgp_route_clippy.c"
+
++void bgp_ls_nlri_format(struct bgp_ls_nlri *nlri, char *buf, size_t buf_len);
++struct json_object *bgp_ls_nlri_to_json(struct bgp_ls_nlri *nlri);
++
++
+ static bool bgp_attr_nexthop_same(const struct attr *attr1, const struct attr *attr2, afi_t afi)
+ {
+ afi_t nh_afi1 = BGP_ATTR_NH_AFI(afi, attr1);
+@@ -10256,6 +10263,18 @@ static void route_vty_out_route(struct bgp_dest *dest, const struct prefix *p,
+ json ?
+ NLRI_STRING_FORMAT_JSON_SIMPLE :
+ NLRI_STRING_FORMAT_MIN, json);
++ } else if (dest->ls_nlri) {
++ char nlri_str[1024];
++
++ bgp_ls_nlri_format(dest->ls_nlri, nlri_str, sizeof(nlri_str));
++ if (!json) {
++ len = vty_out(vty, "%s", nlri_str);
++ } else {
++ json_object *json_nlri = bgp_ls_nlri_to_json(dest->ls_nlri);
++
++ json_object_string_add(json, "nlriStr", nlri_str);
++ json_object_object_add(json, "nlri", json_nlri);
++ }
+ } else {
+ if (!json)
+ len = vty_out(vty, "%pFX", p);
+@@ -10665,7 +10684,8 @@ void route_vty_out(struct vty *vty, const struct prefix *p,
+ vty_out(vty, "%*s", len, " ");
+ }
+ }
+- } else if (p->family == AF_INET && !BGP_ATTR_MP_NEXTHOP_LEN_IP6(attr)) {
++ } else if ((p->family == AF_INET || safi == SAFI_BGP_LS) &&
++ !BGP_ATTR_MP_NEXTHOP_LEN_IP6(attr)) {
+ if (json_paths) {
+ json_nexthop_global = json_object_new_object();
+
+@@ -11005,6 +11025,9 @@ void route_vty_out_tmp(struct vty *vty, struct bgp *bgp, struct bgp_dest *dest,
+ json_object_string_addf(
+ json_net, "nextHop", "%pI4",
+ &attr->mp_nexthop_global_in);
++ } else if (safi == SAFI_BGP_LS) {
++ json_object_string_addf(json_net, "nextHop", "%pI4",
++ &attr->nexthop);
+ }
+
+ if (use_bgp_med_value(attr, bgp)) {
+@@ -11050,6 +11073,11 @@ void route_vty_out_tmp(struct vty *vty, struct bgp *bgp, struct bgp_dest *dest,
+ vty_out(vty, "\n%*s", 38, " ");
+ else
+ vty_out(vty, "%*s", len, " ");
++ } else if (safi == SAFI_BGP_LS) {
++ if (wide)
++ vty_out(vty, "%-41pI4", &attr->nexthop);
++ else
++ vty_out(vty, "%-16pI4", &attr->nexthop);
+ }
+
+ if (use_bgp_med_value(attr, bgp)) {
+@@ -11587,6 +11615,7 @@ void route_vty_out_detail(struct vty *vty, struct bgp *bgp, struct bgp_dest *bn,
+ json_object *json_string = NULL;
+ json_object *json_int = NULL;
+ json_object *json_adv_to = NULL;
++ json_object *json_ls_attr = NULL;
+ int first = 0;
+ struct listnode *node, *nnode;
+ struct peer *peer;
+@@ -11854,6 +11883,21 @@ void route_vty_out_detail(struct vty *vty, struct bgp *bgp, struct bgp_dest *bn,
+ if (json_paths)
+ json_object_string_add(json_nexthop_global, "afi",
+ "ipv4");
++ } else if (safi == SAFI_BGP_LS) {
++ if (json_paths) {
++ json_object_string_addf(json_nexthop_global, "ip", "%pI4",
++ &attr->mp_nexthop_global_in);
++
++ if (path->peer->hostname)
++ json_object_string_add(json_nexthop_global, "hostname",
++ path->peer->hostname);
++ } else {
++ if (nexthop_hostname)
++ vty_out(vty, " %pI4(%s)", &attr->mp_nexthop_global_in,
++ nexthop_hostname);
++ else
++ vty_out(vty, " %pI4", &attr->mp_nexthop_global_in);
++ }
+ } else {
+ if (json_paths) {
+ bool ll_nexthop = IN6_IS_ADDR_LINKLOCAL(&attr->mp_nexthop_global);
+@@ -12697,6 +12741,15 @@ void route_vty_out_detail(struct vty *vty, struct bgp *bgp, struct bgp_dest *bn,
+ }
+ }
+
++ /* Display BGP-LS attributes if this is link-state SAFI */
++ if (safi == SAFI_BGP_LS && attr && attr->ls_attr) {
++ if (json_paths) {
++ json_ls_attr = bgp_ls_attr_to_json(attr->ls_attr);
++ json_object_object_add(json_path, "linkStateAttrs", json_ls_attr);
++ } else
++ bgp_ls_attr_display(vty, attr->ls_attr);
++ }
++
+ /* Output some debug about internal state of the dest flags */
+ if (json_paths) {
+ if (CHECK_FLAG(bn->flags, BGP_NODE_PROCESS_SCHEDULED))
+@@ -13084,6 +13137,8 @@ static int bgp_show_table(struct vty *vty, struct bgp *bgp, afi_t afi, safi_t sa
+ vty_out(vty, BGP_SHOW_NCODE_HEADER);
+ vty_out(vty, BGP_SHOW_OCODE_HEADER);
+ vty_out(vty, BGP_SHOW_RPKI_HEADER);
++ if (safi == SAFI_BGP_LS)
++ vty_out(vty, BGP_SHOW_BGP_LS_PREFIX_CODES_HEADER);
+ }
+ if (type == bgp_show_type_dampend_paths
+ || type == bgp_show_type_damp_neighbor)
+@@ -13165,6 +13220,14 @@ static int bgp_show_table(struct vty *vty, struct bgp *bgp, afi_t afi, safi_t sa
+ vty_out(vty, ",\"%s/%d\": ", retstr,
+ dest_p->u.prefix_flowspec
+ .prefixlen);
++ } else if (safi == SAFI_BGP_LS) {
++ char nlri_str[1024];
++
++ bgp_ls_nlri_format(dest->ls_nlri, nlri_str, sizeof(nlri_str));
++ if (first)
++ vty_out(vty, "\"%s\": ", nlri_str);
++ else
++ vty_out(vty, ",\"%s\": ", nlri_str);
+ } else {
+ if (first)
+ vty_out(vty, "\"%pFX\": ", dest_p);
+@@ -13473,6 +13536,33 @@ void route_vty_out_detail_header(struct vty *vty, struct bgp *bgp,
+ : "");
+ bgp_evpn_route2json((struct prefix_evpn *)p, json);
+ }
++ } else if (safi == SAFI_BGP_LS) {
++ /* BGP-LS: Display NLRI in format [V][L2][I0x0][N[s0000.0000.0001]]/length */
++ char nlri_str[512];
++
++ if (dest->ls_nlri) {
++ bgp_ls_nlri_format(dest->ls_nlri, nlri_str, sizeof(nlri_str));
++ size_t nlri_len = bgp_ls_nlri_size(dest->ls_nlri);
++
++ if (!json) {
++ vty_out(vty, "BGP routing table entry for %s/%zu\n", nlri_str,
++ nlri_len * 8);
++ bgp_ls_nlri_display(vty, dest->ls_nlri);
++ } else {
++ json_object *json_nlri = bgp_ls_nlri_to_json(dest->ls_nlri);
++
++ /* Use structured JSON format for NLRI */
++ snprintfrr(nlri_str, sizeof(nlri_str), "%s/%zu", nlri_str,
++ nlri_len * 8);
++ json_object_string_add(json, "nlriStr", nlri_str);
++ json_object_object_add(json, "nlri", json_nlri);
++ }
++ } else {
++ if (!json)
++ vty_out(vty, "BGP routing table entry for (unknown BGP-LS NLRI)\n");
++ else
++ json_object_string_add(json, "nlri", "unknown");
++ }
+ } else {
+ if (!json) {
+ vty_out(vty,
+@@ -13734,6 +13824,7 @@ const struct prefix_rd *bgp_rd_from_dest(const struct bgp_dest *dest,
+ case SAFI_ENCAP:
+ case SAFI_EVPN:
+ return (struct prefix_rd *)(bgp_dest_get_prefix(dest));
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_UNICAST:
+ case SAFI_MULTICAST:
+@@ -14652,6 +14743,65 @@ DEFPY(show_ip_bgp, show_ip_bgp_cmd,
+ return CMD_SUCCESS;
+ }
+
++DEFUN (show_bgp_link_state_route,
++ show_bgp_link_state_route_cmd,
++ "show bgp link-state link-state WORD [json]",
++ SHOW_STR
++ BGP_STR
++ "Link-State information\n"
++ "BGP-LS routes\n"
++ "NLRI string\n"
++ JSON_STR)
++{
++ struct bgp *bgp = NULL;
++ struct bgp_dest *dest;
++ int display = 0;
++ const char *nlri_str;
++ bool uj = false;
++ json_object *json = NULL;
++ uint16_t show_opts = 0;
++
++ bgp = bgp_get_default();
++ if (!bgp) {
++ vty_out(vty, "%% No BGP instance found\n");
++ return CMD_WARNING;
++ }
++
++ if (!bgp->ls_info) {
++ vty_out(vty, "%% BGP-LS not available\n");
++ return CMD_WARNING;
++ }
++
++ nlri_str = argv[4]->arg;
++ uj = use_json(argc, argv);
++
++ if (uj) {
++ json = json_object_new_object();
++ SET_FLAG(show_opts, BGP_SHOW_OPT_JSON);
++ }
++
++ /* Lookup the NLRI */
++ dest = bgp_ls_lookup_nlri_by_str(bgp, nlri_str);
++ if (!dest) {
++ if (uj) {
++ json_object_string_add(json, "error", "NLRI not found");
++ vty_json(vty, json);
++ } else {
++ vty_out(vty, "%% BGP-LS NLRI not found\n");
++ }
++ return CMD_WARNING;
++ }
++
++ /* Display the path info */
++ bgp_show_path_info(NULL, dest, vty, bgp, AFI_BGP_LS, SAFI_BGP_LS, json, BGP_PATH_SHOW_ALL,
++ &display, RPKI_NOT_BEING_USED, NULL, show_opts);
++
++ if (uj)
++ vty_json(vty, json);
++
++ return CMD_SUCCESS;
++}
++
+ DEFUN (show_ip_bgp_route,
+ show_ip_bgp_route_cmd,
+ "show [ip] bgp [<view|vrf> VIEWVRFNAME] ["BGP_AFI_CMD_STR" ["BGP_SAFI_WITH_LABEL_CMD_STR"]]<A.B.C.D|A.B.C.D/M|X:X::X:X|X:X::X:X/M> [internal] [<bestpath|multipath>] [rpki <valid|invalid|notfound>] [json]",
+@@ -15004,6 +15154,8 @@ static void bgp_table_stats_walker(struct event *t)
+ case AFI_L2VPN:
+ space = EVPN_ROUTE_PREFIXLEN;
+ break;
++ case AFI_BGP_LS:
++ /* TODO */
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ return;
+@@ -15266,6 +15418,8 @@ static int bgp_table_stats_single(struct vty *vty, struct bgp *bgp, afi_t afi,
+ case AFI_L2VPN:
+ bitlen = EVPN_ROUTE_PREFIXLEN;
+ break;
++ case AFI_BGP_LS:
++ /* TODO */
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ break;
+@@ -15634,6 +15788,24 @@ DEFUN (show_bgp_l2vpn_evpn_route_prefix,
+ BGP_PATH_SHOW_ALL, RPKI_NOT_BEING_USED, use_json(argc, argv), 0);
+ }
+
++DEFUN (show_bgp_ls,
++ show_bgp_ls_cmd,
++ "show bgp link-state link-state [json]",
++ SHOW_STR
++ BGP_STR
++ "Link-State information\n"
++ "BGP-LS routes\n"
++ JSON_STR)
++{
++ uint16_t show_flags = 0;
++
++ if (use_json(argc, argv))
++ SET_FLAG(show_flags, BGP_SHOW_OPT_JSON);
++
++ return bgp_show(vty, NULL, AFI_BGP_LS, SAFI_BGP_LS, bgp_show_type_normal, NULL, show_flags,
++ RPKI_NOT_BEING_USED);
++}
++
+ static void show_adj_route_header(struct vty *vty, struct peer *peer,
+ struct bgp_table *table, int *header1,
+ int *header2, json_object *json, bool wide,
+@@ -17658,6 +17830,7 @@ void bgp_route_init(void)
+ install_element(VIEW_NODE, &show_bgp_afi_vpn_rd_route_cmd);
+ install_element(VIEW_NODE,
+ &show_bgp_l2vpn_evpn_route_prefix_cmd);
++ install_element(VIEW_NODE, &show_bgp_ls_cmd);
+
+ /* BGP dampening clear commands */
+ install_element(ENABLE_NODE, &clear_ip_bgp_dampening_cmd);
+@@ -17743,6 +17916,9 @@ void bgp_route_init(void)
+ install_element(VIEW_NODE, &show_ip_bgp_large_community_list_cmd);
+ install_element(VIEW_NODE, &show_ip_bgp_large_community_cmd);
+
++ /* BGP Link-State */
++ install_element(VIEW_NODE, &show_bgp_link_state_route_cmd);
++
+ /* show bgp vrf <afi> <safi> detailed */
+ install_element(VIEW_NODE,
+ &show_ip_bgp_vrf_afi_safi_routes_detailed_cmd);
+diff --git a/bgpd/bgp_route.h b/bgpd/bgp_route.h
+index e9f9368a9a..26d69e31c1 100644
+--- a/bgpd/bgp_route.h
++++ b/bgpd/bgp_route.h
+@@ -68,6 +68,16 @@ enum bgp_show_adj_route_type {
+ "RPKI validation codes: V valid, I invalid, N Not found\n\n"
+ #define BGP_SHOW_HEADER " Network Next Hop Metric LocPrf Weight Path\n"
+ #define BGP_SHOW_HEADER_WIDE " Network Next Hop Metric LocPrf Weight Path\n"
++#define BGP_SHOW_BGP_LS_PREFIX_CODES_HEADER \
++ "Prefix codes: E link, V node, T IP reachable route, S SRv6 SID, SP SRTE Policy, u/U unknown\n" \
++ " I Identifier, N local node, R remote node, L link, P prefix, S SID, C candidate path\n" \
++ " L1/L2 ISIS level-1/level-2, O OSPF, O3 OSPFv3, D direct, ST static/peer-node, SR Segment Routing\n" \
++ " a area-ID, l link-ID, t topology-ID, s ISO-ID,\n" \
++ " c confed-ID/ASN, b bgp-identifier, r router-ID, te te-router-ID, sd SID\n" \
++ " i if-address, n nbr-address, o OSPF Route-type, p IP-prefix\n" \
++ " d designated router address, po protocol-origin, f flag\n" \
++ " e endpoint-ip, cl color, as originator-asn oa originator-address\n" \
++ " di discriminator, q bgp-router-ID\n"
+
+ /* Maximum number of sids we can process or send with a prefix. */
+ #define BGP_MAX_SIDS 6
+diff --git a/bgpd/bgp_table.c b/bgpd/bgp_table.c
+index ac84ec77a9..354ca8167d 100644
+--- a/bgpd/bgp_table.c
++++ b/bgpd/bgp_table.c
+@@ -17,6 +17,7 @@
+ #include "bgpd/bgp_table.h"
+ #include "bgp_addpath.h"
+ #include "bgp_trace.h"
++#include "bgp_ls.h"
+
+ void bgp_table_lock(struct bgp_table *rt)
+ {
+@@ -88,6 +89,13 @@ inline struct bgp_dest *bgp_dest_unlock_node(struct bgp_dest *dest)
+ &dest->tx_addpath, rt->afi,
+ rt->safi);
+ }
++
++ if (dest->ls_nlri) {
++ if (rt->bgp && rt->bgp->ls_info)
++ bgp_ls_nlri_hash_del(&rt->bgp->ls_info->nlri_hash, dest->ls_nlri);
++ bgp_ls_nlri_free(dest->ls_nlri);
++ }
++
+ XFREE(MTYPE_BGP_NODE, dest);
+ dest = NULL;
+ rn->info = NULL;
+diff --git a/bgpd/bgp_table.h b/bgpd/bgp_table.h
+index c3964433e6..4bb76705d5 100644
+--- a/bgpd/bgp_table.h
++++ b/bgpd/bgp_table.h
+@@ -85,6 +85,8 @@ struct bgp_dest {
+
+ mpls_label_t local_label;
+
++ struct bgp_ls_nlri *ls_nlri;
++
+ uint16_t flags;
+ #define BGP_NODE_PROCESS_SCHEDULED (1 << 0)
+ #define BGP_NODE_USER_CLEAR (1 << 1)
+diff --git a/bgpd/bgp_updgrp.c b/bgpd/bgp_updgrp.c
+index b680cdda27..c6c8ed62e1 100644
+--- a/bgpd/bgp_updgrp.c
++++ b/bgpd/bgp_updgrp.c
+@@ -467,6 +467,10 @@ static unsigned int updgrp_hash_key_make(const void *p)
+ (CHECK_FLAG(peer->af_flags[afi][safi], PEER_FLAG_NEXTHOP_LOCAL_UNCHANGED)))
+ key = jhash(&peer->nexthop.v6_global, IPV6_MAX_BYTELEN, key);
+
++ key = jhash_1word(CHECK_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV4), key);
++
++ key = jhash_1word(CHECK_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV6), key);
++
+ /*
+ * ANY NEW ITEMS THAT ARE ADDED TO THE key, ENSURE DEBUG
+ * STATEMENT STAYS UP TO DATE
+diff --git a/bgpd/bgp_updgrp_packet.c b/bgpd/bgp_updgrp_packet.c
+index 449173d93f..315bacb2b0 100644
+--- a/bgpd/bgp_updgrp_packet.c
++++ b/bgpd/bgp_updgrp_packet.c
+@@ -42,6 +42,7 @@
+ #include "bgpd/bgp_mplsvpn.h"
+ #include "bgpd/bgp_label.h"
+ #include "bgpd/bgp_addpath.h"
++#include "bgpd/bgp_ls_nlri.h"
+
+ /********************
+ * PRIVATE FUNCTIONS
+@@ -684,6 +685,7 @@ struct bpacket *subgroup_update_packet(struct update_subgroup *subgrp)
+ mpls_label_t *label_pnt = NULL;
+ mpls_label_t labels[BGP_MAX_LABELS] = { MPLS_INVALID_LABEL };
+ uint8_t num_labels = 0;
++ struct bgp_ls_nlri *ls_nlri = NULL;
+
+ if (!subgrp)
+ return NULL;
+@@ -758,8 +760,7 @@ struct bpacket *subgroup_update_packet(struct update_subgroup *subgrp)
+ * attr. */
+ total_attr_len = bgp_packet_attribute(NULL, peer, s, adv->baa->attr,
+ &vecarr, NULL, afi, safi, from, NULL,
+- NULL, 0, 0, 0, path);
+-
++ NULL, 0, 0, 0, path, dest->ls_nlri);
+ space_remaining =
+ STREAM_CONCAT_REMAIN(s, snlri, STREAM_SIZE(s))
+ - BGP_MAX_PACKET_SIZE_OVERFLOW;
+@@ -829,7 +830,39 @@ struct bpacket *subgroup_update_packet(struct update_subgroup *subgrp)
+ labels[0] = bgp_mplsvpn_nh_label_bind_get_label(path);
+ label_pnt = &labels[0];
+ num_labels = 1;
+- } else {
++ } else if (afi == AFI_BGP_LS && safi == SAFI_BGP_LS) {
++ ls_nlri = dest->ls_nlri;
++ if (!ls_nlri) {
++ flog_err(EC_BGP_UPDATE_SND,
++ "BGP-LS path missing ls_nlri data");
++ adv = bgp_advertise_clean_subgroup(subgrp, adj);
++ continue;
++ }
++
++ if (bgp_debug_update(NULL, NULL, subgrp->update_group, 0)) {
++ if (!send_attr_printed) {
++ iana_afi_t pkt_afi = afi_int2iana(afi);
++ iana_safi_t pkt_safi = safi_int2iana(safi);
++
++ zlog_debug("u%" PRIu64 ":s%" PRIu64
++ " send UPDATE w/ attr: %s",
++ subgrp->update_group->id, subgrp->id,
++ send_attr_str);
++
++ zlog_debug("u%" PRIu64 ":s%" PRIu64
++ " send MP_REACH for afi/safi %s/%s",
++ subgrp->update_group->id, subgrp->id,
++ iana_afi2str(pkt_afi),
++ iana_safi2str(pkt_safi));
++ send_attr_printed = 1;
++ }
++ zlog_debug("u%" PRIu64 ":s%" PRIu64
++ " send UPDATE BGP-LS NLRI",
++ subgrp->update_group->id, subgrp->id);
++ }
++ }
++
++ else {
+ num_labels = BGP_PATH_INFO_NUM_LABELS(path);
+ label_pnt =
+ num_labels
+@@ -842,10 +875,9 @@ struct bpacket *subgroup_update_packet(struct update_subgroup *subgrp)
+ snlri, peer, afi, safi, &vecarr,
+ adv->baa->attr);
+
+- bgp_packet_mpattr_prefix(snlri, afi, safi, dest_p, prd,
+- label_pnt, num_labels,
+- addpath_capable, addpath_tx_id,
+- adv->baa->attr);
++ bgp_packet_mpattr_prefix(snlri, afi, safi, dest_p, prd, label_pnt,
++ num_labels, addpath_capable, addpath_tx_id,
++ adv->baa->attr, ls_nlri);
+ }
+
+ num_pfx++;
+@@ -960,6 +992,7 @@ struct bpacket *subgroup_withdraw_packet(struct update_subgroup *subgrp)
+ int addpath_overhead = 0;
+ uint32_t addpath_tx_id = 0;
+ const struct prefix_rd *prd = NULL;
++ struct bgp_ls_nlri *ls_nlri = NULL;
+
+
+ if (!subgrp)
+@@ -1000,6 +1033,53 @@ struct bpacket *subgroup_withdraw_packet(struct update_subgroup *subgrp)
+ } else
+ first_time = 0;
+
++ /* BGP-LS uses special encoding - handle before standard cases */
++ if (afi == AFI_BGP_LS && safi == SAFI_BGP_LS) {
++ /* Format MP_UNREACH header if first time */
++ if (first_time) {
++ iana_afi_t pkt_afi = afi_int2iana(afi);
++ iana_safi_t pkt_safi = safi_int2iana(safi);
++
++ attrlen_pos = stream_get_endp(s);
++ stream_putw(s, 0); /* total attr length = 0 for now */
++ mp_start = stream_get_endp(s);
++ mplen_pos = bgp_packet_mpunreach_start(s, afi, safi);
++
++ if (bgp_debug_update(NULL, NULL, subgrp->update_group, 0))
++ zlog_debug("u%" PRIu64 ":s%" PRIu64
++ " send MP_UNREACH for afi/safi %s/%s",
++ subgrp->update_group->id, subgrp->id,
++ iana_afi2str(pkt_afi), iana_safi2str(pkt_safi));
++ }
++
++ ls_nlri = dest->ls_nlri;
++ if (ls_nlri) {
++ /* Encode the BGP-LS NLRI into the stream */
++ if (bgp_ls_encode_nlri(s, ls_nlri) < 0) {
++ flog_err(EC_BGP_UPDATE_SND,
++ "Failed to encode BGP-LS NLRI withdrawal");
++ bgp_adj_out_remove_subgroup(dest, adj, subgrp);
++ continue;
++ }
++ } else {
++ flog_err(EC_BGP_UPDATE_SND,
++ "BGP-LS withdrawal missing ls_nlri data");
++ bgp_adj_out_remove_subgroup(dest, adj, subgrp);
++ continue;
++ }
++
++ num_pfx++;
++
++ if (bgp_debug_update(NULL, NULL, subgrp->update_group, 0))
++ zlog_debug("u%" PRIu64 ":s%" PRIu64
++ " send UPDATE BGP-LS NLRI -- unreachable",
++ subgrp->update_group->id, subgrp->id);
++
++ subgrp->scount--;
++ bgp_adj_out_remove_subgroup(dest, adj, subgrp);
++ continue;
++ }
++
+ if (afi == AFI_IP && safi == SAFI_UNICAST
+ && !peer_cap_enhe(peer, afi, safi))
+ stream_put_prefix_addpath(s, dest_p, addpath_capable,
+@@ -1009,6 +1089,16 @@ struct bpacket *subgroup_withdraw_packet(struct update_subgroup *subgrp)
+ prd = (struct prefix_rd *)bgp_dest_get_prefix(
+ dest->pdest);
+
++ if (safi == SAFI_BGP_LS) {
++ ls_nlri = dest->ls_nlri;
++ if (!ls_nlri) {
++ flog_err(EC_BGP_UPDATE_SND,
++ "BGP-LS path missing ls_nlri data");
++ bgp_advertise_clean_subgroup(subgrp, adj);
++ continue;
++ }
++ }
++
+ /* If first time, format the MP_UNREACH header
+ */
+ if (first_time) {
+@@ -1036,9 +1126,8 @@ struct bpacket *subgroup_withdraw_packet(struct update_subgroup *subgrp)
+ iana_safi2str(pkt_safi));
+ }
+
+- bgp_packet_mpunreach_prefix(s, dest_p, afi, safi, prd,
+- NULL, 0, addpath_capable,
+- addpath_tx_id, NULL);
++ bgp_packet_mpunreach_prefix(s, dest_p, afi, safi, prd, NULL, 0,
++ addpath_capable, addpath_tx_id, NULL, ls_nlri);
+ }
+
+ num_pfx++;
+@@ -1168,7 +1257,7 @@ void subgroup_default_update_packet(struct update_subgroup *subgrp,
+ stream_putw(s, 0);
+ total_attr_len = bgp_packet_attribute(NULL, peer, s, attr, &vecarr, &p, afi, safi, from,
+ NULL, &label, num_labels, addpath_capable,
+- BGP_ADDPATH_TX_ID_FOR_DEFAULT_ORIGINATE, NULL);
++ BGP_ADDPATH_TX_ID_FOR_DEFAULT_ORIGINATE, NULL, NULL);
+
+ /* Set Total Path Attribute Length. */
+ stream_putw_at(s, pos, total_attr_len);
+@@ -1267,9 +1356,8 @@ void subgroup_default_withdraw_packet(struct update_subgroup *subgrp)
+ stream_putw(s, 0);
+ mp_start = stream_get_endp(s);
+ mplen_pos = bgp_packet_mpunreach_start(s, afi, safi);
+- bgp_packet_mpunreach_prefix(
+- s, &p, afi, safi, NULL, NULL, 0, addpath_capable,
+- BGP_ADDPATH_TX_ID_FOR_DEFAULT_ORIGINATE, NULL);
++ bgp_packet_mpunreach_prefix(s, &p, afi, safi, NULL, NULL, 0, addpath_capable,
++ BGP_ADDPATH_TX_ID_FOR_DEFAULT_ORIGINATE, NULL, NULL);
+
+ /* Set the mp_unreach attr's length */
+ bgp_packet_mpunreach_end(s, mplen_pos);
+diff --git a/bgpd/bgp_vty.c b/bgpd/bgp_vty.c
+index 5669baa87a..ebad07dac8 100644
+--- a/bgpd/bgp_vty.c
++++ b/bgpd/bgp_vty.c
+@@ -170,6 +170,7 @@ static enum node_type bgp_node_type(afi_t afi, safi_t safi)
+ return BGP_VPNV4_NODE;
+ case SAFI_FLOWSPEC:
+ return BGP_FLOWSPECV4_NODE;
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_ENCAP:
+ case SAFI_EVPN:
+@@ -190,6 +191,7 @@ static enum node_type bgp_node_type(afi_t afi, safi_t safi)
+ return BGP_VPNV6_NODE;
+ case SAFI_FLOWSPEC:
+ return BGP_FLOWSPECV6_NODE;
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_ENCAP:
+ case SAFI_EVPN:
+@@ -200,6 +202,8 @@ static enum node_type bgp_node_type(afi_t afi, safi_t safi)
+ break;
+ case AFI_L2VPN:
+ return BGP_EVPN_NODE;
++ case AFI_BGP_LS:
++ return BGP_LS_NODE;
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ // We should never be here but to clarify the switch statement..
+@@ -241,6 +245,9 @@ static const char *get_afi_safi_vty_str(afi_t afi, safi_t safi)
+ } else if (afi == AFI_L2VPN) {
+ if (safi == SAFI_EVPN)
+ return "L2VPN EVPN";
++ } else if (afi == AFI_BGP_LS) {
++ if (safi == SAFI_BGP_LS)
++ return "Link-State Link-State";
+ }
+
+ return "Unknown";
+@@ -283,6 +290,9 @@ static const char *get_afi_safi_json_str(afi_t afi, safi_t safi)
+ } else if (afi == AFI_L2VPN) {
+ if (safi == SAFI_EVPN)
+ return "l2VpnEvpn";
++ } else if (afi == AFI_BGP_LS) {
++ if (safi == SAFI_BGP_LS)
++ return "linkState";
+ }
+
+ return "Unknown";
+@@ -419,6 +429,9 @@ afi_t bgp_node_afi(struct vty *vty)
+ case BGP_EVPN_NODE:
+ afi = AFI_L2VPN;
+ break;
++ case BGP_LS_NODE:
++ afi = AFI_BGP_LS;
++ break;
+ default:
+ afi = AFI_IP;
+ break;
+@@ -451,6 +464,9 @@ safi_t bgp_node_safi(struct vty *vty)
+ case BGP_FLOWSPECV6_NODE:
+ safi = SAFI_FLOWSPEC;
+ break;
++ case BGP_LS_NODE:
++ safi = SAFI_BGP_LS;
++ break;
+ default:
+ safi = SAFI_UNICAST;
+ break;
+@@ -581,6 +597,7 @@ static const char *get_bgp_default_af_flag(afi_t afi, safi_t safi)
+ return "ipv4-labeled-unicast";
+ case SAFI_FLOWSPEC:
+ return "ipv4-flowspec";
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_EVPN:
+ case SAFI_MAX:
+@@ -601,6 +618,7 @@ static const char *get_bgp_default_af_flag(afi_t afi, safi_t safi)
+ return "ipv6-labeled-unicast";
+ case SAFI_FLOWSPEC:
+ return "ipv6-flowspec";
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_EVPN:
+ case SAFI_MAX:
+@@ -611,6 +629,7 @@ static const char *get_bgp_default_af_flag(afi_t afi, safi_t safi)
+ switch (safi) {
+ case SAFI_EVPN:
+ return "l2vpn-evpn";
++ case SAFI_BGP_LS:
+ case SAFI_UNICAST:
+ case SAFI_MULTICAST:
+ case SAFI_MPLS_VPN:
+@@ -622,6 +641,22 @@ static const char *get_bgp_default_af_flag(afi_t afi, safi_t safi)
+ return "unknown-afi/safi";
+ }
+ break;
++ case AFI_BGP_LS:
++ switch (safi) {
++ case SAFI_BGP_LS:
++ return "link-state";
++ case SAFI_UNICAST:
++ case SAFI_MULTICAST:
++ case SAFI_MPLS_VPN:
++ case SAFI_ENCAP:
++ case SAFI_LABELED_UNICAST:
++ case SAFI_FLOWSPEC:
++ case SAFI_EVPN:
++ case SAFI_UNSPEC:
++ case SAFI_MAX:
++ return "unknown-afi/safi";
++ }
++ break;
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ return "unknown-afi/safi";
+@@ -11211,6 +11246,18 @@ DEFUN_NOSH (address_family_evpn,
+ return CMD_SUCCESS;
+ }
+
++DEFUN_NOSH(address_family_link_state,
++ address_family_link_state_cmd,
++ "address-family link-state [link-state]",
++ "Enter Address Family command mode\n"
++ "Link-State Address Family\n"
++ "Link-State Subsequent Address Family\n")
++{
++ VTY_DECLVAR_CONTEXT(bgp, bgp);
++ vty->node = BGP_LS_NODE;
++ return CMD_SUCCESS;
++}
++
+ DEFUN_NOSH (bgp_segment_routing_srv6,
+ bgp_segment_routing_srv6_cmd,
+ "segment-routing srv6",
+@@ -11450,7 +11497,8 @@ DEFUN_NOSH (exit_address_family,
+ || vty->node == BGP_IPV6L_NODE || vty->node == BGP_VPNV6_NODE
+ || vty->node == BGP_EVPN_NODE
+ || vty->node == BGP_FLOWSPECV4_NODE
+- || vty->node == BGP_FLOWSPECV6_NODE)
++ || vty->node == BGP_FLOWSPECV6_NODE
++ || vty->node == BGP_LS_NODE)
+ vty->node = BGP_NODE;
+ return CMD_SUCCESS;
+ }
+@@ -19816,6 +19864,9 @@ static void bgp_config_write_family(struct vty *vty, struct bgp *bgp, afi_t afi,
+ } else if (afi == AFI_L2VPN) {
+ if (safi == SAFI_EVPN)
+ vty_frame(vty, "l2vpn evpn");
++ } else if (afi == AFI_BGP_LS) {
++ if (safi == SAFI_BGP_LS)
++ vty_frame(vty, "link-state link-state");
+ }
+ vty_frame(vty, "\n");
+
+@@ -20458,6 +20509,9 @@ int bgp_config_write(struct vty *vty)
+ /* EVPN configuration. */
+ bgp_config_write_family(vty, bgp, AFI_L2VPN, SAFI_EVPN);
+
++ /* BGP-LS configuration. */
++ bgp_config_write_family(vty, bgp, AFI_BGP_LS, SAFI_BGP_LS);
++
+ hook_call(bgp_inst_config_write, bgp, vty);
+
+ #ifdef ENABLE_BGP_VNC
+@@ -20582,6 +20636,14 @@ static struct cmd_node bgp_srv6_node = {
+ .prompt = "%s(config-router-srv6)# ",
+ };
+
++static struct cmd_node bgp_ls_node = {
++ .name = "bgp link-state",
++ .node = BGP_LS_NODE,
++ .parent_node = BGP_NODE,
++ .prompt = "%s(config-router-af)# ",
++ .no_xpath = true,
++};
++
+ static void community_list_vty(void);
+
+ static void bgp_ac_peergroup(vector comps, struct cmd_token *token)
+@@ -20901,6 +20963,7 @@ void bgp_vty_init(void)
+ install_node(&bgp_flowspecv4_node);
+ install_node(&bgp_flowspecv6_node);
+ install_node(&bgp_srv6_node);
++ install_node(&bgp_ls_node);
+
+ /* Install default VTY commands to new nodes. */
+ install_default(BGP_NODE);
+@@ -20917,6 +20980,7 @@ void bgp_vty_init(void)
+ install_default(BGP_EVPN_NODE);
+ install_default(BGP_EVPN_VNI_NODE);
+ install_default(BGP_SRV6_NODE);
++ install_default(BGP_LS_NODE);
+
+ /* "global bgp inq-limit command */
+ install_element(CONFIG_NODE, &bgp_inq_limit_cmd);
+@@ -21303,6 +21367,7 @@ void bgp_vty_init(void)
+ install_element(BGP_FLOWSPECV4_NODE, &neighbor_activate_cmd);
+ install_element(BGP_FLOWSPECV6_NODE, &neighbor_activate_cmd);
+ install_element(BGP_EVPN_NODE, &neighbor_activate_cmd);
++ install_element(BGP_LS_NODE, &neighbor_activate_cmd);
+
+ /* "no neighbor activate" commands. */
+ install_element(BGP_NODE, &no_neighbor_activate_hidden_cmd);
+@@ -21317,6 +21382,7 @@ void bgp_vty_init(void)
+ install_element(BGP_FLOWSPECV4_NODE, &no_neighbor_activate_cmd);
+ install_element(BGP_FLOWSPECV6_NODE, &no_neighbor_activate_cmd);
+ install_element(BGP_EVPN_NODE, &no_neighbor_activate_cmd);
++ install_element(BGP_LS_NODE, &no_neighbor_activate_cmd);
+
+ /* "neighbor peer-group" set commands. */
+ install_element(BGP_NODE, &neighbor_set_peer_group_cmd);
+@@ -22362,6 +22428,7 @@ void bgp_vty_init(void)
+ #endif /* KEEP_OLD_VPN_COMMANDS */
+
+ install_element(BGP_NODE, &address_family_evpn_cmd);
++ install_element(BGP_NODE, &address_family_link_state_cmd);
+
+ /* "exit-address-family" command. */
+ install_element(BGP_IPV4_NODE, &exit_address_family_cmd);
+@@ -22375,6 +22442,7 @@ void bgp_vty_init(void)
+ install_element(BGP_FLOWSPECV4_NODE, &exit_address_family_cmd);
+ install_element(BGP_FLOWSPECV6_NODE, &exit_address_family_cmd);
+ install_element(BGP_EVPN_NODE, &exit_address_family_cmd);
++ install_element(BGP_LS_NODE, &exit_address_family_cmd);
+
+ /* BGP retain all route-target */
+ install_element(BGP_VPNV4_NODE, &bgp_retain_route_target_cmd);
+diff --git a/bgpd/bgp_zebra.c b/bgpd/bgp_zebra.c
+index 1d33883e87..1b0464dbfb 100644
+--- a/bgpd/bgp_zebra.c
++++ b/bgpd/bgp_zebra.c
+@@ -54,6 +54,7 @@
+ #include "bgpd/bgp_trace.h"
+ #include "bgpd/bgp_community.h"
+ #include "bgpd/bgp_lcommunity.h"
++#include "bgpd/bgp_ls_ted.h"
+
+ /* All information about zebra. */
+ struct zclient *bgp_zclient = NULL;
+@@ -4007,6 +4008,35 @@ static int bgp_zebra_process_srv6_locator_delete(ZAPI_CALLBACK_ARGS)
+ return 0;
+ }
+
++/* Handle ZEBRA_OPAQUE_MESSAGE for BGP-LS link-state updates */
++static int bgp_zebra_opaque_msg_handler(ZAPI_CALLBACK_ARGS)
++{
++ int ret = 0;
++ struct stream *s;
++ struct zapi_opaque_msg info;
++
++ s = zclient->ibuf;
++ if (zclient_opaque_decode(s, &info) != 0) {
++ zlog_err("%s: Failed to decode opaque message", __func__);
++ return -1;
++ }
++
++ switch (info.type) {
++ case LINK_STATE_SYNC:
++ case LINK_STATE_UPDATE:
++ ret = bgp_ls_process_linkstate_message(s, info.type);
++ break;
++
++ default:
++ if (BGP_DEBUG(zebra, ZEBRA))
++ zlog_debug("%s: Ignoring unknown opaque message type %d", __func__,
++ info.type);
++ break;
++ }
++
++ return ret;
++}
++
+ static zclient_handler *const bgp_handlers[] = {
+ [ZEBRA_ROUTER_ID_UPDATE] = bgp_router_id_update,
+ [ZEBRA_INTERFACE_ADDRESS_ADD] = bgp_interface_address_add,
+@@ -4035,9 +4065,9 @@ static zclient_handler *const bgp_handlers[] = {
+ [ZEBRA_ROUTE_NOTIFY_OWNER] = bgp_zebra_route_notify_owner,
+ [ZEBRA_SRV6_LOCATOR_ADD] = bgp_zebra_process_srv6_locator_add,
+ [ZEBRA_SRV6_LOCATOR_DELETE] = bgp_zebra_process_srv6_locator_delete,
+- [ZEBRA_SRV6_MANAGER_GET_LOCATOR_CHUNK] =
+- bgp_zebra_process_srv6_locator_chunk,
++ [ZEBRA_SRV6_MANAGER_GET_LOCATOR_CHUNK] = bgp_zebra_process_srv6_locator_chunk,
+ [ZEBRA_SRV6_SID_NOTIFY] = bgp_zebra_srv6_sid_notify,
++ [ZEBRA_OPAQUE_MESSAGE] = bgp_zebra_opaque_msg_handler,
+ };
+
+ static int bgp_if_new_hook(struct interface *ifp)
+diff --git a/bgpd/bgpd.c b/bgpd/bgpd.c
+index 0000ebd862..7e1f45e4c1 100644
+--- a/bgpd/bgpd.c
++++ b/bgpd/bgpd.c
+@@ -79,6 +79,8 @@
+ #include "bgpd/bgp_evpn_mh.h"
+ #include "bgpd/bgp_mac.h"
+ #include "bgpd/bgp_trace.h"
++#include "bgpd/bgp_ls.h"
++#include "bgpd/bgp_ls_ted.h"
+
+ DEFINE_MTYPE_STATIC(BGPD, PEER_TX_SHUTDOWN_MSG, "Peer shutdown message (TX)");
+ DEFINE_QOBJ_TYPE(bgp_master);
+@@ -2545,6 +2547,19 @@ static int peer_activate_af(struct peer *peer, afi_t afi, safi_t safi)
+ peer_notify_config_change(other->connection);
+ }
+
++ if (afi == AFI_BGP_LS && safi == SAFI_BGP_LS) {
++ if (peer->connection->su.sa.sa_family == AF_INET6) {
++ SET_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV6);
++ UNSET_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV4);
++ } else if (peer->connection->su.sa.sa_family == AF_INET) {
++ SET_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV4);
++ UNSET_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV6);
++ } else {
++ UNSET_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV4);
++ UNSET_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV6);
++ }
++ }
++
+ return 0;
+ }
+
+@@ -2609,6 +2624,24 @@ int peer_activate(struct peer *peer, afi_t afi, safi_t safi)
+ /* connect to table manager */
+ bgp_zebra_init_tm_connect(bgp);
+ }
++
++ /*
++ * Register with zebra link-state database when the first peer is
++ * activated for BGP-LS. This allows BGP to receive IGP topology
++ * updates from ISIS/OSPF for distribution to BGP-LS peers.
++ */
++ if (afi == AFI_BGP_LS && safi == SAFI_BGP_LS && !bgp_ls_is_registered(bgp)) {
++ if (!bgp_ls_register(bgp)) {
++ zlog_err("BGP-LS: Failed to register with link-state database for instance %s",
++ bgp->name_pretty);
++ return -1;
++ }
++
++ if (BGP_DEBUG(linkstate, LINKSTATE) || BGP_DEBUG(zebra, ZEBRA))
++ zlog_debug("BGP-LS: Registered with link-state database for instance %s (first peer with BGP-LS activated: %s)",
++ bgp->name_pretty, peer->host);
++ }
++
+ return ret;
+ }
+
+@@ -2655,6 +2688,11 @@ static bool non_peergroup_deactivate_af(struct peer *peer, afi_t afi,
+ peer_notify_config_change(peer->connection);
+ }
+
++ if (afi == AFI_BGP_LS && safi == SAFI_BGP_LS) {
++ UNSET_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV4);
++ UNSET_FLAG(peer->af_flags[AFI_BGP_LS][SAFI_BGP_LS], PEER_FLAG_BGP_LS_IPV6);
++ }
++
+ return false;
+ }
+
+@@ -2705,6 +2743,36 @@ int peer_deactivate(struct peer *peer, afi_t afi, safi_t safi)
+ bgp->allocate_mpls_labels[afi][safi_check] = 0;
+ bgp_recalculate_afi_safi_bestpaths(bgp, afi, safi_check);
+ }
++
++ /*
++ * Unregister from zebra link-state database when the last peer is
++ * deactivated for BGP-LS. This stops receiving IGP topology updates
++ * from ISIS/OSPF since no BGP-LS peers remain active.
++ */
++ if (afi == AFI_BGP_LS && safi == SAFI_BGP_LS && bgp_ls_is_registered(bgp)) {
++ bool last_peer = true;
++
++ for (ALL_LIST_ELEMENTS_RO(bgp->peer, node, tmp_peer)) {
++ if (tmp_peer != peer && tmp_peer->afc[AFI_BGP_LS][SAFI_BGP_LS]) {
++ last_peer = false;
++ break;
++ }
++ }
++
++ if (last_peer) {
++ bgp_ls_withdraw_ted(bgp);
++
++ if (!bgp_ls_unregister(bgp)) {
++ zlog_err("BGP-LS: Failed to unregister from link-state database for instance %s",
++ bgp->name_pretty);
++ } else {
++ if (BGP_DEBUG(linkstate, LINKSTATE) || BGP_DEBUG(zebra, ZEBRA))
++ zlog_debug("BGP-LS: Unregistered from link-state database for instance %s (last peer with BGP-LS deactivated: %s)",
++ bgp->name_pretty, peer->host);
++ }
++ }
++ }
++
+ return ret;
+ }
+
+@@ -3720,6 +3788,7 @@ peer_init:
+ bgp_evpn_vrf_es_init(bgp);
+ bgp_pbr_init(bgp);
+ bgp_srv6_init(bgp);
++ bgp_ls_init(bgp);
+ }
+
+ /*initilize global GR FSM */
+@@ -4501,6 +4570,7 @@ void bgp_free(struct bgp *bgp)
+
+ bgp_evpn_cleanup(bgp);
+ bgp_pbr_cleanup(bgp);
++ bgp_ls_cleanup(bgp);
+
+ for (afi = AFI_IP; afi < AFI_MAX; afi++) {
+ enum vpn_policy_direction dir;
+@@ -4879,7 +4949,8 @@ enum bgp_peer_active peer_active(struct peer_connection *connection)
+ || peer->afc[AFI_IP6][SAFI_MPLS_VPN]
+ || peer->afc[AFI_IP6][SAFI_ENCAP]
+ || peer->afc[AFI_IP6][SAFI_FLOWSPEC]
+- || peer->afc[AFI_L2VPN][SAFI_EVPN])
++ || peer->afc[AFI_L2VPN][SAFI_EVPN]
++ || peer->afc[AFI_BGP_LS][SAFI_BGP_LS])
+ return BGP_PEER_ACTIVE;
+
+ return BGP_PEER_AF_UNCONFIGURED;
+@@ -4900,7 +4971,8 @@ bool peer_active_nego(struct peer *peer)
+ || peer->afc_nego[AFI_IP6][SAFI_MPLS_VPN]
+ || peer->afc_nego[AFI_IP6][SAFI_ENCAP]
+ || peer->afc_nego[AFI_IP6][SAFI_FLOWSPEC]
+- || peer->afc_nego[AFI_L2VPN][SAFI_EVPN])
++ || peer->afc_nego[AFI_L2VPN][SAFI_EVPN]
++ || peer->afc_nego[AFI_BGP_LS][SAFI_BGP_LS])
+ return true;
+ return false;
+ }
+diff --git a/bgpd/bgpd.h b/bgpd/bgpd.h
+index c863ed2eeb..b1887d9222 100644
+--- a/bgpd/bgpd.h
++++ b/bgpd/bgpd.h
+@@ -95,6 +95,7 @@ enum bgp_af_index {
+ BGP_AF_IPV6_LBL_UNICAST,
+ BGP_AF_IPV4_FLOWSPEC,
+ BGP_AF_IPV6_FLOWSPEC,
++ BGP_AF_BGP_LS,
+ BGP_AF_MAX
+ };
+
+@@ -728,6 +729,9 @@ struct bgp {
+ /* BGP routing information base. */
+ struct bgp_table *rib[AFI_MAX][SAFI_MAX];
+
++ /* BGP-LS specific data */
++ struct bgp_ls *ls_info;
++
+ /* BGP table route-map. */
+ struct bgp_rmap table_map[AFI_MAX][SAFI_MAX];
+
+@@ -1773,6 +1777,8 @@ struct peer {
+ #define PEER_FLAG_CONFIG_DAMPENING (1ULL << 31)
+ #define PEER_FLAG_CONFIG_ENCAPSULATION_SRV6 (1ULL << 32)
+ #define PEER_FLAG_CONFIG_ENCAPSULATION_MPLS (1ULL << 33)
++#define PEER_FLAG_BGP_LS_IPV4 (1ULL << 35)
++#define PEER_FLAG_BGP_LS_IPV6 (1ULL << 36)
+ #define PEER_FLAG_ACCEPT_OWN (1ULL << 63)
+
+ enum bgp_addpath_strat addpath_type[AFI_MAX][SAFI_MAX];
+@@ -2229,6 +2235,7 @@ struct bgp_nlri {
+ #define BGP_ATTR_ENCAP 23
+ #define BGP_ATTR_IPV6_EXT_COMMUNITIES 25
+ #define BGP_ATTR_AIGP 26
++#define BGP_ATTR_LINK_STATE 29 /* BGP-LS Attribute (RFC 9552) */
+ #define BGP_ATTR_LARGE_COMMUNITIES 32
+ #define BGP_ATTR_OTC 35
+ #define BGP_ATTR_NHC 39
+@@ -2834,6 +2841,7 @@ static inline int afindex(afi_t afi, safi_t safi)
+ return BGP_AF_IPV4_ENCAP;
+ case SAFI_FLOWSPEC:
+ return BGP_AF_IPV4_FLOWSPEC;
++ case SAFI_BGP_LS:
+ case SAFI_EVPN:
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+@@ -2854,6 +2862,7 @@ static inline int afindex(afi_t afi, safi_t safi)
+ return BGP_AF_IPV6_ENCAP;
+ case SAFI_FLOWSPEC:
+ return BGP_AF_IPV6_FLOWSPEC;
++ case SAFI_BGP_LS:
+ case SAFI_EVPN:
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+@@ -2864,11 +2873,28 @@ static inline int afindex(afi_t afi, safi_t safi)
+ switch (safi) {
+ case SAFI_EVPN:
+ return BGP_AF_L2VPN_EVPN;
++ case SAFI_BGP_LS:
++ case SAFI_UNICAST:
++ case SAFI_MULTICAST:
++ case SAFI_LABELED_UNICAST:
++ case SAFI_MPLS_VPN:
++ case SAFI_ENCAP:
++ case SAFI_FLOWSPEC:
++ case SAFI_UNSPEC:
++ case SAFI_MAX:
++ return BGP_AF_MAX;
++ }
++ break;
++ case AFI_BGP_LS:
++ switch (safi) {
++ case SAFI_BGP_LS:
++ return BGP_AF_BGP_LS;
+ case SAFI_UNICAST:
+ case SAFI_MULTICAST:
+ case SAFI_LABELED_UNICAST:
+ case SAFI_MPLS_VPN:
+ case SAFI_ENCAP:
++ case SAFI_EVPN:
+ case SAFI_FLOWSPEC:
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+@@ -2921,7 +2947,8 @@ static inline int peer_group_af_configured(struct peer_group *group)
+ || peer->afc[AFI_IP6][SAFI_MPLS_VPN]
+ || peer->afc[AFI_IP6][SAFI_ENCAP]
+ || peer->afc[AFI_IP6][SAFI_FLOWSPEC]
+- || peer->afc[AFI_L2VPN][SAFI_EVPN])
++ || peer->afc[AFI_L2VPN][SAFI_EVPN]
++ || peer->afc[AFI_BGP_LS][SAFI_BGP_LS])
+ return 1;
+ return 0;
+ }
+diff --git a/bgpd/rfapi/rfapi_import.c b/bgpd/rfapi/rfapi_import.c
+index 7f23df799a..c73390b629 100644
+--- a/bgpd/rfapi/rfapi_import.c
++++ b/bgpd/rfapi/rfapi_import.c
+@@ -233,6 +233,7 @@ void rfapiCheckRefcount(struct agg_node *rn, safi_t safi, int lockoffset)
+ }
+ break;
+
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_UNICAST:
+ case SAFI_MULTICAST:
+@@ -2981,6 +2982,7 @@ static void rfapiBgpInfoFilteredImportEncap(
+ rt = import_table->imported_encap[afi];
+ break;
+
++ case AFI_BGP_LS:
+ case AFI_UNSPEC:
+ case AFI_L2VPN:
+ case AFI_MAX:
+@@ -3435,6 +3437,7 @@ void rfapiBgpInfoFilteredImportVPN(
+ rt = import_table->imported_vpn[afi];
+ break;
+
++ case AFI_BGP_LS:
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ flog_err(EC_LIB_DEVELOPMENT, "%s: bad afi %d", __func__, afi);
+@@ -3831,6 +3834,7 @@ rfapiBgpInfoFilteredImportFunction(safi_t safi)
+ case SAFI_ENCAP:
+ return rfapiBgpInfoFilteredImportEncap;
+
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_UNICAST:
+ case SAFI_MULTICAST:
+@@ -4076,6 +4080,7 @@ static void rfapiProcessPeerDownRt(struct peer *peer,
+ rt = import_table->imported_encap[afi];
+ timer_service_func = rfapiWithdrawTimerEncap;
+ break;
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_UNICAST:
+ case SAFI_MULTICAST:
+diff --git a/bgpd/rfapi/rfapi_monitor.c b/bgpd/rfapi/rfapi_monitor.c
+index 83e8afa0e5..45ada52202 100644
+--- a/bgpd/rfapi/rfapi_monitor.c
++++ b/bgpd/rfapi/rfapi_monitor.c
+@@ -231,6 +231,7 @@ void rfapiMonitorExtraFlush(safi_t safi, struct agg_node *rn)
+ }
+ break;
+
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_UNICAST:
+ case SAFI_MULTICAST:
+@@ -299,6 +300,7 @@ void rfapiMonitorExtraPrune(safi_t safi, struct agg_node *rn)
+ }
+ break;
+
++ case SAFI_BGP_LS:
+ case SAFI_UNSPEC:
+ case SAFI_UNICAST:
+ case SAFI_MULTICAST:
+diff --git a/bgpd/subdir.am b/bgpd/subdir.am
+index c59a669b38..c04b3548a1 100644
+--- a/bgpd/subdir.am
++++ b/bgpd/subdir.am
+@@ -51,6 +51,9 @@ bgpd_libbgp_a_SOURCES = \
+ bgpd/bgp_label.c \
+ bgpd/bgp_labelpool.c \
+ bgpd/bgp_lcommunity.c \
++ bgpd/bgp_ls.c \
++ bgpd/bgp_ls_nlri.c \
++ bgpd/bgp_ls_ted.c \
+ bgpd/bgp_mac.c \
+ bgpd/bgp_memory.c \
+ bgpd/bgp_mpath.c \
+@@ -135,6 +138,9 @@ noinst_HEADERS += \
+ bgpd/bgp_label.h \
+ bgpd/bgp_labelpool.h \
+ bgpd/bgp_lcommunity.h \
++ bgpd/bgp_ls.h \
++ bgpd/bgp_ls_nlri.h \
++ bgpd/bgp_ls_ted.h \
+ bgpd/bgp_mac.h \
+ bgpd/bgp_memory.h \
+ bgpd/bgp_mpath.h \
+diff --git a/doc/user/bgp-linkstate.rst b/doc/user/bgp-linkstate.rst
+new file mode 100644
+index 0000000000..ba44a0f00f
+--- /dev/null
++++ b/doc/user/bgp-linkstate.rst
+@@ -0,0 +1,207 @@
++.. _bgp-link-state:
++
++BGP Link-State
++==============
++
++BGP Link-State (BGP-LS) is a mechanism to distribute network topology information
++via BGP. It enables BGP to carry link-state information learned from IGP protocols
++(IS-IS, OSPF) to other BGP speakers. This is useful for applications such as
++Software-Defined Networking (SDN) controllers, Traffic Engineering, and network
++visualization tools that need a complete view of the network topology.
++
++BGP-LS is defined in :rfc:`9552`.
++
++Overview
++--------
++
++BGP-LS introduces a new BGP AFI/SAFI combination (AFI 16388, SAFI 71) to carry
++link-state Network Layer Reachability Information (NLRI). The link-state NLRI
++describes nodes, links, and prefixes in the network topology.
++
++Three types of BGP-LS NLRI are defined:
++
++- **Node NLRI** - Describes routers/nodes in the network
++- **Link NLRI** - Describes links between nodes
++- **Prefix NLRI** - Describes IPv4/IPv6 prefixes advertised by nodes
++
++BGP-LS carries topology information in a new BGP path attribute called the
++BGP-LS Attribute (type 29), which contains various TLVs describing node
++properties, link metrics, bandwidth, prefix attributes, and other topology
++characteristics.
++
++Configuration
++-------------
++
++BGP-LS Producer Configuration
++^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
++
++To enable BGP-LS and originate topology information from the local IGP, configure
++the link-state address family:
++
++.. code-block:: frr
++
++ router bgp 65001
++ neighbor 192.0.2.1 remote-as 65002
++ !
++ address-family link-state
++ neighbor 192.0.2.1 activate
++ exit-address-family
++
++When the link-state address family is activated on a neighbor, BGP will:
++
++1. Negotiate BGP-LS capability during session establishment
++2. Receive IGP topology updates from Zebra (IS-IS and OSPF)
++3. Convert IGP topology to BGP-LS NLRI (Node, Link, Prefix)
++4. Advertise BGP-LS routes to the activated neighbor
++
++BGP-LS Consumer Configuration
++^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
++
++To receive and process BGP-LS information from peers without originating routes:
++
++.. code-block:: frr
++
++ router bgp 65002
++ neighbor 192.0.2.2 remote-as 65001
++ !
++ address-family link-state
++ neighbor 192.0.2.2 activate
++ exit-address-family
++
++Displaying BGP-LS Information
++------------------------------
++
++Show All BGP-LS NLRIs
++^^^^^^^^^^^^^^^^^^^^^^^
++
++.. clicmd:: show bgp link-state link-state [json]
++
++ Display all BGP-LS routes in the link-state RIB. Shows Node, Link, and Prefix
++ NLRI with their associated attributes.
++
++ Example output:
++
++ .. code-block:: frr
++
++ router# show bgp link-state link-state
++ BGP table version is 15, local router ID is 192.0.2.1
++ Status codes: s suppressed, d damped, h history, * valid, > best, = multipath,
++ i internal, r RIB-failure, S Stale, R Removed
++ Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
++ Origin codes: i - IGP, e - EGP, ? - incomplete
++ RPKI validation codes: V valid, I invalid, N Not found
++
++ Network Next Hop Metric LocPrf Weight Path
++ *> [1][0.0.0.0][6][0][0][0300.0000.0001]
++ 0.0.0.0(vrf1) 0 32768 ?
++ *> [2][0.0.0.0][6][0][0][0300.0000.0001][0300.0000.0002][192.0.2.1][192.0.2.2]
++ 0.0.0.0(vrf1) 0 32768 ?
++
++ With the ``json`` option, output is displayed in JSON format suitable for
++ programmatic processing.
++
++Show Specific BGP-LS NLRI
++^^^^^^^^^^^^^^^^^^^^^^^^^^^
++
++.. clicmd:: show bgp link-state link-state NLRI [json]
++
++ Display detailed information about a specific BGP-LS NLRI identified by its
++ NLRI string. The NLRI string format is:
++
++ - Node NLRI: ``[1][IGP-ID][Protocol][Instance][AS][Area/Domain][IGP-Router-ID]``
++ - Link NLRI: ``[2][IGP-ID][Protocol][Instance][AS][Area][Local-Router-ID][Remote-Router-ID][Local-IP][Remote-IP]``
++ - Prefix NLRI: ``[3|4][IGP-ID][Protocol][Instance][AS][Area][Router-ID][Prefix]``
++
++ Example:
++
++ .. code-block:: frr
++
++ router# show bgp link-state link-state [1][0.0.0.0][6][0][0][0300.0000.0001]
++ BGP routing table entry for [1][0.0.0.0][6][0][0][0300.0000.0001]
++ Paths: (1 available, best #1, table link-state)
++ Not advertised to any peer
++ Local from 0.0.0.0 (192.0.2.1)
++ Origin incomplete, metric 0, localpref 100, valid, sourced, local, best (First path received)
++ Last update: Thu Jan 23 10:15:42 2026
++
++ Node Descriptors:
++ Protocol-ID: IS-IS Level 2
++ AS Number: 0
++ IGP Router-ID: 0300.0000.0001
++
++ Node Attributes:
++ Router-ID: 192.0.2.1
++ ISIS Area-ID: 49.0001
++ Node Flags: 0x00
++
++Use ``show bgp [afi] [safi] summary`` to display BGP session summary including
++link-state address family statistics.
++
++Debugging
++---------
++
++.. clicmd:: debug bgp linkstate
++
++ Enable debugging for BGP-LS operations including:
++
++ - NLRI encoding/decoding
++ - Attribute processing
++ - IGP topology updates
++ - Route origination and withdrawal
++ - TED updates
++
++Configuration Example
++---------------------
++
++Complete example with IS-IS IGP and BGP-LS:
++
++.. code-block:: frr
++
++ ! IS-IS configuration
++ router isis 1
++ net 49.0001.0300.0000.0001.00
++ is-type level-2-only
++ topology ipv6-unicast
++ !
++ interface eth0
++ ip router isis 1
++ isis network point-to-point
++ !
++ ! BGP configuration with Link-State
++ router bgp 65001
++ bgp router-id 192.0.2.1
++ neighbor 192.0.2.100 remote-as 65002
++ neighbor 192.0.2.100 description SDN-Controller
++ !
++ address-family link-state
++ neighbor 192.0.2.100 activate
++ exit-address-family
++
++In this example:
++
++- IS-IS is configured to run on the local router
++- BGP session is established with an SDN controller (192.0.2.100)
++- Link-state address family is activated for the controller neighbor
++- BGP-LS will automatically convert IS-IS topology to BGP-LS NLRI and
++ advertise it to the controller
++
++Use Cases
++---------
++
++BGP-LS is typically used in the following scenarios:
++
++**SDN Controllers**
++ SDN controllers use BGP-LS to obtain a complete view of the network topology
++ for path computation, traffic engineering, and network optimization.
++
++**PCE (Path Computation Element)**
++ PCE servers use BGP-LS to maintain an up-to-date TED for computing MPLS-TE
++ and SR-TE paths.
++
++**Network Monitoring and Visualization**
++ Network management systems use BGP-LS to visualize network topology and
++ monitor link utilization, metrics, and failures.
++
++**Multi-Domain TE**
++ BGP-LS enables traffic engineering across multiple IGP domains by sharing
++ topology information between domains via BGP.
+\ No newline at end of file
+diff --git a/doc/user/bgp.rst b/doc/user/bgp.rst
+index 3b8fc9fbc3..b0e880a62e 100644
+--- a/doc/user/bgp.rst
++++ b/doc/user/bgp.rst
+@@ -5751,6 +5751,8 @@ Show command json output:
+
+ .. include:: flowspec.rst
+
++.. include:: bgp-linkstate.rst
++
+ .. [bgp-route-osci-cond] McPherson, D. and Gill, V. and Walton, D., "Border Gateway Protocol (BGP) Persistent Route Oscillation Condition", IETF RFC3345
+ .. [stable-flexible-ibgp] Flavel, A. and M. Roughan, "Stable and flexible iBGP", ACM SIGCOMM 2009
+ .. [ibgp-correctness] Griffin, T. and G. Wilfong, "On the correctness of IBGP configuration", ACM SIGCOMM 2002
+diff --git a/isisd/isis_lsp.c b/isisd/isis_lsp.c
+index 1a1140bc5d..9a18fc4596 100644
+--- a/isisd/isis_lsp.c
++++ b/isisd/isis_lsp.c
+@@ -1075,7 +1075,7 @@ static void lsp_build(struct isis_lsp *lsp, struct isis_area *area)
+
+ lsp_add_auth(lsp);
+
+- isis_tlvs_add_area_addresses(lsp->tlvs, area->area_addrs);
++ isis_tlvs_add_area_addresses(lsp->tlvs, &area->area_addrs);
+
+ /* Protocols Supported */
+ if (area->ip_circuits > 0 || area->ipv6_circuits > 0) {
+diff --git a/isisd/isis_nb_config.c b/isisd/isis_nb_config.c
+index 6bd3b39bc7..5deab4985d 100644
+--- a/isisd/isis_nb_config.c
++++ b/isisd/isis_nb_config.c
+@@ -139,7 +139,6 @@ int isis_instance_area_address_create(struct nb_cb_create_args *args)
+ {
+ struct isis_area *area;
+ struct iso_address addr, *addrr = NULL, *addrp = NULL;
+- struct listnode *node;
+ struct sysid_iter iter;
+ uint8_t buff[255];
+ const char *net_title = yang_dnode_get_string(args->dnode, NULL);
+@@ -192,11 +191,9 @@ int isis_instance_area_address_create(struct nb_cb_create_args *args)
+ area->isis->sysid_set = 1;
+ } else {
+ /* check that we don't already have this address */
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs, node,
+- addrp)) {
+- if ((addrp->addr_len + ISIS_SYS_ID_LEN
+- + ISIS_NSEL_LEN)
+- != (addrr->addr_len))
++ frr_each (iso_address_list, &area->area_addrs, addrp) {
++ if ((addrp->addr_len + ISIS_SYS_ID_LEN + ISIS_NSEL_LEN) !=
++ (addrr->addr_len))
+ continue;
+ if (!memcmp(addrp->area_addr, addrr->area_addr,
+ addrr->addr_len)) {
+@@ -208,11 +205,10 @@ int isis_instance_area_address_create(struct nb_cb_create_args *args)
+
+ /*Forget the systemID part of the address */
+ addrr->addr_len -= (ISIS_SYS_ID_LEN + ISIS_NSEL_LEN);
+- assert(area->area_addrs); /* to silence scan-build sillyness */
+- listnode_add(area->area_addrs, addrr);
++ iso_address_list_add_tail(&area->area_addrs, addrr);
+
+ /* only now we can safely generate our LSPs for this area */
+- if (listcount(area->area_addrs) > 0) {
++ if (iso_address_list_count(&area->area_addrs) > 0) {
+ if (area->is_type & IS_LEVEL_1)
+ lsp_generate(area, IS_LEVEL_1);
+ if (area->is_type & IS_LEVEL_2)
+@@ -227,7 +223,6 @@ int isis_instance_area_address_create(struct nb_cb_create_args *args)
+ int isis_instance_area_address_destroy(struct nb_cb_destroy_args *args)
+ {
+ struct iso_address addr, *addrp = NULL;
+- struct listnode *node;
+ uint8_t buff[255];
+ struct isis_area *area;
+ const char *net_title;
+@@ -243,21 +238,20 @@ int isis_instance_area_address_destroy(struct nb_cb_destroy_args *args)
+ memcpy(addr.area_addr, buff, (int)addr.addr_len);
+ area = nb_running_get_entry(args->dnode, NULL, true);
+
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs, node, addrp)) {
+- if ((addrp->addr_len + ISIS_SYS_ID_LEN + 1) == addr.addr_len
+- && !memcmp(addrp->area_addr, addr.area_addr, addr.addr_len))
++ frr_each (iso_address_list, &area->area_addrs, addrp) {
++ if ((addrp->addr_len + ISIS_SYS_ID_LEN + 1) == addr.addr_len &&
++ !memcmp(addrp->area_addr, addr.area_addr, addr.addr_len))
+ break;
+ }
+ if (!addrp)
+ return NB_ERR_INCONSISTENCY;
+
+- listnode_delete(area->area_addrs, addrp);
++ iso_address_list_del(&area->area_addrs, addrp);
+ /*
+ * Last area address - reset the SystemID for this router
+ */
+- if (!memcmp(addrp->area_addr + addrp->addr_len, area->isis->sysid,
+- ISIS_SYS_ID_LEN) &&
+- listcount(area->area_addrs) == 0) {
++ if (!memcmp(addrp->area_addr + addrp->addr_len, area->isis->sysid, ISIS_SYS_ID_LEN) &&
++ iso_address_list_count(&area->area_addrs) == 0) {
+ for (ALL_LIST_ELEMENTS_RO(area->circuit_list, cnode, circuit))
+ for (lvl = IS_LEVEL_1; lvl <= IS_LEVEL_2; ++lvl) {
+ if (circuit->u.bc.is_dr[lvl - 1])
+diff --git a/isisd/isis_pdu.c b/isisd/isis_pdu.c
+index 1a3142e72a..6df061a2f8 100644
+--- a/isisd/isis_pdu.c
++++ b/isisd/isis_pdu.c
+@@ -163,7 +163,7 @@ static int process_p2p_hello(struct iih_info *iih)
+ (iih->circuit->is_type_config == IS_LEVEL_1_AND_2) &&
+ (iih->circ_type == IS_LEVEL_1))) {
+ if (!isis_tlvs_area_addresses_match(iih->tlvs,
+- iih->circuit->area
++ &iih->circuit->area
+ ->area_addrs)) {
+ if (IS_DEBUG_ADJ_PACKETS) {
+ zlog_debug("ISIS-Adj (%s): Rcvd P2P IIH from (%s), cir type %s, cir id %u, length %u",
+@@ -201,7 +201,7 @@ static int process_p2p_hello(struct iih_info *iih)
+ iih->calculated_type = IS_LEVEL_1_AND_2;
+
+ if (!isis_tlvs_area_addresses_match(iih->tlvs,
+- iih->circuit->area
++ &iih->circuit->area
+ ->area_addrs)) {
+ iih->calculated_type = IS_LEVEL_2;
+ }
+@@ -277,7 +277,7 @@ static int process_p2p_hello(struct iih_info *iih)
+
+ /* 8.2.5.2 a) a match was detected */
+ if (isis_tlvs_area_addresses_match(iih->tlvs,
+- iih->circuit->area->area_addrs)) {
++ &iih->circuit->area->area_addrs)) {
+ /* 8.2.5.2 a) 2) If the calculated type is L1 - table 5 */
+ if (iih->calculated_type == IS_LEVEL_1) {
+ switch (iih->circ_type) {
+@@ -362,7 +362,7 @@ static int process_p2p_hello(struct iih_info *iih)
+ }
+ }
+ /* 8.2.5.2 b) if no match was detected */
+- else if (listcount(iih->circuit->area->area_addrs) > 0) {
++ else if (iso_address_list_count(&iih->circuit->area->area_addrs) > 0) {
+ if (iih->calculated_type == IS_LEVEL_1) {
+ /* 8.2.5.2 b) 1) is_type L1 and adj is not up */
+ if (adj->adj_state != ISIS_ADJ_UP) {
+@@ -789,10 +789,10 @@ static int process_hello(uint8_t pdu_type, struct isis_circuit *circuit,
+ }
+
+ if (!p2p_hello
+- && (listcount(circuit->area->area_addrs) == 0
++ && (iso_address_list_count(&circuit->area->area_addrs) == 0
+ || (level == ISIS_LEVEL1
+ && !isis_tlvs_area_addresses_match(
+- iih.tlvs, circuit->area->area_addrs)))) {
++ iih.tlvs, &circuit->area->area_addrs)))) {
+ if (IS_DEBUG_ADJ_PACKETS) {
+ zlog_debug(
+ "ISIS-Adj (%s): Area mismatch, level %d IIH on %s",
+@@ -1952,12 +1952,12 @@ int send_hello(struct isis_circuit *circuit, int level)
+
+ isis_tlvs_add_auth(tlvs, &circuit->passwd);
+
+- if (!listcount(circuit->area->area_addrs)) {
++ if (!iso_address_list_count(&circuit->area->area_addrs)) {
+ isis_free_tlvs(tlvs);
+ return ISIS_WARNING;
+ }
+
+- isis_tlvs_add_area_addresses(tlvs, circuit->area->area_addrs);
++ isis_tlvs_add_area_addresses(tlvs, &circuit->area->area_addrs);
+
+ if (circuit->circ_type == CIRCUIT_T_BROADCAST) {
+ isis_tlvs_add_lan_neighbors(
+diff --git a/isisd/isis_snmp.c b/isisd/isis_snmp.c
+index 24320e9ed9..624890d5ad 100644
+--- a/isisd/isis_snmp.c
++++ b/isisd/isis_snmp.c
+@@ -839,7 +839,6 @@ static int isis_snmp_area_addr_lookup_exact(oid *oid_idx, size_t oid_idx_len,
+ size_t addr_len;
+ struct isis_area *area = NULL;
+ struct iso_address *addr = NULL;
+- struct listnode *addr_node;
+ struct isis *isis = isis_lookup_by_vrfid(VRF_DEFAULT);
+
+ if (isis == NULL)
+@@ -861,7 +860,7 @@ static int isis_snmp_area_addr_lookup_exact(oid *oid_idx, size_t oid_idx_len,
+ return 0;
+ }
+
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs, addr_node, addr)) {
++ frr_each (iso_address_list, &area->area_addrs, addr) {
+ if (addr->addr_len != addr_len)
+ continue;
+
+@@ -889,7 +888,6 @@ static int isis_snmp_area_addr_lookup_next(oid *oid_idx, size_t oid_idx_len,
+ struct isis_area *area = NULL;
+ struct iso_address *found_addr = NULL;
+ struct iso_address *addr = NULL;
+- struct listnode *addr_node;
+ struct isis *isis = isis_lookup_by_vrfid(VRF_DEFAULT);
+
+ if (isis == NULL)
+@@ -908,7 +906,7 @@ static int isis_snmp_area_addr_lookup_next(oid *oid_idx, size_t oid_idx_len,
+ if (!res)
+ return 0;
+
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs, addr_node, addr)) {
++ frr_each (iso_address_list, &area->area_addrs, addr) {
+ if (addr->addr_len < addr_len)
+ continue;
+
+diff --git a/isisd/isis_te.c b/isisd/isis_te.c
+index cf92ae46e9..61fad6e8ee 100644
+--- a/isisd/isis_te.c
++++ b/isisd/isis_te.c
+@@ -762,6 +762,17 @@ static struct ls_vertex *lsp_to_vertex(struct ls_ted *ted, struct isis_lsp *lsp)
+ vertex->status == NEW ? "Create" : "Found", vertex->key,
+ print_sys_hostname(old->adv.id.iso.sys_id));
+
++ if (iso_address_list_count(&lsp->area->area_addrs)) {
++ struct iso_address *addr = iso_address_list_first(&lsp->area->area_addrs);
++
++ lnode.isis_area_id_len = addr->addr_len;
++ memcpy(lnode.isis_area_id, addr->area_addr, addr->addr_len);
++ SET_FLAG(lnode.flags, LS_NODE_ISIS_AREA_ID);
++ if (iso_address_list_count(&lsp->area->area_addrs) > 1)
++ zlog_warn("%s: Only one IS-IS Area ID is supported, ignoring others",
++ __func__);
++ }
++
+ /* Fulfill Link State Node information */
+ tlvs = lsp->tlvs;
+ if (tlvs) {
+diff --git a/isisd/isis_tlvs.c b/isisd/isis_tlvs.c
+index d29d917145..0e0d4cfd60 100644
+--- a/isisd/isis_tlvs.c
++++ b/isisd/isis_tlvs.c
+@@ -7461,15 +7461,12 @@ void isis_tlvs_add_auth(struct isis_tlvs *tlvs, struct isis_passwd *passwd)
+ append_item(&tlvs->isis_auth, (struct isis_item *)auth);
+ }
+
+-void isis_tlvs_add_area_addresses(struct isis_tlvs *tlvs,
+- struct list *addresses)
++void isis_tlvs_add_area_addresses(struct isis_tlvs *tlvs, struct iso_address_list_head *addresses)
+ {
+- struct listnode *node;
+ struct iso_address *area_addr;
+
+- for (ALL_LIST_ELEMENTS_RO(addresses, node, area_addr)) {
+- struct isis_area_address *a =
+- XCALLOC(MTYPE_ISIS_TLV, sizeof(*a));
++ frr_each (iso_address_list, addresses, area_addr) {
++ struct isis_area_address *a = XCALLOC(MTYPE_ISIS_TLV, sizeof(*a));
+
+ a->len = area_addr->addr_len;
+ memcpy(a->addr, area_addr->area_addr, ISO_ADDR_SIZE);
+@@ -7660,20 +7657,17 @@ int isis_tlvs_auth_is_valid(struct isis_tlvs *tlvs, struct isis_passwd *passwd,
+ return ISIS_AUTH_FAILURE;
+ }
+
+-bool isis_tlvs_area_addresses_match(struct isis_tlvs *tlvs,
+- struct list *addresses)
++bool isis_tlvs_area_addresses_match(struct isis_tlvs *tlvs, struct iso_address_list_head *addresses)
+ {
+ struct isis_area_address *addr_head;
+
+ addr_head = (struct isis_area_address *)tlvs->area_addresses.head;
+- for (struct isis_area_address *addr = addr_head; addr;
+- addr = addr->next) {
+- struct listnode *node;
++ for (struct isis_area_address *addr = addr_head; addr; addr = addr->next) {
+ struct iso_address *a;
+
+- for (ALL_LIST_ELEMENTS_RO(addresses, node, a)) {
+- if (a->addr_len == addr->len
+- && !memcmp(a->area_addr, addr->addr, addr->len))
++ frr_each (iso_address_list, addresses, a) {
++ if (a->addr_len == addr->len &&
++ !memcmp(a->area_addr, addr->addr, addr->len))
+ return true;
+ }
+ }
+diff --git a/isisd/isis_tlvs.h b/isisd/isis_tlvs.h
+index 5798d318f2..f1bcde0830 100644
+--- a/isisd/isis_tlvs.h
++++ b/isisd/isis_tlvs.h
+@@ -17,7 +17,7 @@
+ #include "prefix.h"
+ #include "flex_algo.h"
+ #include "affinitymap.h"
+-
++#include "lib/iso.h"
+
+ #include "lib/srv6.h"
+
+@@ -778,14 +778,11 @@ struct list *isis_fragment_tlvs(struct isis_tlvs *tlvs, size_t size);
+ #endif
+
+ void isis_tlvs_add_auth(struct isis_tlvs *tlvs, struct isis_passwd *passwd);
+-void isis_tlvs_add_area_addresses(struct isis_tlvs *tlvs,
+- struct list *addresses);
+-void isis_tlvs_add_lan_neighbors(struct isis_tlvs *tlvs,
+- struct list *neighbors);
+-void isis_tlvs_set_protocols_supported(struct isis_tlvs *tlvs,
+- struct nlpids *nlpids);
+-void isis_tlvs_add_mt_router_info(struct isis_tlvs *tlvs, uint16_t mtid,
+- bool overload, bool attached);
++void isis_tlvs_add_area_addresses(struct isis_tlvs *tlvs, struct iso_address_list_head *addresses);
++void isis_tlvs_add_lan_neighbors(struct isis_tlvs *tlvs, struct list *neighbors);
++void isis_tlvs_set_protocols_supported(struct isis_tlvs *tlvs, struct nlpids *nlpids);
++void isis_tlvs_add_mt_router_info(struct isis_tlvs *tlvs, uint16_t mtid, bool overload,
++ bool attached);
+ void isis_tlvs_add_ipv4_address(struct isis_tlvs *tlvs, struct in_addr *addr);
+ void isis_tlvs_add_ipv4_addresses(struct isis_tlvs *tlvs,
+ struct list *addresses);
+@@ -796,7 +793,7 @@ void isis_tlvs_add_global_ipv6_addresses(struct isis_tlvs *tlvs,
+ int isis_tlvs_auth_is_valid(struct isis_tlvs *tlvs, struct isis_passwd *passwd,
+ struct stream *stream, bool is_lsp);
+ bool isis_tlvs_area_addresses_match(struct isis_tlvs *tlvs,
+- struct list *addresses);
++ struct iso_address_list_head *addresses);
+ struct isis_adjacency;
+ void isis_tlvs_to_adj(struct isis_tlvs *tlvs, struct isis_adjacency *adj,
+ bool *changed);
+diff --git a/isisd/isis_zebra.c b/isisd/isis_zebra.c
+index 9389d6665a..2b8bc73eda 100644
+--- a/isisd/isis_zebra.c
++++ b/isisd/isis_zebra.c
+@@ -68,7 +68,7 @@ static int isis_router_id_update_zebra(ZAPI_CALLBACK_ARGS)
+
+ isis->router_id = router_id.u.prefix4.s_addr;
+ for (ALL_LIST_ELEMENTS_RO(isis->area_list, node, area))
+- if (listcount(area->area_addrs) > 0)
++ if (iso_address_list_count(&area->area_addrs) > 0)
+ lsp_regenerate_schedule(area, area->is_type, 0);
+
+ return 0;
+diff --git a/isisd/isisd.c b/isisd/isisd.c
+index 78fbbe13b1..0de8793a50 100644
+--- a/isisd/isisd.c
++++ b/isisd/isisd.c
+@@ -353,8 +353,7 @@ struct isis_area *isis_area_create(const char *area_tag, const char *vrf_name)
+
+ area->circuit_list = list_new();
+ area->adjacency_list = list_new();
+- area->area_addrs = list_new();
+- area->area_addrs->del = isis_area_address_delete;
++ iso_address_list_init(&area->area_addrs);
+
+ if (!CHECK_FLAG(im->options, F_ISIS_UNIT_TEST))
+ event_add_timer(master, lsp_tick, area, 1, &area->t_tick);
+@@ -507,8 +506,8 @@ struct isis_area *isis_area_lookup_by_sysid(const uint8_t *sysid)
+ return NULL;
+
+ for (ALL_LIST_ELEMENTS_RO(isis->area_list, node, area)) {
+- if (listcount(area->area_addrs) > 0) {
+- addr = listgetdata(listhead(area->area_addrs));
++ addr = iso_address_list_first(&area->area_addrs);
++ if (addr != NULL) {
+ if (!memcmp(addr->area_addr + addr->addr_len, sysid,
+ ISIS_SYS_ID_LEN))
+ return area;
+@@ -593,8 +592,8 @@ void isis_area_destroy(struct isis_area *area)
+ if (!CHECK_FLAG(im->options, F_ISIS_UNIT_TEST))
+ isis_redist_area_finish(area);
+
+- if (listcount(area->area_addrs) > 0) {
+- addr = listgetdata(listhead(area->area_addrs));
++ addr = iso_address_list_first(&area->area_addrs);
++ if (addr != NULL) {
+ if (!memcmp(addr->area_addr + addr->addr_len, area->isis->sysid,
+ ISIS_SYS_ID_LEN)) {
+ memset(area->isis->sysid, 0, ISIS_SYS_ID_LEN);
+@@ -602,7 +601,9 @@ void isis_area_destroy(struct isis_area *area)
+ }
+ }
+
+- list_delete(&area->area_addrs);
++ while ((addr = iso_address_list_pop(&area->area_addrs)))
++ XFREE(MTYPE_ISIS_AREA_ADDR, addr);
++ iso_address_list_fini(&area->area_addrs);
+
+ for (int i = SPF_PREFIX_PRIO_CRITICAL; i <= SPF_PREFIX_PRIO_MEDIUM;
+ i++) {
+@@ -884,12 +885,11 @@ int area_net_title(struct vty *vty, const char *net_title)
+ VTY_DECLVAR_CONTEXT(isis_area, area);
+ struct iso_address *addr;
+ struct iso_address *addrp;
+- struct listnode *node;
+
+ uint8_t buff[255];
+
+ /* We check that we are not over the maximal number of addresses */
+- if (listcount(area->area_addrs) >= area->isis->max_area_addrs) {
++ if (iso_address_list_count(&area->area_addrs) >= area->isis->max_area_addrs) {
+ vty_out(vty,
+ "Maximum of area addresses (%d) already reached \n",
+ area->isis->max_area_addrs);
+@@ -940,7 +940,7 @@ int area_net_title(struct vty *vty, const char *net_title)
+ }
+
+ /* now we see that we don't already have this address */
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs, node, addrp)) {
++ frr_each (iso_address_list, &area->area_addrs, addrp) {
+ if ((addrp->addr_len + ISIS_SYS_ID_LEN + ISIS_NSEL_LEN)
+ != (addr->addr_len))
+ continue;
+@@ -956,10 +956,10 @@ int area_net_title(struct vty *vty, const char *net_title)
+ * Forget the systemID part of the address
+ */
+ addr->addr_len -= (ISIS_SYS_ID_LEN + ISIS_NSEL_LEN);
+- listnode_add(area->area_addrs, addr);
++ iso_address_list_add_tail(&area->area_addrs, addr);
+
+ /* only now we can safely generate our LSPs for this area */
+- if (listcount(area->area_addrs) > 0) {
++ if (iso_address_list_count(&area->area_addrs) > 0) {
+ if (area->is_type & IS_LEVEL_1)
+ lsp_generate(area, IS_LEVEL_1);
+ if (area->is_type & IS_LEVEL_2)
+@@ -973,7 +973,6 @@ int area_clear_net_title(struct vty *vty, const char *net_title)
+ {
+ VTY_DECLVAR_CONTEXT(isis_area, area);
+ struct iso_address addr, *addrp = NULL;
+- struct listnode *node;
+ uint8_t buff[255];
+
+ addr.addr_len = dotformat2buff(buff, net_title);
+@@ -986,7 +985,7 @@ int area_clear_net_title(struct vty *vty, const char *net_title)
+
+ memcpy(addr.area_addr, buff, (int)addr.addr_len);
+
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs, node, addrp))
++ frr_each (iso_address_list, &area->area_addrs, addrp)
+ if ((addrp->addr_len + ISIS_SYS_ID_LEN + 1) == addr.addr_len
+ && !memcmp(addrp->area_addr, addr.area_addr, addr.addr_len))
+ break;
+@@ -997,13 +996,13 @@ int area_clear_net_title(struct vty *vty, const char *net_title)
+ return CMD_ERR_NO_MATCH;
+ }
+
+- listnode_delete(area->area_addrs, addrp);
++ iso_address_list_del(&area->area_addrs, addrp);
+ XFREE(MTYPE_ISIS_AREA_ADDR, addrp);
+
+ /*
+ * Last area address - reset the SystemID for this router
+ */
+- if (listcount(area->area_addrs) == 0) {
++ if (iso_address_list_count(&area->area_addrs) == 0) {
+ memset(area->isis->sysid, 0, ISIS_SYS_ID_LEN);
+ area->isis->sysid_set = 0;
+ if (IS_DEBUG_EVENTS)
+@@ -2388,7 +2387,7 @@ static void common_isis_summary_json(struct json_object *json,
+ int level;
+ json_object *vrf_json, *areas_json, *area_json, *tx_pdu_json, *rx_pdu_json, *levels_json,
+ *level_json;
+- struct listnode *node, *node2;
++ struct listnode *node;
+ struct isis_area *area;
+ time_t cur;
+ char uptime[MONOTIME_STRLEN];
+@@ -2423,10 +2422,9 @@ static void common_isis_summary_json(struct json_object *json,
+ : stier);
+ }
+
+- if (listcount(area->area_addrs) > 0) {
++ {
+ struct iso_address *area_addr;
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs, node2,
+- area_addr))
++ frr_each (iso_address_list, &area->area_addrs, area_addr)
+ json_object_string_addf(area_json, "net",
+ "%pISl", area_addr);
+ }
+@@ -2495,7 +2493,7 @@ static void common_isis_summary_json(struct json_object *json,
+
+ static void common_isis_summary_vty(struct vty *vty, struct isis *isis)
+ {
+- struct listnode *node, *node2;
++ struct listnode *node;
+ struct isis_area *area;
+ int level;
+
+@@ -2523,10 +2521,9 @@ static void common_isis_summary_vty(struct vty *vty, struct isis *isis)
+ vty_out(vty, " Tier: %hhu\n", tier);
+ }
+
+- if (listcount(area->area_addrs) > 0) {
++ if (iso_address_list_count(&area->area_addrs) > 0) {
+ struct iso_address *area_addr;
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs, node2,
+- area_addr))
++ frr_each (iso_address_list, &area->area_addrs, area_addr)
+ vty_out(vty, " Net: %pISl\n", area_addr);
+ }
+
+@@ -3275,7 +3272,7 @@ void isis_area_is_type_set(struct isis_area *area, int is_type)
+
+ spftree_area_init(area);
+
+- if (listcount(area->area_addrs) > 0) {
++ if (iso_address_list_count(&area->area_addrs) > 0) {
+ if (is_type & IS_LEVEL_1)
+ lsp_generate(area, IS_LEVEL_1);
+ if (is_type & IS_LEVEL_2)
+@@ -3332,7 +3329,7 @@ void isis_area_overload_on_startup_set(struct isis_area *area,
+
+ void config_end_lsp_generate(struct isis_area *area)
+ {
+- if (listcount(area->area_addrs) > 0) {
++ if (iso_address_list_count(&area->area_addrs) > 0) {
+ if (CHECK_FLAG(area->is_type, IS_LEVEL_1))
+ lsp_generate(area, IS_LEVEL_1);
+ if (CHECK_FLAG(area->is_type, IS_LEVEL_2))
+@@ -3548,7 +3545,7 @@ static int isis_config_write(struct vty *vty)
+ {
+ int write = 0;
+ struct isis_area *area;
+- struct listnode *node, *node2, *inode;
++ struct listnode *node, *inode;
+ struct isis *isis;
+
+ if (!im) {
+@@ -3562,10 +3559,9 @@ static int isis_config_write(struct vty *vty)
+ vty_out(vty, "router " PROTO_NAME " %s\n", area->area_tag);
+ write++;
+ /* ISIS - Net */
+- if (listcount(area->area_addrs) > 0) {
++ if (iso_address_list_count(&area->area_addrs) > 0) {
+ struct iso_address *area_addr;
+- for (ALL_LIST_ELEMENTS_RO(area->area_addrs,
+- node2, area_addr)) {
++ frr_each (iso_address_list, &area->area_addrs, area_addr) {
+ vty_out(vty, " net %pISl\n", area_addr);
+ write++;
+ }
+diff --git a/isisd/isisd.h b/isisd/isisd.h
+index cd50d43a18..f43164da4d 100644
+--- a/isisd/isisd.h
++++ b/isisd/isisd.h
+@@ -174,7 +174,7 @@ struct isis_area {
+ /* identifies the routing instance */
+ char *area_tag;
+ /* area addresses for this area */
+- struct list *area_addrs;
++ struct iso_address_list_head area_addrs;
+ uint16_t max_lsp_lifetime[ISIS_LEVELS];
+ char is_type; /* level-1 level-1-2 or level-2-only */
+ /* are we overloaded? */
+diff --git a/lib/command.h b/lib/command.h
+index d5119fd97c..ce3d86b659 100644
+--- a/lib/command.h
++++ b/lib/command.h
+@@ -120,6 +120,7 @@ enum node_type {
+ BGP_VNC_L2_GROUP_NODE, /* BGP VNC L2 group */
+ BGP_EVPN_NODE, /* BGP EVPN node. */
+ BGP_SRV6_NODE, /* BGP SRv6 node. */
++ BGP_LS_NODE, /* BGP Link-State node. */
+ OSPF_NODE, /* OSPF protocol mode */
+ OSPF6_NODE, /* OSPF protocol for IPv6 mode */
+ LDP_NODE, /* LDP protocol mode */
+diff --git a/lib/iana_afi.h b/lib/iana_afi.h
+index b9c19cc3d5..4d4687c4c4 100644
+--- a/lib/iana_afi.h
++++ b/lib/iana_afi.h
+@@ -26,6 +26,7 @@ typedef enum {
+ IANA_AFI_IPV4 = 1,
+ IANA_AFI_IPV6 = 2,
+ IANA_AFI_L2VPN = 25,
++ IANA_AFI_BGP_LS = 16388, /* BGP-LS per RFC 9552 */
+ } iana_afi_t;
+
+ typedef enum {
+@@ -35,6 +36,7 @@ typedef enum {
+ IANA_SAFI_LABELED_UNICAST = 4,
+ IANA_SAFI_ENCAP = 7,
+ IANA_SAFI_EVPN = 70,
++ IANA_SAFI_BGP_LS = 71, /* BGP-LS per RFC 9552 */
+ IANA_SAFI_MPLS_VPN = 128,
+ IANA_SAFI_FLOWSPEC = 133
+ } iana_safi_t;
+@@ -48,6 +50,8 @@ static inline afi_t afi_iana2int(iana_afi_t afi)
+ return AFI_IP6;
+ case IANA_AFI_L2VPN:
+ return AFI_L2VPN;
++ case IANA_AFI_BGP_LS:
++ return AFI_BGP_LS;
+ case IANA_AFI_RESERVED:
+ return AFI_MAX;
+ }
+@@ -64,6 +68,8 @@ static inline iana_afi_t afi_int2iana(afi_t afi)
+ return IANA_AFI_IPV6;
+ case AFI_L2VPN:
+ return IANA_AFI_L2VPN;
++ case AFI_BGP_LS:
++ return IANA_AFI_BGP_LS;
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ return IANA_AFI_RESERVED;
+@@ -94,6 +100,8 @@ static inline safi_t safi_iana2int(iana_safi_t safi)
+ return SAFI_LABELED_UNICAST;
+ case IANA_SAFI_FLOWSPEC:
+ return SAFI_FLOWSPEC;
++ case IANA_SAFI_BGP_LS:
++ return SAFI_BGP_LS;
+ case IANA_SAFI_RESERVED:
+ return SAFI_MAX;
+ }
+@@ -118,6 +126,8 @@ static inline iana_safi_t safi_int2iana(safi_t safi)
+ return IANA_SAFI_LABELED_UNICAST;
+ case SAFI_FLOWSPEC:
+ return IANA_SAFI_FLOWSPEC;
++ case SAFI_BGP_LS:
++ return IANA_SAFI_BGP_LS;
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+ return IANA_SAFI_RESERVED;
+diff --git a/lib/iso.h b/lib/iso.h
+index bdc9d39031..8e9df8cee6 100644
+--- a/lib/iso.h
++++ b/lib/iso.h
+@@ -11,16 +11,26 @@
+ #define LIB_ISO_H_
+
+ #include "compiler.h"
++#include "typesafe.h"
+
+ /* len of "xx.xxxx.xxxx.xxxx.xxxx.xxxx.xxxx.xxxx.xxxx.xxxx.xx" + '\0' */
+ #define ISO_ADDR_STRLEN 51
+ #define ISO_ADDR_MIN 8
+ #define ISO_ADDR_SIZE 20
++
++/* Predeclare typesafe list for isis_area->area_addrs */
++PREDECL_DLIST(iso_address_list);
++
+ struct iso_address {
+ uint8_t addr_len;
+ uint8_t area_addr[ISO_ADDR_SIZE];
++
++ /* Typesafe list linkage for isis_area->area_addrs */
++ struct iso_address_list_item item;
+ };
+
++DECLARE_DLIST(iso_address_list, struct iso_address, item);
++
+ /* len of "xxxx.xxxx.xxxx.xx-xx" + '\0' */
+ #define ISO_SYSID_STRLEN 21
+
+diff --git a/lib/link_state.c b/lib/link_state.c
+index 3603407edb..8a9acc51ee 100644
+--- a/lib/link_state.c
++++ b/lib/link_state.c
+@@ -146,6 +146,12 @@ int ls_node_same(struct ls_node *n1, struct ls_node *n2)
+ if (memcmp(&n1->srv6_msd, &n2->srv6_msd, sizeof(n1->srv6_msd)))
+ return 0;
+ }
++ if (CHECK_FLAG(n1->flags, LS_NODE_ISIS_AREA_ID)) {
++ if (n1->isis_area_id_len != n2->isis_area_id_len)
++ return 0;
++ if (memcmp(n1->isis_area_id, n2->isis_area_id, n1->isis_area_id_len))
++ return 0;
++ }
+
+ /* OK, n1 & n2 are equal */
+ return 1;
+@@ -1218,6 +1224,15 @@ static struct ls_node *ls_parse_node(struct stream *s)
+ }
+ if (CHECK_FLAG(node->flags, LS_NODE_MSD))
+ STREAM_GETC(s, node->msd);
++ if (CHECK_FLAG(node->flags, LS_NODE_ISIS_AREA_ID)) {
++ STREAM_GETC(s, node->isis_area_id_len);
++ if (node->isis_area_id_len > ISO_ADDR_SIZE) {
++ zlog_err("LS(%s): IS-IS Area ID length exceeds maximum size (%d)",
++ __func__, node->isis_area_id_len);
++ goto stream_failure;
++ }
++ STREAM_GET(node->isis_area_id, s, node->isis_area_id_len);
++ }
+
+ return node;
+
+@@ -1485,6 +1500,10 @@ static int ls_format_node(struct stream *s, struct ls_node *node)
+ }
+ if (CHECK_FLAG(node->flags, LS_NODE_MSD))
+ stream_putc(s, node->msd);
++ if (CHECK_FLAG(node->flags, LS_NODE_ISIS_AREA_ID)) {
++ stream_putc(s, node->isis_area_id_len);
++ stream_put(s, node->isis_area_id, node->isis_area_id_len);
++ }
+
+ return 0;
+ }
+@@ -2114,6 +2133,14 @@ static void ls_show_vertex_vty(struct ls_vertex *vertex, struct vty *vty,
+ sbuf_push(&sbuf, 4, "Type: %s\n", type2txt[lsn->type]);
+ if (CHECK_FLAG(lsn->flags, LS_NODE_AS_NUMBER))
+ sbuf_push(&sbuf, 4, "AS number: %u\n", lsn->as_number);
++ if (CHECK_FLAG(lsn->flags, LS_NODE_ISIS_AREA_ID)) {
++ struct iso_address area_addr;
++
++ area_addr.addr_len = lsn->isis_area_id_len;
++ memcpy(area_addr.area_addr, lsn->isis_area_id, lsn->isis_area_id_len);
++
++ sbuf_push(&sbuf, 4, "IS-IS Area ID: %pIS\n", &area_addr);
++ }
+ if (CHECK_FLAG(lsn->flags, LS_NODE_SR)) {
+ sbuf_push(&sbuf, 4, "Segment Routing Capabilities:\n");
+ upper = lsn->srgb.lower_bound + lsn->srgb.range_size - 1;
+@@ -2246,6 +2273,13 @@ static void ls_show_vertex_json(struct ls_vertex *vertex,
+ if (CHECK_FLAG(lsn->flags, LS_NODE_MSD))
+ json_object_int_add(jsr, "msd", lsn->msd);
+ }
++ if (CHECK_FLAG(lsn->flags, LS_NODE_ISIS_AREA_ID)) {
++ struct iso_address area_addr;
++
++ area_addr.addr_len = lsn->isis_area_id_len;
++ memcpy(area_addr.area_addr, lsn->isis_area_id, lsn->isis_area_id_len);
++ json_object_string_addf(json, "isis-area-id", "%pIS", &area_addr);
++ }
+ }
+
+ void ls_show_vertex(struct ls_vertex *vertex, struct vty *vty,
+diff --git a/lib/link_state.h b/lib/link_state.h
+index 7169926a1a..14b8deef2d 100644
+--- a/lib/link_state.h
++++ b/lib/link_state.h
+@@ -48,6 +48,7 @@ struct zclient;
+ /* Link State Common definitions */
+ #define MAX_NAME_LENGTH 256
+ #define ISO_SYS_ID_LEN 6
++#define ISO_ADDR_SIZE 20
+
+ /* Type of Node */
+ enum ls_node_type {
+@@ -107,12 +108,15 @@ extern int ls_node_id_same(struct ls_node_id i1, struct ls_node_id i2);
+ #define LS_NODE_SRLB 0x0080
+ #define LS_NODE_MSD 0x0100
+ #define LS_NODE_SRV6 0x0200
++#define LS_NODE_ISIS_AREA_ID 0x0400
+
+ /* Link State Node structure */
+ struct ls_node {
+ uint16_t flags; /* Flag for parameters validity */
+ struct ls_node_id adv; /* Adv. Router of this Link State */
+ char name[MAX_NAME_LENGTH]; /* Name of the Node (IS-IS only) */
++ uint8_t isis_area_id_len; /* Length of isis_area_id data */
++ uint8_t isis_area_id[ISO_ADDR_SIZE]; /* IS-IS Area ID */
+ struct in_addr router_id; /* IPv4 Router ID */
+ struct in6_addr router_id6; /* IPv6 Router ID */
+ uint8_t node_flag; /* IS-IS or OSPF Node flag */
+diff --git a/lib/prefix.c b/lib/prefix.c
+index 40277f0609..17a3129df6 100644
+--- a/lib/prefix.c
++++ b/lib/prefix.c
+@@ -132,6 +132,8 @@ const char *afi2str_lower(afi_t afi)
+ return "ipv6";
+ case AFI_L2VPN:
+ return "l2vpn";
++ case AFI_BGP_LS:
++ return "bgp-ls";
+ case AFI_MAX:
+ case AFI_UNSPEC:
+ return "bad-value";
+@@ -150,6 +152,8 @@ const char *afi2str(afi_t afi)
+ return "IPv6";
+ case AFI_L2VPN:
+ return "l2vpn";
++ case AFI_BGP_LS:
++ return "BGP-LS";
+ case AFI_MAX:
+ case AFI_UNSPEC:
+ return "bad-value";
+@@ -176,6 +180,8 @@ const char *safi2str(safi_t safi)
+ return "labeled-unicast";
+ case SAFI_FLOWSPEC:
+ return "flowspec";
++ case SAFI_BGP_LS:
++ return "bgp-ls";
+ case SAFI_UNSPEC:
+ case SAFI_MAX:
+ return "unknown";
+diff --git a/lib/zebra.h b/lib/zebra.h
+index 46d3c2f660..ca137058b7 100644
+--- a/lib/zebra.h
++++ b/lib/zebra.h
+@@ -167,7 +167,8 @@ typedef enum {
+ AFI_IP = 1,
+ AFI_IP6 = 2,
+ AFI_L2VPN = 3,
+- AFI_MAX = 4
++ AFI_BGP_LS = 4, /* BGP-LS (RFC 9552) */
++ AFI_MAX = 5
+ } afi_t;
+
+ #define IS_VALID_AFI(a) ((a) > AFI_UNSPEC && (a) < AFI_MAX)
+@@ -182,7 +183,8 @@ typedef enum {
+ SAFI_EVPN = 5,
+ SAFI_LABELED_UNICAST = 6,
+ SAFI_FLOWSPEC = 7,
+- SAFI_MAX = 8
++ SAFI_BGP_LS = 8, /* BGP-LS (RFC 9552) */
++ SAFI_MAX = 9
+ } safi_t;
+
+ #define FOREACH_AFI_SAFI(afi, safi) \
+diff --git a/pbrd/pbr_zebra.c b/pbrd/pbr_zebra.c
+index c8b5c1e814..e1cd2f038e 100644
+--- a/pbrd/pbr_zebra.c
++++ b/pbrd/pbr_zebra.c
+@@ -311,6 +311,10 @@ void route_add(struct pbr_nexthop_group_cache *pnhgc, struct nexthop_group nhg,
+ case AFI_IP6:
+ route_add_helper(&api, nhg, AF_INET6);
+ break;
++ case AFI_BGP_LS:
++ DEBUGD(&pbr_dbg_zebra, "%s: Asked to install unsupported route type: BGP-LS",
++ __func__);
++ break;
+ case AFI_L2VPN:
+ DEBUGD(&pbr_dbg_zebra,
+ "%s: Asked to install unsupported route type: L2VPN",
+@@ -356,6 +360,10 @@ void route_delete(struct pbr_nexthop_group_cache *pnhgc, afi_t afi)
+ api.prefix.family = AF_INET6;
+ zclient_route_send(ZEBRA_ROUTE_DELETE, pbr_zclient, &api);
+ break;
++ case AFI_BGP_LS:
++ DEBUGD(&pbr_dbg_zebra, "%s: Asked to delete unsupported route type: BGP-LS",
++ __func__);
++ break;
+ case AFI_L2VPN:
+ DEBUGD(&pbr_dbg_zebra,
+ "%s: Asked to delete unsupported route type: L2VPN",
+diff --git a/staticd/static_vty.c b/staticd/static_vty.c
+index 837a65bcb7..e7362570e4 100644
+--- a/staticd/static_vty.c
++++ b/staticd/static_vty.c
+@@ -138,6 +138,7 @@ static int static_route_nb_run(struct vty *vty, struct static_route_args *args)
+ assert(!!str2prefix(args->source, &src));
+ break;
+ case AFI_L2VPN:
++ case AFI_BGP_LS:
+ case AFI_UNSPEC:
+ case AFI_MAX:
+ break;
+diff --git a/tests/bgpd/test_peer_attr.c b/tests/bgpd/test_peer_attr.c
+index ec2ea88282..fecaaa2d39 100644
+--- a/tests/bgpd/test_peer_attr.c
++++ b/tests/bgpd/test_peer_attr.c
+@@ -663,6 +663,8 @@ static const char *str_from_afi(afi_t afi)
+ return "ipv6";
+ case AFI_L2VPN:
+ return "l2vpn";
++ case AFI_BGP_LS:
++ return "bgp-ls";
+ case AFI_MAX:
+ case AFI_UNSPEC:
+ return "bad-value";
+diff --git a/tests/isisd/test_isis_spf.c b/tests/isisd/test_isis_spf.c
+index bbafe7204e..d834a8de57 100644
+--- a/tests/isisd/test_isis_spf.c
++++ b/tests/isisd/test_isis_spf.c
+@@ -261,7 +261,7 @@ static int test_run(struct vty *vty, const struct isis_topology *topology,
+ addr->addr_len = dotformat2buff(buff, net_title);
+ memcpy(addr->area_addr, buff, addr->addr_len);
+ addr->addr_len -= (ISIS_SYS_ID_LEN + ISIS_NSEL_LEN);
+- listnode_add(area->area_addrs, addr);
++ iso_address_list_add_tail(&area->area_addrs, addr);
+ if (test_topology_load(topology, area, area->lspdb) != 0) {
+ vty_out(vty, "%% Failed to load topology\n");
+ return CMD_WARNING;
+diff --git a/tests/topotests/bgp_link_state/__init__.py b/tests/topotests/bgp_link_state/__init__.py
+new file mode 100644
+index 0000000000..e69de29bb2
+diff --git a/tests/topotests/bgp_link_state/r1/frr.conf b/tests/topotests/bgp_link_state/r1/frr.conf
+new file mode 100644
+index 0000000000..70f46ed496
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r1/frr.conf
+@@ -0,0 +1,57 @@
++
++hostname r1
++!
++!debug isis te-events
++!
++interface lo
++ ip address 1.1.1.1/32
++ ipv6 address fc00:0:1::1/128
++ ip router isis 1
++ ipv6 router isis 1
++ isis passive
++!
++interface r1-eth0
++ ip address 10.0.1.1/24
++ ipv6 address fc00:10:0:1::1/64
++ link-params
++ neighbor 10.0.255.2
++ ip router isis 1
++ ipv6 router isis 1
++ isis circuit-type level-2
++ isis hello-interval 1
++ isis hello-multiplier 3
++ isis network point-to-point
++!
++interface r1-eth1
++ ip address 10.0.4.1/24
++ ipv6 address fc00:10:0:4::1/64
++ link-params
++ neighbor 10.0.255.4
++ ip router isis 1
++ ipv6 router isis 1
++ isis circuit-type level-2
++ isis hello-interval 1
++ isis hello-multiplier 3
++ isis network point-to-point
++!
++router isis 1
++ net 49.0000.0000.0000.0001.00
++ is-type level-2-only
++ topology ipv6-unicast
++ redistribute ipv4 static level-2
++ redistribute ipv6 static level-2
++ redistribute ipv4 connected level-2
++ redistribute ipv6 connected level-2
++ lsp-gen-interval 1
++ lsp-refresh-interval 10
++ max-lsp-lifetime 20
++ mpls-te on
++ mpls-te router-address 1.1.1.1
++ mpls-te router-address ipv6 fc00:0:1::1
++ segment-routing on
++ segment-routing node-msd 7
++ segment-routing global-block 100000 100999
++ segment-routing prefix 1.1.1.1/32 index 1
++!
++ip forwarding
++!
+diff --git a/tests/topotests/bgp_link_state/r1/isis_adj.json b/tests/topotests/bgp_link_state/r1/isis_adj.json
+new file mode 100644
+index 0000000000..03b7d29a04
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r1/isis_adj.json
+@@ -0,0 +1,18 @@
++{
++ "areas": [
++ {
++ "circuits": [
++ {
++ "adj": "r2",
++ "interface": "r1-eth0",
++ "state": "Up"
++ },
++ {
++ "adj": "r4",
++ "interface": "r1-eth1",
++ "state": "Up"
++ }
++ ]
++ }
++ ]
++}
+diff --git a/tests/topotests/bgp_link_state/r2/bgp_capability.json b/tests/topotests/bgp_link_state/r2/bgp_capability.json
+new file mode 100644
+index 0000000000..720bc7cc70
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r2/bgp_capability.json
+@@ -0,0 +1,11 @@
++{
++ "10.0.3.4": {
++ "neighborCapabilities": {
++ "multiprotocolExtensions": {
++ "linkState": {
++ "advertisedAndReceived": true
++ }
++ }
++ }
++ }
++}
+diff --git a/tests/topotests/bgp_link_state/r2/bgp_ls_nlri.json b/tests/topotests/bgp_link_state/r2/bgp_ls_nlri.json
+new file mode 100644
+index 0000000000..dee6768b71
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r2/bgp_ls_nlri.json
+@@ -0,0 +1,1507 @@
++{
++ "vrfId": 0,
++ "vrfName": "default",
++ "routerId": "2.2.2.2",
++ "routes": {
++ "[V][L2][I0x0][N[s0000.0000.0002]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "node",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ }
++ },
++ "nlriStr": "[V][L2][I0x0][N[s0000.0000.0002]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.1.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.1.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.1.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.2.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.2.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.2.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[p2.2.2.2/32]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "2.2.2.2/32"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[p2.2.2.2/32]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:0:2::1/128]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:0:2::1/128"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:0:2::1/128]]"
++ }
++ ],
++ "[V][L2][I0x0][N[s0000.0000.0001]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "node",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ }
++ },
++ "nlriStr": "[V][L2][I0x0][N[s0000.0000.0001]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[p1.1.1.1/32]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "1.1.1.1/32"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[p1.1.1.1/32]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[p10.0.4.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.4.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[p10.0.4.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:0:1::1/128]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:0:1::1/128"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:0:1::1/128]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:10:0:1::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:1::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:10:0:1::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:10:0:4::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:4::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:10:0:4::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.3.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.3.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.3.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[p10.0.1.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.1.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[p10.0.1.0/24]]"
++ }
++ ],
++ "[V][L2][I0x0][N[s0000.0000.0003]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "node",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ }
++ },
++ "nlriStr": "[V][L2][I0x0][N[s0000.0000.0003]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[p3.3.3.3/32]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "3.3.3.3/32"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[p3.3.3.3/32]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[p10.0.5.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.5.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[p10.0.5.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:0:3::1/128]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:0:3::1/128"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:0:3::1/128]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:10:0:1::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:1::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:10:0:1::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:10:0:2::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:2::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:10:0:2::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[p10.0.2.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.2.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[p10.0.2.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:10:0:5::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:5::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:10:0:5::/64]]"
++ }
++ ],
++ "[V][L2][I0x0][N[s0000.0000.0004]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "node",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ }
++ },
++ "nlriStr": "[V][L2][I0x0][N[s0000.0000.0004]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[p4.4.4.4/32]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "4.4.4.4/32"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[p4.4.4.4/32]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:0:4::1/128]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:0:4::1/128"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:0:4::1/128]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:10:0:2::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:2::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:10:0:2::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[p10.0.4.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.4.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[p10.0.4.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[p10.0.5.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.5.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[p10.0.5.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:10:0:5::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:5::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:10:0:5::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:10:0:4::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:4::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:10:0:4::/64]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0002]][L[i10.0.1.1][n10.0.1.2]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.1.1",
++ "ipv4NeighborAddress": "10.0.1.2"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0002]][L[i10.0.1.1][n10.0.1.2]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0001]][L[i10.0.1.2][n10.0.1.1]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.1.2",
++ "ipv4NeighborAddress": "10.0.1.1"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0001]][L[i10.0.1.2][n10.0.1.1]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0003]][L[i10.0.2.2][n10.0.2.3]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.2.2",
++ "ipv4NeighborAddress": "10.0.2.3"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0003]][L[i10.0.2.2][n10.0.2.3]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0002]][L[i10.0.2.3][n10.0.2.2]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.2.3",
++ "ipv4NeighborAddress": "10.0.2.2"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0002]][L[i10.0.2.3][n10.0.2.2]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0004]][L[i10.0.4.1][n10.0.4.4]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.4.1",
++ "ipv4NeighborAddress": "10.0.4.4"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0004]][L[i10.0.4.1][n10.0.4.4]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0001]][L[i10.0.4.4][n10.0.4.1]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.4.4",
++ "ipv4NeighborAddress": "10.0.4.1"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0001]][L[i10.0.4.4][n10.0.4.1]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0004]][L[i10.0.5.3][n10.0.5.4]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.5.3",
++ "ipv4NeighborAddress": "10.0.5.4"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0004]][L[i10.0.5.3][n10.0.5.4]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[i10.0.5.4][n10.0.5.3]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.5.4",
++ "ipv4NeighborAddress": "10.0.5.3"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[i10.0.5.4][n10.0.5.3]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0002]][L[ifc00:10:0:1::1][nfc00:10:0:1::2]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:1::1",
++ "ipv6NeighborAddress": "fc00:10:0:1::2"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0002]][L[ifc00:10:0:1::1][nfc00:10:0:1::2]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0001]][L[ifc00:10:0:1::2][nfc00:10:0:1::1]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:1::2",
++ "ipv6NeighborAddress": "fc00:10:0:1::1"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0001]][L[ifc00:10:0:1::2][nfc00:10:0:1::1]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0003]][L[ifc00:10:0:2::2][nfc00:10:0:2::3]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:2::2",
++ "ipv6NeighborAddress": "fc00:10:0:2::3"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0003]][L[ifc00:10:0:2::2][nfc00:10:0:2::3]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0002]][L[ifc00:10:0:2::3][nfc00:10:0:2::2]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:2::3",
++ "ipv6NeighborAddress": "fc00:10:0:2::2"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0002]][L[ifc00:10:0:2::3][nfc00:10:0:2::2]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0004]][L[ifc00:10:0:4::1][nfc00:10:0:4::4]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:4::1",
++ "ipv6NeighborAddress": "fc00:10:0:4::4"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0004]][L[ifc00:10:0:4::1][nfc00:10:0:4::4]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0001]][L[ifc00:10:0:4::4][nfc00:10:0:4::1]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:4::4",
++ "ipv6NeighborAddress": "fc00:10:0:4::1"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0001]][L[ifc00:10:0:4::4][nfc00:10:0:4::1]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0004]][L[ifc00:10:0:5::3][nfc00:10:0:5::4]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:5::3",
++ "ipv6NeighborAddress": "fc00:10:0:5::4"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0004]][L[ifc00:10:0:5::3][nfc00:10:0:5::4]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[ifc00:10:0:5::4][nfc00:10:0:5::3]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "external",
++ "weight": 32768,
++ "peerId": "(unspec)",
++ "path": "",
++ "origin": "incomplete",
++ "nexthops": [
++ {
++ "ip": "::",
++ "hostname": "r2",
++ "afi": "ipv6",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:5::4",
++ "ipv6NeighborAddress": "fc00:10:0:5::3"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[ifc00:10:0:5::4][nfc00:10:0:5::3]]"
++ }
++ ]
++ }
++}
+\ No newline at end of file
+diff --git a/tests/topotests/bgp_link_state/r2/bgp_neighbor.json b/tests/topotests/bgp_link_state/r2/bgp_neighbor.json
+new file mode 100644
+index 0000000000..d1df9a5102
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r2/bgp_neighbor.json
+@@ -0,0 +1,7 @@
++{
++ "10.0.3.4": {
++ "remoteAs": 65000,
++ "localAs": 65000,
++ "bgpState": "Established"
++ }
++}
+diff --git a/tests/topotests/bgp_link_state/r2/frr.conf b/tests/topotests/bgp_link_state/r2/frr.conf
+new file mode 100644
+index 0000000000..05fe6646b3
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r2/frr.conf
+@@ -0,0 +1,72 @@
++hostname r2
++!
++!debug isis te-events
++!debug bgp link-state
++!debug bgp zebra
++!debug bgp updates
++!
++router bgp 65000
++ bgp router-id 2.2.2.2
++ no bgp ebgp-requires-policy
++ neighbor 10.0.3.4 remote-as 65000
++ !
++ address-family link-state
++ neighbor 10.0.3.4 activate
++ exit-address-family
++!
++interface r2-eth0
++ ip address 10.0.1.2/24
++ ipv6 address fc00:10:0:1::2/64
++ link-params
++ neighbor 10.0.255.1
++ ip router isis 1
++ ipv6 router isis 1
++ isis circuit-type level-2
++ isis hello-interval 1
++ isis hello-multiplier 3
++ isis network point-to-point
++!
++interface r2-eth1
++ ip address 10.0.2.2/24
++ ipv6 address fc00:10:0:2::2/64
++ link-params
++ neighbor 10.0.255.3
++ ip router isis 1
++ ipv6 router isis 1
++ isis circuit-type level-2
++ isis hello-interval 1
++ isis hello-multiplier 3
++ isis network point-to-point
++!
++interface r2-eth2
++ ip address 10.0.3.2/24
++!
++interface lo
++ ip address 2.2.2.2/32
++ ipv6 address fc00:0:2::1/128
++ ip router isis 1
++ ipv6 router isis 1
++ isis passive
++!
++router isis 1
++ net 49.0000.0000.0000.0002.00
++ is-type level-2-only
++ topology ipv6-unicast
++ redistribute ipv4 static level-2
++ redistribute ipv6 static level-2
++ redistribute ipv4 connected level-2
++ redistribute ipv6 connected level-2
++ lsp-gen-interval 1
++ lsp-refresh-interval 10
++ max-lsp-lifetime 20
++ mpls-te on
++ mpls-te export
++ mpls-te router-address 2.2.2.2
++ mpls-te router-address ipv6 fc00:0:2::1
++ segment-routing on
++ segment-routing node-msd 7
++ segment-routing global-block 200000 201999
++ segment-routing prefix 2.2.2.2/32 index 2
++!
++ip forwarding
++!
+diff --git a/tests/topotests/bgp_link_state/r2/isis_adj.json b/tests/topotests/bgp_link_state/r2/isis_adj.json
+new file mode 100644
+index 0000000000..199f548177
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r2/isis_adj.json
+@@ -0,0 +1,18 @@
++{
++ "areas": [
++ {
++ "circuits": [
++ {
++ "adj": "r1",
++ "interface": "r2-eth0",
++ "state": "Up"
++ },
++ {
++ "adj": "r3",
++ "interface": "r2-eth1",
++ "state": "Up"
++ }
++ ]
++ }
++ ]
++}
+diff --git a/tests/topotests/bgp_link_state/r3/frr.conf b/tests/topotests/bgp_link_state/r3/frr.conf
+new file mode 100644
+index 0000000000..e4212eb0d4
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r3/frr.conf
+@@ -0,0 +1,56 @@
++hostname r3
++!
++!debug isis te-events
++!
++interface lo
++ ip address 3.3.3.3/32
++ ipv6 address fc00:0:3::1/128
++ ip router isis 1
++ ipv6 router isis 1
++ isis passive
++!
++interface r3-eth0
++ ip address 10.0.2.3/24
++ ipv6 address fc00:10:0:2::3/64
++ link-params
++ neighbor 10.0.255.2
++ ip router isis 1
++ ipv6 router isis 1
++ isis circuit-type level-2
++ isis hello-interval 1
++ isis hello-multiplier 3
++ isis network point-to-point
++!
++interface r3-eth1
++ ip address 10.0.5.3/24
++ ipv6 address fc00:10:0:5::3/64
++ link-params
++ neighbor 10.0.255.4
++ ip router isis 1
++ ipv6 router isis 1
++ isis circuit-type level-2
++ isis hello-interval 1
++ isis hello-multiplier 3
++ isis network point-to-point
++!
++router isis 1
++ net 49.0000.0000.0000.0003.00
++ is-type level-2-only
++ topology ipv6-unicast
++ redistribute ipv4 static level-2
++ redistribute ipv6 static level-2
++ redistribute ipv4 connected level-2
++ redistribute ipv6 connected level-2
++ lsp-gen-interval 1
++ lsp-refresh-interval 10
++ max-lsp-lifetime 20
++ mpls-te on
++ mpls-te router-address 3.3.3.3
++ mpls-te router-address ipv6 fc00:0:3::1
++ segment-routing on
++ segment-routing node-msd 7
++ segment-routing global-block 300000 302999
++ segment-routing prefix 3.3.3.3/32 index 3
++!
++ip forwarding
++!
+diff --git a/tests/topotests/bgp_link_state/r3/isis_adj.json b/tests/topotests/bgp_link_state/r3/isis_adj.json
+new file mode 100644
+index 0000000000..c6ef44912f
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r3/isis_adj.json
+@@ -0,0 +1,18 @@
++{
++ "areas": [
++ {
++ "circuits": [
++ {
++ "adj": "r2",
++ "interface": "r3-eth0",
++ "state": "Up"
++ },
++ {
++ "adj": "r4",
++ "interface": "r3-eth1",
++ "state": "Up"
++ }
++ ]
++ }
++ ]
++}
+diff --git a/tests/topotests/bgp_link_state/r4/frr.conf b/tests/topotests/bgp_link_state/r4/frr.conf
+new file mode 100644
+index 0000000000..273af2299a
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r4/frr.conf
+@@ -0,0 +1,54 @@
++!
++hostname r4
++!
++interface lo
++ ip address 4.4.4.4/32
++ ipv6 address fc00:0:4::1/128
++ ip router isis 1
++ ipv6 router isis 1
++ isis passive
++!
++interface r4-eth0
++ ip address 10.0.4.4/24
++ ipv6 address fc00:10:0:4::4/64
++ link-params
++ neighbor 10.0.255.1
++ ip router isis 1
++ ipv6 router isis 1
++ isis circuit-type level-2
++ isis hello-interval 1
++ isis hello-multiplier 3
++ isis network point-to-point
++!
++interface r4-eth1
++ ip address 10.0.5.4/24
++ ipv6 address fc00:10:0:5::4/64
++ link-params
++ neighbor 10.0.255.3
++ ip router isis 1
++ ipv6 router isis 1
++ isis circuit-type level-2
++ isis hello-interval 1
++ isis hello-multiplier 3
++ isis network point-to-point
++!
++router isis 1
++ net 49.0000.0000.0000.0004.00
++ is-type level-2-only
++ topology ipv6-unicast
++ redistribute ipv4 static level-2
++ redistribute ipv6 static level-2
++ redistribute ipv4 connected level-2
++ redistribute ipv6 connected level-2
++ lsp-gen-interval 1
++ lsp-refresh-interval 10
++ max-lsp-lifetime 20
++ mpls-te on
++ mpls-te router-address 4.4.4.4
++ mpls-te router-address ipv6 fc00:0:4::1
++ segment-routing on
++ segment-routing node-msd 7
++ segment-routing global-block 400000 403999
++ segment-routing prefix 4.4.4.4/32 index 4
++!
++end
+diff --git a/tests/topotests/bgp_link_state/r4/isis_adj.json b/tests/topotests/bgp_link_state/r4/isis_adj.json
+new file mode 100644
+index 0000000000..7139b66acf
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/r4/isis_adj.json
+@@ -0,0 +1,18 @@
++{
++ "areas": [
++ {
++ "circuits": [
++ {
++ "adj": "r1",
++ "interface": "r4-eth0",
++ "state": "Up"
++ },
++ {
++ "adj": "r3",
++ "interface": "r4-eth1",
++ "state": "Up"
++ }
++ ]
++ }
++ ]
++}
+diff --git a/tests/topotests/bgp_link_state/rr/bgp_capability.json b/tests/topotests/bgp_link_state/rr/bgp_capability.json
+new file mode 100644
+index 0000000000..34a040af60
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/rr/bgp_capability.json
+@@ -0,0 +1,11 @@
++{
++ "10.0.3.2": {
++ "neighborCapabilities": {
++ "multiprotocolExtensions": {
++ "linkState": {
++ "advertisedAndReceived": true
++ }
++ }
++ }
++ }
++}
+diff --git a/tests/topotests/bgp_link_state/rr/bgp_ls_attrs_link43.json b/tests/topotests/bgp_link_state/rr/bgp_ls_attrs_link43.json
+new file mode 100644
+index 0000000000..4ce4cb631b
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/rr/bgp_ls_attrs_link43.json
+@@ -0,0 +1,14 @@
++{
++ "paths": [
++ {
++ "linkStateAttrs": {
++ "adjSids": [
++ {
++ "flags": "0x30",
++ "weight": 0
++ }
++ ]
++ }
++ }
++ ]
++}
+diff --git a/tests/topotests/bgp_link_state/rr/bgp_ls_attrs_node4.json b/tests/topotests/bgp_link_state/rr/bgp_ls_attrs_node4.json
+new file mode 100644
+index 0000000000..59bf79196f
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/rr/bgp_ls_attrs_node4.json
+@@ -0,0 +1,15 @@
++{
++ "paths": [
++ {
++ "linkStateAttrs": {
++ "srCapabilities": {
++ "lowerBound": 400000,
++ "rangeSize": 4000,
++ "flags": "0xc0"
++ },
++ "msd": 7,
++ "isisAreaId": "49.0000"
++ }
++ }
++ ]
++}
+diff --git a/tests/topotests/bgp_link_state/rr/bgp_ls_nlri.json b/tests/topotests/bgp_link_state/rr/bgp_ls_nlri.json
+new file mode 100644
+index 0000000000..c5b5b687e9
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/rr/bgp_ls_nlri.json
+@@ -0,0 +1,1552 @@
++{
++ "vrfId": 0,
++ "vrfName": "default",
++ "routerId": "10.10.10.10",
++ "routes": {
++ "[V][L2][I0x0][N[s0000.0000.0002]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "node",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ }
++ },
++ "nlriStr": "[V][L2][I0x0][N[s0000.0000.0002]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.1.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.1.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.1.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.2.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.2.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.2.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[p2.2.2.2/32]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "2.2.2.2/32"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[p2.2.2.2/32]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:0:2::1/128]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:0:2::1/128"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:0:2::1/128]]"
++ }
++ ],
++ "[V][L2][I0x0][N[s0000.0000.0001]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "node",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ }
++ },
++ "nlriStr": "[V][L2][I0x0][N[s0000.0000.0001]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[p1.1.1.1/32]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "1.1.1.1/32"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[p1.1.1.1/32]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[p10.0.4.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.4.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[p10.0.4.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:0:1::1/128]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:0:1::1/128"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:0:1::1/128]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:10:0:1::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:1::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:10:0:1::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:10:0:4::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:4::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[pfc00:10:0:4::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.3.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.3.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[p10.0.3.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0001]][P[p10.0.1.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.1.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0001]][P[p10.0.1.0/24]]"
++ }
++ ],
++ "[V][L2][I0x0][N[s0000.0000.0003]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "node",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ }
++ },
++ "nlriStr": "[V][L2][I0x0][N[s0000.0000.0003]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[p3.3.3.3/32]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "3.3.3.3/32"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[p3.3.3.3/32]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[p10.0.5.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.5.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[p10.0.5.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:0:3::1/128]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:0:3::1/128"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:0:3::1/128]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:10:0:1::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:1::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:10:0:1::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:10:0:2::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:2::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0002]][P[pfc00:10:0:2::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[p10.0.2.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.2.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[p10.0.2.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:10:0:5::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:5::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:10:0:5::/64]]"
++ }
++ ],
++ "[V][L2][I0x0][N[s0000.0000.0004]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "node",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ }
++ },
++ "nlriStr": "[V][L2][I0x0][N[s0000.0000.0004]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[p4.4.4.4/32]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "4.4.4.4/32"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[p4.4.4.4/32]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:0:4::1/128]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:0:4::1/128"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:0:4::1/128]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:10:0:2::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:2::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0003]][P[pfc00:10:0:2::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[p10.0.4.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.4.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[p10.0.4.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[p10.0.5.0/24]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv4Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "10.0.5.0/24"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[p10.0.5.0/24]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:10:0:5::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:5::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:10:0:5::/64]]"
++ }
++ ],
++ "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:10:0:4::/64]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "ipv6Prefix",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "prefixDescriptors": {
++ "ipReachabilityInformation": "fc00:10:0:4::/64"
++ }
++ },
++ "nlriStr": "[T][L2][I0x0][N[s0000.0000.0004]][P[pfc00:10:0:4::/64]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0002]][L[i10.0.1.1][n10.0.1.2]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.1.1",
++ "ipv4NeighborAddress": "10.0.1.2"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0002]][L[i10.0.1.1][n10.0.1.2]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0001]][L[i10.0.1.2][n10.0.1.1]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.1.2",
++ "ipv4NeighborAddress": "10.0.1.1"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0001]][L[i10.0.1.2][n10.0.1.1]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0003]][L[i10.0.2.2][n10.0.2.3]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.2.2",
++ "ipv4NeighborAddress": "10.0.2.3"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0003]][L[i10.0.2.2][n10.0.2.3]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0002]][L[i10.0.2.3][n10.0.2.2]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.2.3",
++ "ipv4NeighborAddress": "10.0.2.2"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0002]][L[i10.0.2.3][n10.0.2.2]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0004]][L[i10.0.4.1][n10.0.4.4]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.4.1",
++ "ipv4NeighborAddress": "10.0.4.4"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0004]][L[i10.0.4.1][n10.0.4.4]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0001]][L[i10.0.4.4][n10.0.4.1]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.4.4",
++ "ipv4NeighborAddress": "10.0.4.1"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0001]][L[i10.0.4.4][n10.0.4.1]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0004]][L[i10.0.5.3][n10.0.5.4]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.5.3",
++ "ipv4NeighborAddress": "10.0.5.4"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0004]][L[i10.0.5.3][n10.0.5.4]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[i10.0.5.4][n10.0.5.3]]": [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "linkDescriptors": {
++ "ipv4InterfaceAddress": "10.0.5.4",
++ "ipv4NeighborAddress": "10.0.5.3"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[i10.0.5.4][n10.0.5.3]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0002]][L[ifc00:10:0:1::1][nfc00:10:0:1::2]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:1::1",
++ "ipv6NeighborAddress": "fc00:10:0:1::2"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0002]][L[ifc00:10:0:1::1][nfc00:10:0:1::2]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0001]][L[ifc00:10:0:1::2][nfc00:10:0:1::1]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:1::2",
++ "ipv6NeighborAddress": "fc00:10:0:1::1"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0001]][L[ifc00:10:0:1::2][nfc00:10:0:1::1]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0003]][L[ifc00:10:0:2::2][nfc00:10:0:2::3]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:2::2",
++ "ipv6NeighborAddress": "fc00:10:0:2::3"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0002]][R[s0000.0000.0003]][L[ifc00:10:0:2::2][nfc00:10:0:2::3]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0002]][L[ifc00:10:0:2::3][nfc00:10:0:2::2]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0002"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:2::3",
++ "ipv6NeighborAddress": "fc00:10:0:2::2"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0002]][L[ifc00:10:0:2::3][nfc00:10:0:2::2]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0004]][L[ifc00:10:0:4::1][nfc00:10:0:4::4]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:4::1",
++ "ipv6NeighborAddress": "fc00:10:0:4::4"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0001]][R[s0000.0000.0004]][L[ifc00:10:0:4::1][nfc00:10:0:4::4]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0001]][L[ifc00:10:0:4::4][nfc00:10:0:4::1]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0001"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:4::4",
++ "ipv6NeighborAddress": "fc00:10:0:4::1"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0001]][L[ifc00:10:0:4::4][nfc00:10:0:4::1]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0004]][L[ifc00:10:0:5::3][nfc00:10:0:5::4]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:5::3",
++ "ipv6NeighborAddress": "fc00:10:0:5::4"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0003]][R[s0000.0000.0004]][L[ifc00:10:0:5::3][nfc00:10:0:5::4]]"
++ }
++ ],
++ "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[ifc00:10:0:5::4][nfc00:10:0:5::3]]":
++ [
++ {
++ "valid": true,
++ "bestpath": true,
++ "selectionReason": "First path received",
++ "pathFrom": "internal",
++ "weight": 0,
++ "peerId": "10.0.3.2",
++ "path": "",
++ "origin": "incomplete",
++ "locPrf": 100,
++ "nexthops": [
++ {
++ "ip": "10.0.3.2",
++ "hostname": "r2",
++ "afi": "ipv4",
++ "used": true
++ }
++ ],
++ "nlri": {
++ "nlriType": "link",
++ "protocolId": 2,
++ "identifier": 0,
++ "localNodeDescriptors": {
++ "igpRouterId": "0000.0000.0004"
++ },
++ "remoteNodeDescriptors": {
++ "igpRouterId": "0000.0000.0003"
++ },
++ "linkDescriptors": {
++ "ipv6InterfaceAddress": "fc00:10:0:5::4",
++ "ipv6NeighborAddress": "fc00:10:0:5::3"
++ }
++ },
++ "nlriStr": "[E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[ifc00:10:0:5::4][nfc00:10:0:5::3]]"
++ }
++ ]
++ }
++}
+\ No newline at end of file
+diff --git a/tests/topotests/bgp_link_state/rr/bgp_ls_prefix4.json b/tests/topotests/bgp_link_state/rr/bgp_ls_prefix4.json
+new file mode 100644
+index 0000000000..46339e92fe
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/rr/bgp_ls_prefix4.json
+@@ -0,0 +1,13 @@
++{
++ "paths": [
++ {
++ "linkStateAttrs": {
++ "prefixSid": {
++ "sid": 4,
++ "flags": "0x40",
++ "algo": 0
++ }
++ }
++ }
++ ]
++}
+diff --git a/tests/topotests/bgp_link_state/rr/bgp_neighbor.json b/tests/topotests/bgp_link_state/rr/bgp_neighbor.json
+new file mode 100644
+index 0000000000..986514610b
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/rr/bgp_neighbor.json
+@@ -0,0 +1,7 @@
++{
++ "10.0.3.2": {
++ "remoteAs": 65000,
++ "localAs": 65000,
++ "bgpState": "Established"
++ }
++}
+diff --git a/tests/topotests/bgp_link_state/rr/frr.conf b/tests/topotests/bgp_link_state/rr/frr.conf
+new file mode 100644
+index 0000000000..7f8ade7e6a
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/rr/frr.conf
+@@ -0,0 +1,23 @@
++hostname rr
++!
++!debug bgp link-state
++!debug bgp zebra
++!
++router bgp 65000
++ bgp router-id 10.10.10.10
++ no bgp ebgp-requires-policy
++ neighbor 10.0.3.2 remote-as 65000
++ neighbor 10.0.3.2 update-source 10.0.3.4
++ !
++ address-family link-state
++ neighbor 10.0.3.2 activate
++ exit-address-family
++!
++interface rr-eth0
++ ip address 10.0.3.4/24
++!
++interface lo
++ ip address 10.10.10.10/32
++!
++ip forwarding
++!
+diff --git a/tests/topotests/bgp_link_state/test_bgp_link_state.py b/tests/topotests/bgp_link_state/test_bgp_link_state.py
+new file mode 100644
+index 0000000000..97444690ba
+--- /dev/null
++++ b/tests/topotests/bgp_link_state/test_bgp_link_state.py
+@@ -0,0 +1,1007 @@
++#!/usr/bin/env python
++# SPDX-License-Identifier: ISC
++#
++# Copyright (c) 2025 by Carmine Scarpitta
++#
++"""
++Test BGP Link-State (RFC 9552) functionality:
++- BGP-LS capability negotiation
++- Producer mode: Export IGP topology to BGP-LS
++- Consumer mode: Build TED from BGP-LS routes
++
++Topology:
++
++ +-----+
++ | rr | (Consumer)
++ +-----+
++ |
++ | BGP-LS
++ |
++ +-----+ +-----+ +-----+
++ | r1 |--| r2 |--| r3 |
++ +-----+ +-----+ +-----+
++ \ (Producer) /
++ \ /
++ \ ISIS /
++ \ /
++ \ /
++ \ /
++ +-----+
++ | r4 |
++ +-----+
++
++- r1, r2, r3, r4: Run ISIS L2 IGP
++- r2: BGP-LS Producer (collects ISIS topology and exports via BGP-LS)
++- rr: BGP-LS Consumer (receives BGP-LS routes and builds TED)
++"""
++
++import os
++import sys
++import json
++import pytest
++import functools
++
++# pylint: disable=C0413
++# Import topogen and topotest helpers
++from lib import topotest
++from lib.topogen import Topogen, TopoRouter, get_topogen
++from lib.topolog import logger
++
++# Required to instantiate the topology builder class.
++pytestmark = [pytest.mark.bgpd, pytest.mark.isisd]
++
++
++#
++# Helper functions for BGP-LS validation
++#
++
++def extract_bgp_ls_routes(json_data):
++ """
++ Extract routes from BGP-LS JSON output.
++
++ The actual output has format: {"routes": {"[key]": [{"nlri": {...}, ...}]}}
++
++ Args:
++ json_data: Parsed JSON dictionary from show bgp link-state command
++
++ Returns:
++ Array of route objects with nlri and path attributes
++ """
++ routes_array = []
++
++ if isinstance(json_data, dict) and "routes" in json_data:
++ routes_dict = json_data["routes"]
++ for nlri_key, paths_list in routes_dict.items():
++ if paths_list and isinstance(paths_list, list):
++ for path in paths_list:
++ routes_array.append(path)
++ elif isinstance(json_data, list):
++ routes_array = json_data
++
++ return routes_array
++
++
++def check_bgp_ls_prefix(router, prefix, protocol_id=None, should_exist=True):
++ """
++ Check if a prefix exists in BGP-LS NLRI
++
++ Args:
++ router: Router instance
++ prefix: IP prefix to check (e.g., "192.168.100.0/24")
++ protocol_id: Optional protocol ID filter (e.g., 5 for static)
++ should_exist: True to verify presence, False to verify absence
++
++ Returns:
++ None if check passes, error message otherwise
++ """
++ output = router.vtysh_cmd("show bgp link-state link-state json")
++ json_data = json.loads(output)
++ data = extract_bgp_ls_routes(json_data)
++
++ for entry in data:
++ nlri = entry.get("nlri", {})
++ # Filter for prefix NLRI types (ipv4Prefix or ipv6Prefix)
++ nlri_type = nlri.get("nlriType")
++ if nlri_type not in ["ipv4Prefix", "ipv6Prefix"]:
++ continue
++
++ if nlri.get("prefixDescriptors", {}).get("ipReachabilityInformation") == prefix:
++ if protocol_id is not None and nlri.get("protocolId") != protocol_id:
++ continue
++ # Found the prefix
++ if should_exist:
++ return None
++ else:
++ return f"Prefix {prefix} still present in BGP-LS"
++
++ # Prefix not found
++ if should_exist:
++ return f"Prefix {prefix} not found in BGP-LS"
++ else:
++ return None
++
++
++def check_bgp_ls_link(router, local_id, remote_id, should_exist=True):
++ """
++ Check if a link exists in BGP-LS NLRI
++
++ Args:
++ router: Router instance
++ local_id: Local node IGP Router ID (e.g., "0000.0000.0002")
++ remote_id: Remote node IGP Router ID (e.g., "0000.0000.0001")
++ should_exist: True to verify presence, False to verify absence
++
++ Returns:
++ None if check passes, error message otherwise
++ """
++ output = router.vtysh_cmd("show bgp link-state link-state json")
++ json_data = json.loads(output)
++ data = extract_bgp_ls_routes(json_data)
++
++ for entry in data:
++ nlri = entry.get("nlri", {})
++ # Filter for link NLRI type
++ if nlri.get("nlriType") != "link":
++ continue
++
++ nlri_local_id = nlri.get("localNodeDescriptors", {}).get("igpRouterId")
++ nlri_remote_id = nlri.get("remoteNodeDescriptors", {}).get("igpRouterId")
++ if nlri_local_id == local_id and nlri_remote_id == remote_id:
++ # Found the link
++ if should_exist:
++ return None
++ else:
++ return f"Link {local_id}->{remote_id} still present in BGP-LS"
++
++ # Link not found
++ if should_exist:
++ return f"Link {local_id}->{remote_id} not found in BGP-LS"
++ else:
++ return None
++
++
++def check_bgp_ls_node(router, node_id, should_exist=True):
++ """
++ Check if a node exists in BGP-LS NLRI
++
++ Args:
++ router: Router instance
++ node_id: Node IGP Router ID (e.g., "0000.0000.0001")
++ should_exist: True to verify presence, False to verify absence
++
++ Returns:
++ None if check passes, error message otherwise
++ """
++ output = router.vtysh_cmd("show bgp link-state link-state json")
++ json_data = json.loads(output)
++ data = extract_bgp_ls_routes(json_data)
++
++ for entry in data:
++ nlri = entry.get("nlri", {})
++ # Filter for node NLRI type
++ if nlri.get("nlriType") != "node":
++ continue
++
++ nlri_node_id = nlri.get("localNodeDescriptors", {}).get("igpRouterId")
++ if nlri_node_id == node_id:
++ # Found the node
++ if should_exist:
++ return None
++ else:
++ return f"Node {node_id} still present in BGP-LS"
++
++ # Node not found
++ if should_exist:
++ return f"Node {node_id} not found in BGP-LS"
++ else:
++ return None
++
++
++def get_bgp_ls_count(router):
++ """
++ Get the count of BGP-LS NLRIs
++
++ Args:
++ router: Router instance
++
++ Returns:
++ Count of BGP-LS routes
++ """
++ output = router.vtysh_cmd("show bgp link-state link-state json")
++ json_data = json.loads(output)
++ data = extract_bgp_ls_routes(json_data)
++ return len(data)
++
++
++def check_bgp_ls_empty(router):
++ """
++ Check that the BGP-LS routing table is empty.
++
++ Args:
++ router: Router instance
++
++ Returns:
++ None if table is empty, error message otherwise
++ """
++ count = get_bgp_ls_count(router)
++ if count > 0:
++ return f"BGP-LS table not empty: {count} route(s) still present"
++ return None
++
++
++def build_topo(tgen):
++ """Build the test topology"""
++
++ # Create routers
++ for routern in [1, 2, 3, 4]:
++ tgen.add_router("r{}".format(routern))
++ tgen.add_router("rr")
++
++ # Create switches
++ switch = tgen.add_switch("s1")
++ switch.add_link(tgen.gears["r1"])
++ switch.add_link(tgen.gears["r2"])
++
++ switch = tgen.add_switch("s2")
++ switch.add_link(tgen.gears["r2"])
++ switch.add_link(tgen.gears["r3"])
++
++ switch = tgen.add_switch("s3")
++ switch.add_link(tgen.gears["r2"])
++ switch.add_link(tgen.gears["rr"])
++
++ # r4 connections
++ switch = tgen.add_switch("s4")
++ switch.add_link(tgen.gears["r1"])
++ switch.add_link(tgen.gears["r4"])
++
++ switch = tgen.add_switch("s5")
++ switch.add_link(tgen.gears["r3"])
++ switch.add_link(tgen.gears["r4"])
++
++
++def setup_module(mod):
++ """Setup module for the tests"""
++ tgen = Topogen(build_topo, mod.__name__)
++ tgen.start_topology()
++
++ router_list = tgen.routers()
++
++ # Initialize all routers
++ for rname, router in router_list.items():
++ router.load_frr_config(os.path.join(CWD, "{}/frr.conf".format(rname)))
++
++ # Start routers
++ tgen.start_router()
++
++
++def teardown_module(mod):
++ """Teardown the pytest environment"""
++ tgen = get_topogen()
++ tgen.stop_topology()
++
++
++def test_isis_convergence():
++ """Test ISIS convergence"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Checking ISIS convergence")
++
++ for rname in ["r1", "r2", "r3", "r4"]:
++ router = tgen.gears[rname]
++
++ # Check ISIS adjacencies
++ reffile = os.path.join(CWD, "{}/isis_adj.json".format(rname))
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show isis neighbor json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assertmsg = '"{}" JSON output mismatches'.format(rname)
++ assert result is None, assertmsg
++
++
++def test_bgp_convergence():
++ """Test BGP convergence between r2 (producer) and rr (consumer)"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Checking BGP convergence")
++
++ # Check BGP neighbor status on r2 (producer)
++ router = tgen.gears["r2"]
++ reffile = os.path.join(CWD, "r2/bgp_neighbor.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp neighbor json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assert result is None, '"r2" BGP neighbor not established'
++
++ # Check BGP neighbor status on rr (consumer)
++ router = tgen.gears["rr"]
++ reffile = os.path.join(CWD, "rr/bgp_neighbor.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp neighbor json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assert result is None, '"rr" BGP neighbor not established'
++
++
++def test_bgp_ls_capability():
++ """Test BGP-LS capability negotiation"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Checking BGP-LS capability negotiation")
++
++ # Check r2 advertised and received BGP-LS capability
++ router = tgen.gears["r2"]
++ reffile = os.path.join(CWD, "r2/bgp_capability.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp neighbor 10.0.3.4 json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS capability not negotiated'
++
++ # Check rr advertised and received BGP-LS capability
++ router = tgen.gears["rr"]
++ reffile = os.path.join(CWD, "rr/bgp_capability.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp neighbor 10.0.3.2 json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" BGP-LS capability not negotiated'
++
++
++def test_bgp_ls_routes_producer():
++ """Test BGP-LS routes on producer (r2)"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Checking BGP-LS routes on producer")
++
++ router = tgen.gears["r2"]
++
++ # Check BGP-LS routes are originated
++ reffile = os.path.join(CWD, "r2/bgp_ls_nlri.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp link-state link-state json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assertmsg = '"r2" BGP-LS routes not originated correctly'
++ assert result is None, assertmsg
++
++
++def test_bgp_ls_routes_consumer():
++ """Test BGP-LS routes on consumer (rr)"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Checking BGP-LS routes on consumer")
++
++ router = tgen.gears["rr"]
++
++ # Check BGP-LS routes are received
++ reffile = os.path.join(CWD, "rr/bgp_ls_nlri.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp link-state link-state json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assertmsg = '"rr" BGP-LS routes not received correctly'
++ assert result is None, assertmsg
++
++def test_bgp_ls_attributes_consumer():
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Checking BGP-LS prefix attributes on consumer")
++
++ router = tgen.gears["rr"]
++
++ # Check BGP-LS attributes are received for prefix
++ reffile = os.path.join(CWD, "rr/bgp_ls_prefix4.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp link-state link-state [T][L2][I0x0][N[s0000.0000.0004]][P[p4.4.4.4/32]] json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assertmsg = '"rr" BGP-LS prefix attributes not received correctly'
++ assert result is None, assertmsg
++
++ # Check BGP-LS attributes are received for node
++ reffile = os.path.join(CWD, "rr/bgp_ls_attrs_node4.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp link-state link-state [V][L2][I0x0][N[s0000.0000.0004]] json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assertmsg = '"rr" BGP-LS node attributes not received correctly'
++ assert result is None, assertmsg
++
++ # Check BGP-LS attributes are received for link
++ reffile = os.path.join(CWD, "rr/bgp_ls_attrs_link43.json")
++ expected = json.loads(open(reffile).read())
++
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ router,
++ "show bgp link-state link-state [E][L2][I0x0][N[s0000.0000.0004]][R[s0000.0000.0003]][L[i10.0.5.4][n10.0.5.3]] json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assertmsg = '"rr" BGP-LS link attributes not received correctly'
++ assert result is None, assertmsg
++
++
++
++def test_bgp_ls_static_route_add():
++ """Test adding a static route and verifying BGP-LS update"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Adding static route on r1 and checking BGP-LS update")
++
++ r2 = tgen.gears["r2"]
++ consumer = tgen.gears["rr"]
++
++ # Get initial route count
++ initial_count_r2 = get_bgp_ls_count(r2)
++ initial_count_rr = get_bgp_ls_count(consumer)
++
++ router = tgen.gears["r1"]
++
++ # Add static route on r1
++ router.vtysh_cmd("configure terminal\nip route 192.168.100.0/24 Null0")
++
++ # Wait for ISIS to propagate to r2, then BGP-LS to advertise the new prefix (protocol ID 2 = ISIS)
++ test_func = functools.partial(
++ check_bgp_ls_prefix, r2, "192.168.100.0/24", protocol_id=2, should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS static route not advertised'
++
++ # Verify route count increased by 1
++ new_count_r2 = get_bgp_ls_count(r2)
++ assert new_count_r2 == initial_count_r2 + 1, f'"r2" route count should be {initial_count_r2 + 1}, got {new_count_r2}'
++
++ # Verify consumer rr received the update
++ test_func = functools.partial(
++ check_bgp_ls_prefix, consumer, "192.168.100.0/24", protocol_id=2, should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS static route update'
++
++ # Verify route count increased by 1
++ new_count_rr = get_bgp_ls_count(consumer)
++ assert new_count_rr == initial_count_rr + 1, f'"rr" route count should be {initial_count_rr + 1}, got {new_count_rr}'
++
++
++def test_bgp_ls_static_route_remove():
++ """Test removing static route and verifying BGP-LS withdrawal"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Removing static route on r1 and checking BGP-LS withdrawal")
++
++ r2 = tgen.gears["r2"]
++ consumer = tgen.gears["rr"]
++
++ # Get current route count (should have the static route from previous test)
++ initial_count_r2 = get_bgp_ls_count(r2)
++ initial_count_rr = get_bgp_ls_count(consumer)
++
++ router = tgen.gears["r1"]
++
++ # Remove static route from r1
++ router.vtysh_cmd("configure terminal\nno ip route 192.168.100.0/24 Null0")
++
++ # Wait for ISIS to propagate to r2, then BGP-LS to withdraw the prefix
++ test_func = functools.partial(
++ check_bgp_ls_prefix, r2, "192.168.100.0/24", protocol_id=2, should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS static route not withdrawn'
++
++ # Verify route count decreased by 1
++ new_count_r2 = get_bgp_ls_count(r2)
++ assert new_count_r2 == initial_count_r2 - 1, f'"r2" route count should be {initial_count_r2 - 1}, got {new_count_r2}'
++
++ # Verify consumer rr received the withdrawal
++ test_func = functools.partial(
++ check_bgp_ls_prefix, consumer, "192.168.100.0/24", protocol_id=2, should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS static route withdrawal'
++
++ # Verify route count decreased by 1
++ new_count_rr = get_bgp_ls_count(consumer)
++ assert new_count_rr == initial_count_rr - 1, f'"rr" route count should be {initial_count_rr - 1}, got {new_count_rr}'
++
++ # Verify r2 routes match original reference file
++ reffile = os.path.join(CWD, "r2/bgp_ls_nlri.json")
++ expected = json.loads(open(reffile).read())
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ r2,
++ "show bgp link-state link-state json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS routes do not match expected after route removal'
++
++ # Verify rr routes match original reference file
++ reffile = os.path.join(CWD, "rr/bgp_ls_nlri.json")
++ expected = json.loads(open(reffile).read())
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ consumer,
++ "show bgp link-state link-state json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" BGP-LS routes do not match expected after route removal'
++
++
++def test_bgp_ls_interface_address_add():
++ """Test adding interface address and verifying BGP-LS update"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Adding interface address on r1 and checking BGP-LS update")
++
++ r2 = tgen.gears["r2"]
++ consumer = tgen.gears["rr"]
++
++ # Get initial route count
++ initial_count_r2 = get_bgp_ls_count(r2)
++ initial_count_rr = get_bgp_ls_count(consumer)
++
++ router = tgen.gears["r1"]
++
++ # Add secondary IP address to loopback on r1
++ router.vtysh_cmd("configure terminal\ninterface lo\nip address 1.1.1.11/32")
++
++ # Wait for ISIS to propagate to r2, then BGP-LS to advertise the new prefix
++ test_func = functools.partial(
++ check_bgp_ls_prefix, r2, "1.1.1.11/32", should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS secondary address not advertised'
++
++ # Verify route count increased by 1
++ new_count_r2 = get_bgp_ls_count(r2)
++ assert new_count_r2 == initial_count_r2 + 1, f'"r2" route count should be {initial_count_r2 + 1}, got {new_count_r2}'
++
++ # Verify consumer rr received the update
++ test_func = functools.partial(
++ check_bgp_ls_prefix, consumer, "1.1.1.11/32", should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS secondary address update'
++
++ # Verify route count increased by 1
++ new_count_rr = get_bgp_ls_count(consumer)
++ assert new_count_rr == initial_count_rr + 1, f'"rr" route count should be {initial_count_rr + 1}, got {new_count_rr}'
++
++
++def test_bgp_ls_interface_address_remove():
++ """Test removing interface address and verifying BGP-LS withdrawal"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Removing interface address on r1 and checking BGP-LS withdrawal")
++
++ r2 = tgen.gears["r2"]
++ consumer = tgen.gears["rr"]
++
++ # Get current route count
++ initial_count_r2 = get_bgp_ls_count(r2)
++ initial_count_rr = get_bgp_ls_count(consumer)
++
++ router = tgen.gears["r1"]
++
++ # Remove secondary IP address from r1
++ router.vtysh_cmd("configure terminal\ninterface lo\nno ip address 1.1.1.11/32")
++
++ # Wait for ISIS to propagate to r2, then BGP-LS to withdraw the prefix
++ test_func = functools.partial(
++ check_bgp_ls_prefix, r2, "1.1.1.11/32", should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS secondary address not withdrawn'
++
++ # Verify route count decreased by 1
++ new_count_r2 = get_bgp_ls_count(r2)
++ assert new_count_r2 == initial_count_r2 - 1, f'"r2" route count should be {initial_count_r2 - 1}, got {new_count_r2}'
++
++ # Verify consumer rr received the withdrawal
++ test_func = functools.partial(
++ check_bgp_ls_prefix, consumer, "1.1.1.11/32", should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS secondary address withdrawal'
++
++ # Verify route count decreased by 1
++ new_count_rr = get_bgp_ls_count(consumer)
++ assert new_count_rr == initial_count_rr - 1, f'"rr" route count should be {initial_count_rr - 1}, got {new_count_rr}'
++
++ # Verify routes match original reference files
++ reffile = os.path.join(CWD, "r2/bgp_ls_nlri.json")
++ expected = json.loads(open(reffile).read())
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ r2,
++ "show bgp link-state link-state json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS routes do not match expected'
++
++ reffile = os.path.join(CWD, "rr/bgp_ls_nlri.json")
++ expected = json.loads(open(reffile).read())
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ consumer,
++ "show bgp link-state link-state json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" BGP-LS routes do not match expected'
++
++
++def test_bgp_ls_ipv6_address_add():
++ """Test adding IPv6 interface address and verifying BGP-LS update"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Adding IPv6 interface address on r1 and checking BGP-LS update")
++
++ router = tgen.gears["r1"]
++
++ # Add secondary IPv6 address to loopback on r1
++ router.vtysh_cmd("configure terminal\ninterface lo\nipv6 address fc00:0:1::11/128")
++
++ # Wait for ISIS to propagate to r2, then BGP-LS to advertise the new prefix
++ r2 = tgen.gears["r2"]
++ test_func = functools.partial(
++ check_bgp_ls_prefix, r2, "fc00:0:1::11/128", should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS IPv6 secondary address not advertised'
++
++ # Verify consumer rr received the update
++ consumer = tgen.gears["rr"]
++ test_func = functools.partial(
++ check_bgp_ls_prefix, consumer, "fc00:0:1::11/128", should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS IPv6 secondary address update'
++
++
++def test_bgp_ls_ipv6_address_remove():
++ """Test removing IPv6 interface address and verifying BGP-LS withdrawal"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Removing IPv6 interface address on r1 and checking BGP-LS withdrawal")
++
++ router = tgen.gears["r1"]
++
++ # Remove secondary IPv6 address from r1
++ router.vtysh_cmd("configure terminal\ninterface lo\nno ipv6 address fc00:0:1::11/128")
++
++ # Wait for ISIS to propagate to r2, then BGP-LS to withdraw the prefix
++ r2 = tgen.gears["r2"]
++ test_func = functools.partial(
++ check_bgp_ls_prefix, r2, "fc00:0:1::11/128", should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS IPv6 secondary address not withdrawn'
++
++ # Verify consumer rr received the withdrawal
++ consumer = tgen.gears["rr"]
++ test_func = functools.partial(
++ check_bgp_ls_prefix, consumer, "fc00:0:1::11/128", should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS IPv6 secondary address withdrawal'
++
++
++def test_bgp_ls_ipv6_static_route_add():
++ """Test adding a static IPv6 route and verifying BGP-LS update"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Adding static IPv6 route on r1 and checking BGP-LS update")
++
++ router = tgen.gears["r1"]
++
++ # Add static IPv6 route on r1
++ router.vtysh_cmd("configure terminal\nipv6 route fc00:100::/64 Null0")
++
++ # Wait for ISIS to propagate to r2, then BGP-LS to advertise the new prefix (protocol ID 2 = ISIS)
++ r2 = tgen.gears["r2"]
++ test_func = functools.partial(
++ check_bgp_ls_prefix, r2, "fc00:100::/64", protocol_id=2, should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS static IPv6 route not advertised'
++
++ # Verify consumer rr received the update
++ consumer = tgen.gears["rr"]
++ test_func = functools.partial(
++ check_bgp_ls_prefix, consumer, "fc00:100::/64", protocol_id=2, should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS static IPv6 route update'
++
++
++def test_bgp_ls_ipv6_static_route_remove():
++ """Test removing static IPv6 route and verifying BGP-LS withdrawal"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Removing static IPv6 route on r1 and checking BGP-LS withdrawal")
++
++ router = tgen.gears["r1"]
++
++ # Remove static IPv6 route from r1
++ router.vtysh_cmd("configure terminal\nno ipv6 route fc00:100::/64 Null0")
++
++ # Wait for ISIS to propagate to r2, then BGP-LS to withdraw the prefix
++ r2 = tgen.gears["r2"]
++ test_func = functools.partial(
++ check_bgp_ls_prefix, r2, "fc00:100::/64", protocol_id=2, should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS static IPv6 route not withdrawn'
++
++ # Verify consumer rr received the withdrawal
++ consumer = tgen.gears["rr"]
++ test_func = functools.partial(
++ check_bgp_ls_prefix, consumer, "fc00:100::/64", protocol_id=2, should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS static IPv6 route withdrawal'
++
++
++def test_bgp_ls_r4_link_shutdown():
++ """Test shutting down r4 link and verifying BGP-LS updates"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Shutting down link r4-eth0 (to r1) and checking BGP-LS update")
++
++ router = tgen.gears["r4"]
++
++ # Shutdown interface r4-eth0 (link to r1)
++ router.vtysh_cmd("configure terminal\ninterface r4-eth0\nshutdown")
++
++ # Wait for ISIS to detect the link down and propagate to r2, then BGP-LS to withdraw link NLRI
++ r2 = tgen.gears["r2"]
++ test_func = functools.partial(
++ check_bgp_ls_link, r2, "0000.0000.0004", "0000.0000.0001", should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS r4 link down not reflected'
++
++ # Verify consumer rr received the update
++ consumer = tgen.gears["rr"]
++ test_func = functools.partial(
++ check_bgp_ls_link, consumer, "0000.0000.0004", "0000.0000.0001", should_exist=False
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS r4 link down update'
++
++
++def test_bgp_ls_r4_link_no_shutdown():
++ """Test bringing up r4 link and verifying BGP-LS updates"""
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info("Bringing up link r4-eth0 and checking BGP-LS update")
++
++ router = tgen.gears["r4"]
++
++ # Bring up interface r4-eth0
++ router.vtysh_cmd("configure terminal\ninterface r4-eth0\nno shutdown")
++
++ # Wait for ISIS adjacency to re-establish and propagate to r2, then BGP-LS to advertise link NLRI
++ r2 = tgen.gears["r2"]
++ test_func = functools.partial(
++ check_bgp_ls_link, r2, "0000.0000.0004", "0000.0000.0001", should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS r4 link up not reflected'
++
++ # Verify consumer rr received the update
++ consumer = tgen.gears["rr"]
++ test_func = functools.partial(
++ check_bgp_ls_link, consumer, "0000.0000.0004", "0000.0000.0001", should_exist=True
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS r4 link up update'
++
++
++def test_bgp_ls_peer_deactivate():
++ """
++ Test that deactivating the last BGP-LS peer:
++ - Withdraws all locally originated BGP-LS routes on the producer (r2)
++ - Clears all received BGP-LS routes on the consumer (rr)
++ """
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info(
++ "Deactivating BGP-LS peer on r2 and verifying route withdrawal on r2 and rr"
++ )
++
++ r2 = tgen.gears["r2"]
++
++ # Deactivate the only BGP-LS peer on r2 (neighbor 10.0.3.4 = rr).
++ r2.vtysh_cmd(
++ "configure terminal\n"
++ "router bgp 65000\n"
++ "address-family link-state\n"
++ "no neighbor 10.0.3.4 activate"
++ )
++
++ # r2 must have withdrawn all its locally originated BGP-LS routes
++ test_func = functools.partial(check_bgp_ls_empty, r2)
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"r2" BGP-LS routes not withdrawn after peer deactivation'
++
++ # rr (consumer) must have no BGP-LS routes once r2's session goes down
++ consumer = tgen.gears["rr"]
++ test_func = functools.partial(check_bgp_ls_empty, consumer)
++ _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
++ assert result is None, '"rr" BGP-LS routes not cleared after r2 peer deactivation'
++
++
++def test_bgp_ls_peer_reactivate():
++ """
++ Test that reactivating a BGP-LS peer after deactivation:
++ - Triggers a fresh TED sync from the IGP
++ - Re-originates all IGP topology as BGP-LS NLRIs
++ - Re-advertises all routes to the consumer (rr)
++
++ The previous test (test_bgp_ls_peer_deactivate) must run first.
++ """
++ tgen = get_topogen()
++
++ if tgen.routers_have_failure():
++ pytest.skip(tgen.errors)
++
++ logger.info(
++ "Reactivating BGP-LS peer on r2 and verifying route re-advertisement on r2 and rr"
++ )
++
++ r2 = tgen.gears["r2"]
++
++ # Reactivate the BGP-LS peer.
++ r2.vtysh_cmd(
++ "configure terminal\n"
++ "router bgp 65000\n"
++ "address-family link-state\n"
++ "neighbor 10.0.3.4 activate"
++ )
++
++ # r2 must re-originate all BGP-LS NLRIs for the full ISIS topology
++ reffile = os.path.join(CWD, "r2/bgp_ls_nlri.json")
++ expected = json.loads(open(reffile).read())
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ r2,
++ "show bgp link-state link-state json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assert result is None, '"r2" did not re-originate BGP-LS routes after peer reactivation'
++
++ # rr (consumer) must receive all BGP-LS routes again
++ consumer = tgen.gears["rr"]
++ reffile = os.path.join(CWD, "rr/bgp_ls_nlri.json")
++ expected = json.loads(open(reffile).read())
++ test_func = functools.partial(
++ topotest.router_json_cmp,
++ consumer,
++ "show bgp link-state link-state json",
++ expected,
++ )
++ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
++ assert result is None, '"rr" did not receive BGP-LS routes after r2 peer reactivation'
++
++
++def test_memory_leak():
++ """Run the memory leak test and report results"""
++ tgen = get_topogen()
++ if not tgen.is_memleak_enabled():
++ pytest.skip("Memory leak test is disabled")
++
++ tgen.report_memory_leaks()
++
++
++if __name__ == "__main__":
++ args = ["-s"] + sys.argv[1:]
++ sys.exit(pytest.main(args))
++
++
++# Get current working directory
++CWD = os.path.dirname(os.path.realpath(__file__))
+diff --git a/vtysh/vtysh.c b/vtysh/vtysh.c
+index 6c0d9fefc6..dfbc492496 100644
+--- a/vtysh/vtysh.c
++++ b/vtysh/vtysh.c
+@@ -1517,6 +1517,13 @@ static struct cmd_node bgp_srv6_node = {
+ .prompt = "%s(config-router-srv6)# ",
+ };
+
++static struct cmd_node bgp_ls_node = {
++ .name = "bgp link-state",
++ .node = BGP_LS_NODE,
++ .parent_node = BGP_NODE,
++ .prompt = "%s(config-router-af)# ",
++};
++
+ static struct cmd_node ospf_node = {
+ .name = "ospf",
+ .node = OSPF_NODE,
+@@ -1995,6 +2002,16 @@ DEFUNSH(VTYSH_BGPD, bgp_evpn_vni, bgp_evpn_vni_cmd, "vni " CMD_VNI_RANGE,
+ return CMD_SUCCESS;
+ }
+
++DEFUNSH(VTYSH_BGPD, address_family_link_state, address_family_link_state_cmd,
++ "address-family link-state [link-state]",
++ "Enter Address Family command mode\n"
++ "Link-State Address Family\n"
++ "Link-State Subsequent Address Family\n")
++{
++ vty->node = BGP_LS_NODE;
++ return CMD_SUCCESS;
++}
++
+ #if defined(ENABLE_BGP_VNC)
+ DEFUNSH(VTYSH_BGPD, vnc_defaults, vnc_defaults_cmd, "vnc defaults",
+ "VNC/RFP related configuration\n"
+@@ -2598,7 +2615,8 @@ DEFUNSH(VTYSH_BGPD, exit_address_family, exit_address_family_cmd,
+ || vty->node == BGP_IPV6L_NODE || vty->node == BGP_IPV6M_NODE
+ || vty->node == BGP_EVPN_NODE
+ || vty->node == BGP_FLOWSPECV4_NODE
+- || vty->node == BGP_FLOWSPECV6_NODE)
++ || vty->node == BGP_FLOWSPECV6_NODE
++ || vty->node == BGP_LS_NODE)
+ vty->node = BGP_NODE;
+ return CMD_SUCCESS;
+ }
+@@ -5090,6 +5108,7 @@ void vtysh_init_vty(void)
+ install_node(&rpki_node);
+ install_node(&bmp_node);
+ install_node(&bgp_srv6_node);
++ install_node(&bgp_ls_node);
+ install_node(&rip_node);
+ install_node(&ripng_node);
+ install_node(&ospf_node);
+@@ -5270,6 +5289,12 @@ void vtysh_init_vty(void)
+ install_element(BGP_SRV6_NODE, &exit_bgp_srv6_cmd);
+ install_element(BGP_SRV6_NODE, &quit_bgp_srv6_cmd);
+ install_element(BGP_SRV6_NODE, &vtysh_end_all_cmd);
++
++ install_element(BGP_NODE, &address_family_link_state_cmd);
++ install_element(BGP_LS_NODE, &vtysh_quit_bgpd_cmd);
++ install_element(BGP_LS_NODE, &vtysh_exit_bgpd_cmd);
++ install_element(BGP_LS_NODE, &vtysh_end_all_cmd);
++ install_element(BGP_LS_NODE, &exit_address_family_cmd);
+ #endif /* HAVE_BGPD */
+
+ /* ripd */
+diff --git a/zebra/connected.c b/zebra/connected.c
+index 0a4d9220b4..9a9ec96759 100644
+--- a/zebra/connected.c
++++ b/zebra/connected.c
+@@ -283,6 +283,7 @@ void connected_up(struct interface *ifp, struct connected *ifc)
+ break;
+ case AFI_UNSPEC:
+ case AFI_L2VPN:
++ case AFI_BGP_LS:
+ case AFI_MAX:
+ flog_warn(EC_ZEBRA_CONNECTED_AFI_UNKNOWN,
+ "Received unknown AFI: %s", afi2str(afi));
+@@ -496,6 +497,7 @@ void connected_down(struct interface *ifp, struct connected *ifc)
+ break;
+ case AFI_UNSPEC:
+ case AFI_L2VPN:
++ case AFI_BGP_LS:
+ case AFI_MAX:
+ zlog_warn("Unknown AFI: %s", afi2str(afi));
+ break;
+diff --git a/zebra/router-id.c b/zebra/router-id.c
+index 945f09cd86..bf91f5f7e2 100644
+--- a/zebra/router-id.c
++++ b/zebra/router-id.c
+@@ -102,6 +102,7 @@ int router_id_get(afi_t afi, struct prefix *p, struct zebra_vrf *zvrf)
+ return 0;
+ case AFI_UNSPEC:
+ case AFI_L2VPN:
++ case AFI_BGP_LS:
+ case AFI_MAX:
+ return -1;
+ }
+@@ -126,6 +127,7 @@ int router_id_set(afi_t afi, struct prefix *p, struct zebra_vrf *zvrf)
+ break;
+ case AFI_UNSPEC:
+ case AFI_L2VPN:
++ case AFI_BGP_LS:
+ case AFI_MAX:
+ return -1;
+ }
+diff --git a/zebra/zapi_msg.c b/zebra/zapi_msg.c
+index bd8ee28b21..b35d5bda28 100644
+--- a/zebra/zapi_msg.c
++++ b/zebra/zapi_msg.c
+@@ -547,6 +547,7 @@ int zsend_redistribute_route(int cmd, struct zserv *client, const struct route_n
+ break;
+ case AFI_L2VPN:
+ case AFI_MAX:
++ case AFI_BGP_LS:
+ case AFI_UNSPEC:
+ break;
+ }
+diff --git a/zebra/zebra_nhg.c b/zebra/zebra_nhg.c
+index ccb354cf86..5673d39e69 100644
+--- a/zebra/zebra_nhg.c
++++ b/zebra/zebra_nhg.c
+@@ -2355,6 +2355,7 @@ static int nexthop_active(struct nexthop *nexthop, struct nhg_hash_entry *nhe,
+ break;
+ case AFI_UNSPEC:
+ case AFI_L2VPN:
++ case AFI_BGP_LS:
+ case AFI_MAX:
+ flog_err(EC_LIB_DEVELOPMENT,
+ "%s: unknown address-family: %u", __func__,
+@@ -2398,6 +2399,7 @@ static int nexthop_active(struct nexthop *nexthop, struct nhg_hash_entry *nhe,
+ break;
+ case AFI_UNSPEC:
+ case AFI_L2VPN:
++ case AFI_BGP_LS:
+ case AFI_MAX:
+ assert(afi != AFI_IP && afi != AFI_IP6);
+ break;
+--
+2.51.2
+