From 572d01b2fb0152b0b95430066db9c632c15d204a Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Wed, 3 Sep 2025 09:06:16 -0500 Subject: [PATCH 01/10] [fpmsyncd]: Fix programming of ECMP routes with multiple SRv6 nexthops There is a bug in fpmsyncd related to the handling of ECMP routes with multiple SRv6 nexthops. When FRR attempts to program an ECMP route with multiple SRv6 nexthops, the FPM module encodes all nexthops in the Netlink message. Upon receiving this Netlink message, fpmsyncd processes it but returns the error "Multipath SRv6 routes aren't supported" and subsequently drops the route. As a result, the ECMP route is not installed in the ASIC. This pull request extends fpmsyncd to properly handle and install ECMP routes with multiple SRv6 nexthops. Signed-off-by: Carmine Scarpitta --- fpmsyncd/routesync.cpp | 182 +++++++++++++++++++++++++++++++---------- fpmsyncd/routesync.h | 4 + 2 files changed, 143 insertions(+), 43 deletions(-) diff --git a/fpmsyncd/routesync.cpp b/fpmsyncd/routesync.cpp index 0b4503262ae..32877563a25 100644 --- a/fpmsyncd/routesync.cpp +++ b/fpmsyncd/routesync.cpp @@ -167,6 +167,8 @@ RouteSync::RouteSync(RedisPipeline *pipeline) : m_nl_sock = nl_socket_alloc(); nl_connect(m_nl_sock, NETLINK_ROUTE); rtnl_link_alloc_cache(m_nl_sock, AF_UNSPEC, &m_link_cache); + + m_appDb = make_shared("APPL_DB", 0); } void RouteSync::setRouteWithWarmRestart(FieldValueTupleWrapperBase & fvw, @@ -953,8 +955,69 @@ bool RouteSync::getSrv6SteerRouteNextHop(struct nlmsghdr *h, int received_bytes, else { /* This is a multipath route */ - SWSS_LOG_NOTICE("Multipath SRv6 routes aren't supported"); - return false; + int len; + struct rtattr *subtb[RTA_MAX + 1]; + struct rtnexthop *rtnh = (struct rtnexthop *)RTA_DATA(tb[RTA_MULTIPATH]); + len = (int)RTA_PAYLOAD(tb[RTA_MULTIPATH]); + bool first_nh = true; + + for (;;) + { + uint16_t encap = 0; + string nh_sid; + string nh_src_addr; + + if (len < (int)sizeof(*rtnh) || rtnh->rtnh_len > len) + { + break; + } + + if (rtnh->rtnh_len > sizeof(*rtnh)) + { + memset(subtb, 0, sizeof(subtb)); + + netlink_parse_rtattr(subtb, RTA_MAX, RTNH_DATA(rtnh), + (int)(rtnh->rtnh_len - sizeof(*rtnh))); + + if (subtb[RTA_ENCAP_TYPE]) + { + encap = *(uint16_t *)RTA_DATA(subtb[RTA_ENCAP_TYPE]); + } + + if (subtb[RTA_ENCAP] && subtb[RTA_ENCAP_TYPE] && + *(uint16_t *)RTA_DATA(subtb[RTA_ENCAP_TYPE]) == + NH_ENCAP_SRV6_ROUTE) + { + parseEncapSrv6SteerRoute(subtb[RTA_ENCAP], nh_sid, nh_src_addr); + } + SWSS_LOG_DEBUG("Multipath nexthop encap:%d nh_sid:%s nh_src_addr:%s", + encap, nh_sid.c_str(), nh_src_addr.c_str()); + + if (nh_sid.empty()) + { + SWSS_LOG_ERROR("Received an invalid SRv6 nexthop: SID is missing. Skipping."); + continue; + } + + if (!first_nh) { + vpn_sid += ","; + src_addr += ","; + } + + vpn_sid += nh_sid; + src_addr += nh_src_addr; + + first_nh = false; + } + + if (rtnh->rtnh_len == 0) + { + break; + } + + len -= NLMSG_ALIGN(rtnh->rtnh_len); + rtnh = RTNH_NEXT(rtnh); + } } return true; @@ -1076,6 +1139,27 @@ Srv6SidListTableFieldValueTupleWrapper::fieldValueTupleVector() { } +bool RouteSync::getSrv6SidListsFromRoute(string &routeTableKey, string &sidlists) +{ + SWSS_LOG_ENTER(); + + Table routeTable{m_appDb.get(), APP_ROUTE_TABLE_NAME}; + + std::vector fieldValues; + routeTable.get(routeTableKey, fieldValues); + + for (auto iter : fieldValues) + { + if (fvField(iter) == "segment") + { + sidlists = fvValue(iter); + return true; + } + } + + return false; +} + void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) { @@ -1239,36 +1323,44 @@ void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) if (nlmsg_type == RTM_DELROUTE) { string routeTableKeyStr = string(routeTableKey); - string srv6SidListTableKey = vpn_sid_str; SWSS_LOG_INFO("SRV6 RouteTable del msg: %s", routeTableKeyStr.c_str()); delWithWarmRestart( RouteTableFieldValueTupleWrapper{std::move(routeTableKeyStr), std::string()}, *m_routeTable); - auto it = m_srv6_sidlist_refcnt.find(srv6SidListTableKey); - if (it != m_srv6_sidlist_refcnt.end()) + getSrv6SidListsFromRoute(routeTableKeyStr, vpn_sid_str); + SWSS_LOG_INFO("Got Srv6 Sid list for route '%s' from route table: '%s'", + routeTableKeyStr.c_str(), vpn_sid_str.c_str()); + + /* Delete SID lists from SRV6_SID_LIST_TABLE */ + vector sidlists = tokenize(vpn_sid_str, ','); + for (auto sidlist : sidlists) { - assert (it->second > 0); + auto it = m_srv6_sidlist_refcnt.find(sidlist); + if (it != m_srv6_sidlist_refcnt.end()) + { + assert (it->second > 0); - /* Decrement the refcount for this SID list */ - (it->second)--; - SWSS_LOG_INFO("Refcount for SID list '%s' decreased to %u", - srv6SidListTableKey.c_str(), it->second); + /* Decrement the refcount for this SID list */ + (it->second)--; + SWSS_LOG_INFO("Refcount for SID list '%s' decreased to %u", + sidlist.c_str(), it->second); - /* If the refcount drops to zero, remove the SID list from ApplDB */ - if (it->second == 0) - { - m_srv6SidListTable.del(srv6SidListTableKey); - SWSS_LOG_INFO("Refcount for SID list '%s' is zero. SID list removed from ApplDB", - srv6SidListTableKey.c_str()); + /* If the refcount drops to zero, remove the SID list from ApplDB */ + if (it->second == 0) + { + m_srv6SidListTable.del(sidlist); + SWSS_LOG_INFO("Refcount for SID list '%s' is zero. SID list removed from ApplDB", + sidlist.c_str()); - m_srv6_sidlist_refcnt.erase(srv6SidListTableKey); + m_srv6_sidlist_refcnt.erase(sidlist); + } + } + else + { + SWSS_LOG_WARN("SID list '%s' not found in the map.", sidlist.c_str()); } - } - else - { - SWSS_LOG_WARN("SID list '%s' not found in the map.", srv6SidListTableKey.c_str()); } return; @@ -1276,31 +1368,35 @@ void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) else if (nlmsg_type == RTM_NEWROUTE) { string routeTableKeyStr = string(routeTableKey); - /* Write SID list to SRV6_SID_LIST_TABLE */ - string srv6SidListTableKey = vpn_sid_str; - - auto it = m_srv6_sidlist_refcnt.find(srv6SidListTableKey); - if (it != m_srv6_sidlist_refcnt.end()) + /* Write SID lists to SRV6_SID_LIST_TABLE */ + vector sidlists = tokenize(vpn_sid_str, ','); + for (auto sidlist : sidlists) { - /* SID list already exists: just bump the refcount */ - (it->second)++; - SWSS_LOG_INFO("Refcount for SID list'%s' increased to %u", - srv6SidListTableKey.c_str(), it->second); - } - else - { - /* First time we see this SID list: program it into ApplDB and initialize the refcount to 1 */ - Srv6SidListTableFieldValueTupleWrapper fvw{srv6SidListTableKey}; - fvw.path = vpn_sid_str; + vector fvVectorSidList; + + auto it = m_srv6_sidlist_refcnt.find(sidlist); + if (it != m_srv6_sidlist_refcnt.end()) + { + /* SID list already exists: just bump the refcount */ + (it->second)++; + SWSS_LOG_INFO("Refcount for SID list'%s' increased to %u", + sidlist.c_str(), it->second); + } + else + { + /* First time we see this SID list: program it into ApplDB and initialize the refcount to 1 */ + Srv6SidListTableFieldValueTupleWrapper fvw{sidlist}; + fvw.path = sidlist; - setTable(fvw, m_srv6SidListTable); - SWSS_LOG_DEBUG("Srv6SidListTable set msg: %s path: %s", - srv6SidListTableKey.c_str(), vpn_sid_str.c_str()); + setTable(fvw, m_srv6SidListTable); + SWSS_LOG_DEBUG("Srv6SidListTable set msg: %s path: %s", + sidlist.c_str(), sidlist.c_str()); - m_srv6_sidlist_refcnt[srv6SidListTableKey] = 1; - SWSS_LOG_INFO("SID list '%s' created and refcount initialized to 1", - srv6SidListTableKey.c_str()); + m_srv6_sidlist_refcnt[sidlist] = 1; + SWSS_LOG_INFO("SID list '%s' created and refcount initialized to 1", + sidlist.c_str()); + } } /* Write route to ROUTE_TABLE */ @@ -1309,7 +1405,7 @@ void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) routeTableKeyStr.c_str(), vpn_sid_str.c_str(), src_addr_str.empty() ? "NONE" : src_addr_str.c_str()); RouteTableFieldValueTupleWrapper rfvw{std::move(routeTableKeyStr), ""}; - rfvw.segment = std::move(srv6SidListTableKey); + rfvw.segment = std::move(vpn_sid_str); if (!src_addr_str.empty()) { diff --git a/fpmsyncd/routesync.h b/fpmsyncd/routesync.h index 3c6f6ef1185..4437b0101c3 100644 --- a/fpmsyncd/routesync.h +++ b/fpmsyncd/routesync.h @@ -243,6 +243,8 @@ class RouteSync : public NetMsg } private: + /* ApplDB connector */ + shared_ptr m_appDb; /* ZMQ client */ shared_ptr m_zmqClient; /* regular route table */ @@ -376,6 +378,8 @@ class RouteSync : public NetMsg void getNextHopGroupFields(const NextHopGroup& nhg, string& nexthops, string& ifnames, string& weights, uint8_t af = AF_INET); void getPicContextGroupFields(const NextHopGroup& nhg, struct NextHopField& nhField, uint8_t af = AF_INET); + bool getSrv6SidListsFromRoute(string &routeTableKey, string &sidlists); + }; struct NextHopField { string nexthops; From afc8b5031b601df7b297b0d0c563243050c12f5c Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Wed, 26 Nov 2025 03:59:37 -0500 Subject: [PATCH 02/10] [tests]: Verify steering of IPv4 prefix over multiple SID lists Signed-off-by: Carmine Scarpitta --- tests/test_srv6.py | 154 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 123 insertions(+), 31 deletions(-) diff --git a/tests/test_srv6.py b/tests/test_srv6.py index 92174e5dda8..eed6e65006c 100644 --- a/tests/test_srv6.py +++ b/tests/test_srv6.py @@ -1511,6 +1511,13 @@ def create_vrf(self, vrf_name): def remove_vrf(self, vrf_name): self.cdb.delete_entry("VRF", vrf_name) + def set_interface_status(self, dvs, interface, admin_status): + tbl_name = "PORT" + tbl = swsscommon.Table(self.cdb.db_connection, tbl_name) + fvs = swsscommon.FieldValuePairs([("admin_status", "up")]) + tbl.set(interface, fvs) + time.sleep(1) + def setup_srv6(self, dvs): self.setup_db(dvs) @@ -1523,6 +1530,10 @@ def setup_srv6(self, dvs): dvs.runcmd("ip link add sr0 type dummy") dvs.runcmd("ip link set sr0 up") + # bring up interfaces + self.set_interface_status(dvs, "Ethernet104", "up") + self.set_interface_status(dvs, "Ethernet108", "up") + def teardown_srv6(self, dvs): # remove dummy interface sr0 dvs.runcmd("ip link del sr0 type dummy") @@ -1532,6 +1543,12 @@ def teardown_srv6(self, dvs): self.remove_vrf("Vrf13") self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER", len(initial_vrf_entries) - 1) + # put interfaces down + self.set_interface_status(dvs, "Ethernet104", "down") + self.set_interface_status(dvs, "Ethernet108", "down") + + time.sleep(3) + def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): _, output = dvs.runcmd(f"vtysh -c 'show zebra dplane providers'") @@ -1550,9 +1567,13 @@ def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): nexthop_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") route_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") sidlist_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") + nexthop_group_member_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER") + nexthop_group_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP") # create v4 route with vpn sid - dvs.runcmd("ip route add 192.0.2.0/24 encap seg6 mode encap segs fc00:0:1:e000:: dev sr0 vrf Vrf13") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ip route 192.0.2.0/24 Ethernet104 vrf Vrf13 nexthop-vrf default segments fc00:0:1:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ip route 192.0.3.0/24 Ethernet104 vrf Vrf13 nexthop-vrf default segments fc00:0:1:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ip route 192.0.3.0/24 Ethernet108 vrf Vrf13 nexthop-vrf default segments fc00:0:3:e000::\"") time.sleep(3) @@ -1561,51 +1582,113 @@ def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): expected_fields = {"segment": "fc00:0:1:e000::", "seg_src": "fc00:0:2::1"} self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:192.0.2.0/24", expected_fields) + self.pdb.wait_for_entry("ROUTE_TABLE", "Vrf13:192.0.3.0/24") + expected_fields = {"segment": "fc00:0:1:e000::,fc00:0:3:e000::", "seg_src": "fc00:0:2::1,fc00:0:2::1"} + self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:192.0.3.0/24", expected_fields) + self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fc00:0:1:e000::") expected_fields = {"path": "fc00:0:1:e000::"} self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fc00:0:1:e000::", expected_fields) + self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:e000::") + expected_fields = {"path": "fc00:0:3:e000::"} + self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fc00:0:3:e000::", expected_fields) + # verify that the route has been programmed into the ASIC self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 2) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 2) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 2) # get created entries - route_key = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries) - nexthop_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries) - tunnel_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries) - sidlist_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries) + route_keys = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries, 2) + nexthop_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries, 2) + tunnel_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries, 1) + sidlist_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries, 2) + nexthop_group_member_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER", nexthop_group_member_entries, 2) + nexthop_group_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP", nexthop_group_entries, 1) + + sidlist_id_1 = None + sidlist_id_2 = None + nexthop_id_1 = None + nexthop_id_2 = None + nexthop_group_member_id_1 = None + nexthop_group_member_id_2 = None + tunnel_id = tunnel_ids[0] + nexthop_group_id = nexthop_group_ids[0] # check ASIC SAI_OBJECT_TYPE_SRV6_SIDLIST database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") - (status, fvs) = tbl.get(sidlist_id) - assert status == True - for fv in fvs: - if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": - assert fv[1] == "1:fc00:0:1:e000::" - elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": - assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" - - # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") - (status, fvs) = tbl.get(route_key) - assert status == True - for fv in fvs: - if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": - assert fv[1] == nexthop_id + for sidlist_id in sidlist_ids: + (status, fvs) = tbl.get(sidlist_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": + if fv[1] == "1:fc00:0:1:e000::": + sidlist_id_1 = sidlist_id + elif fv[1] in "1:fc00:0:3:e000::": + sidlist_id_2 = sidlist_id + else: + assert False, "Sidlist %s not expected" % fv[1] + elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": + assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") - (status, fvs) = tbl.get(nexthop_id) + for nexthop_id in nexthop_ids: + (status, fvs) = tbl.get(nexthop_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": + assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" + if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": + if fv[1] == sidlist_id_1: + nexthop_id_1 = nexthop_id + elif fv[1] == sidlist_id_2: + nexthop_id_2 = nexthop_id + else: + assert False, "Nexthop with sidlist %s not expected" % fv[1] + elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": + assert fv[1] == tunnel_id + + # check ASIC SAI_OBJECT_TYPE_NEXT_HOP_GROUP database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP") + (status, fvs) = tbl.get(nexthop_group_id) assert status == True for fv in fvs: - if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": - assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" - if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": - assert fv[1] == sidlist_id - elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": - assert fv[1] == tunnel_id + if fv[0] == "SAI_NEXT_HOP_GROUP_ATTR_TYPE": + assert fv[1] == "SAI_NEXT_HOP_GROUP_TYPE_DYNAMIC_UNORDERED_ECMP" + + # check ASIC SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER") + for nexthop_group_member_id in nexthop_group_member_ids: + (status, fvs) = tbl.get(nexthop_group_member_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_GROUP_ID": + assert fv[1] == nexthop_group_id + if fv[0] == "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_ID": + if fv[1] == nexthop_id_1: + nexthop_group_member_id_1 = nexthop_group_member_id + elif fv[1] == nexthop_id_2: + nexthop_group_member_id_2 = nexthop_group_member_id + else: + assert False, "Nexthop group member for nexthop id %s not expected" % fv[1] + + # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + for route_key in route_keys: + route_dest = json.loads(route_key)["dest"] + (status, fvs) = tbl.get(route_key) + assert status == True + for fv in fvs: + if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": + if route_dest == "192.0.2.0/24": + assert fv[1] == nexthop_id_1 + elif route_dest == "192.0.3.0/24": + assert fv[1] == nexthop_group_id + else: + assert False, "Route with destination %s not expected" % route_dest # check ASIC SAI_OBJECT_TYPE_TUNNEL database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") @@ -1618,15 +1701,21 @@ def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): assert fv[1] == "fc00:0:2::1" # remove v4 route with vpn sid - dvs.runcmd("ip route del 192.0.2.0/24 encap seg6 mode encap segs fc00:0:1:e000:: dev sr0 vrf Vrf13") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ip route 192.0.2.0/24 Ethernet104 vrf Vrf13 nexthop-vrf default segments fc00:0:1:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ip route 192.0.3.0/24 Ethernet104 vrf Vrf13 nexthop-vrf default segments fc00:0:1:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ip route 192.0.3.0/24 Ethernet108 vrf Vrf13 nexthop-vrf default segments fc00:0:3:e000::\"") time.sleep(3) # check application database self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:192.0.2.0/24") + self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:192.0.3.0/24") self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fc00:0:1:e000::") + self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:e000::") # verify that the route has been removed from the ASIC + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP", len(nexthop_group_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER", len(nexthop_group_member_entries)) self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries)) self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries)) self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries)) @@ -1634,6 +1723,9 @@ def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): # unconfigure srv6 locator dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"no srv6\"") + # unconfigure loopback source address + dvs.runcmd("vtysh -c \"configure terminal\" -c \"interface lo\" -c \"no ip address fc00:0:2::1/128\"") + self.teardown_srv6(dvs) def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): From 957983aa57541ef882dd497f919be4cc5b2d7d06 Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Wed, 26 Nov 2025 04:09:14 -0500 Subject: [PATCH 03/10] [tests]: Verify steering of IPv6 prefix over multiple SID lists Signed-off-by: Carmine Scarpitta --- tests/test_srv6.py | 137 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 106 insertions(+), 31 deletions(-) diff --git a/tests/test_srv6.py b/tests/test_srv6.py index eed6e65006c..ff21cd35b50 100644 --- a/tests/test_srv6.py +++ b/tests/test_srv6.py @@ -1746,9 +1746,13 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): nexthop_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") route_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") sidlist_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") + nexthop_group_member_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER") + nexthop_group_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP") # create v6 route with vpn sid - dvs.runcmd("ip -6 route add 2001:db8:1:1::/64 encap seg6 mode encap segs fc00:0:1:e000:: dev sr0 vrf Vrf13") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ipv6 route 2001:db8:1:1::/64 Ethernet104 vrf Vrf13 nexthop-vrf default segments fc00:0:1:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ipv6 route 2001:db8:2:2::/64 Ethernet104 vrf Vrf13 nexthop-vrf default segments fc00:0:1:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ipv6 route 2001:db8:2:2::/64 Ethernet108 vrf Vrf13 nexthop-vrf default segments fc00:0:3:e000::\"") time.sleep(3) @@ -1757,51 +1761,113 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): expected_fields = {"segment": "fc00:0:1:e000::", "seg_src": "fc00:0:2::1"} self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:2001:db8:1:1::/64", expected_fields) + self.pdb.wait_for_entry("ROUTE_TABLE", "Vrf13:2001:db8:2:2::/64") + expected_fields = {"segment": "fc00:0:1:e000::,fc00:0:3:e000::", "seg_src": "fc00:0:2::1,fc00:0:2::1"} + self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:2001:db8:2:2::/64", expected_fields) + self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fc00:0:1:e000::") expected_fields = {"path": "fc00:0:1:e000::"} self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fc00:0:1:e000::", expected_fields) + self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:e000::") + expected_fields = {"path": "fc00:0:3:e000::"} + self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fc00:0:3:e000::", expected_fields) + # verify that the route has been programmed into the ASIC self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 2) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 2) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 2) # get created entries - route_key = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries) - nexthop_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries) - tunnel_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries) - sidlist_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries) + route_keys = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries, 2) + nexthop_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries, 2) + tunnel_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries, 1) + sidlist_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries, 2) + nexthop_group_member_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER", nexthop_group_member_entries, 2) + nexthop_group_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP", nexthop_group_entries, 1) + + sidlist_id_1 = None + sidlist_id_2 = None + nexthop_id_1 = None + nexthop_id_2 = None + nexthop_group_member_id_1 = None + nexthop_group_member_id_2 = None + tunnel_id = tunnel_ids[0] + nexthop_group_id = nexthop_group_ids[0] # check ASIC SAI_OBJECT_TYPE_SRV6_SIDLIST database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") - (status, fvs) = tbl.get(sidlist_id) - assert status == True - for fv in fvs: - if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": - assert fv[1] == "1:fc00:0:1:e000::" - elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": - assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" - - # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") - (status, fvs) = tbl.get(route_key) - assert status == True - for fv in fvs: - if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": - assert fv[1] == nexthop_id + for sidlist_id in sidlist_ids: + (status, fvs) = tbl.get(sidlist_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": + if fv[1] == "1:fc00:0:1:e000::": + sidlist_id_1 = sidlist_id + elif fv[1] in "1:fc00:0:3:e000::": + sidlist_id_2 = sidlist_id + else: + assert False, "Sidlist %s not expected" % fv[1] + elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": + assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") - (status, fvs) = tbl.get(nexthop_id) + for nexthop_id in nexthop_ids: + (status, fvs) = tbl.get(nexthop_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": + assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" + if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": + if fv[1] == sidlist_id_1: + nexthop_id_1 = nexthop_id + elif fv[1] == sidlist_id_2: + nexthop_id_2 = nexthop_id + else: + assert False, "Nexthop with sidlist %s not expected" % fv[1] + elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": + assert fv[1] == tunnel_id + + # check ASIC SAI_OBJECT_TYPE_NEXT_HOP_GROUP database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP") + (status, fvs) = tbl.get(nexthop_group_id) assert status == True for fv in fvs: - if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": - assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" - if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": - assert fv[1] == sidlist_id - elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": - assert fv[1] == tunnel_id + if fv[0] == "SAI_NEXT_HOP_GROUP_ATTR_TYPE": + assert fv[1] == "SAI_NEXT_HOP_GROUP_TYPE_DYNAMIC_UNORDERED_ECMP" + + # check ASIC SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER") + for nexthop_group_member_id in nexthop_group_member_ids: + (status, fvs) = tbl.get(nexthop_group_member_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_GROUP_ID": + assert fv[1] == nexthop_group_id + if fv[0] == "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_ID": + if fv[1] == nexthop_id_1: + nexthop_group_member_id_1 = nexthop_group_member_id + elif fv[1] == nexthop_id_2: + nexthop_group_member_id_2 = nexthop_group_member_id + else: + assert False, "Nexthop group member for nexthop id %s not expected" % fv[1] + + # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + for route_key in route_keys: + route_dest = json.loads(route_key)["dest"] + (status, fvs) = tbl.get(route_key) + assert status == True + for fv in fvs: + if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": + if route_dest == "2001:db8:1:1::/64": + assert fv[1] == nexthop_id_1 + elif route_dest == "2001:db8:2:2::/64": + assert fv[1] == nexthop_group_id + else: + assert False, "Route with destination %s not expected" % route_dest # check ASIC SAI_OBJECT_TYPE_TUNNEL database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") @@ -1814,15 +1880,21 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): assert fv[1] == "fc00:0:2::1" # remove v4 route with vpn sid - dvs.runcmd("ip route del 2001:db8:1:1::/64 encap seg6 mode encap segs fc00:0:1:e000:: dev sr0 vrf Vrf13") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ipv6 route 2001:db8:1:1::/64 Ethernet104 vrf Vrf13 nexthop-vrf default segments fc00:0:1:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ipv6 route 2001:db8:2:2::/64 Ethernet104 vrf Vrf13 nexthop-vrf default segments fc00:0:1:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ipv6 route 2001:db8:2:2::/64 Ethernet108 vrf Vrf13 nexthop-vrf default segments fc00:0:3:e000::\"") time.sleep(3) # check application database self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:2001:db8:1:1::/64") + self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:2001:db8:2:2::/64") self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fc00:0:1:e000::") + self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:e000::") # verify that the route has been removed from the ASIC + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP", len(nexthop_group_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER", len(nexthop_group_member_entries)) self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries)) self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries)) self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries)) @@ -1830,6 +1902,9 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): # unconfigure srv6 locator dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"no srv6\"") + # unconfigure loopback source address + dvs.runcmd("vtysh -c \"configure terminal\" -c \"interface lo\" -c \"no ip address fc00:0:2::1/128\"") + self.teardown_srv6(dvs) class TestSrv6Vpn(object): From 330626d75ad654dd8941124ee71cad71cfb3b018 Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Wed, 26 Nov 2025 04:10:03 -0500 Subject: [PATCH 04/10] [tests]: Verify SRv6 SID List sharing among multiple prefixes Signed-off-by: Carmine Scarpitta --- tests/test_srv6.py | 115 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/test_srv6.py b/tests/test_srv6.py index ff21cd35b50..141df94d7eb 100644 --- a/tests/test_srv6.py +++ b/tests/test_srv6.py @@ -1907,6 +1907,121 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): self.teardown_srv6(dvs) + def test_AddRemoveSrv6SteeringRouteSidlistSharing(self, dvs, testlog): + + _, output = dvs.runcmd(f"vtysh -c 'show zebra dplane providers'") + if 'dplane_fpm_sonic' not in output: + pytest.skip("'dplane_fpm_sonic' required for this test is not available, skipping", allow_module_level=True) + + self.setup_srv6(dvs) + + dvs.runcmd("vtysh -c \"configure terminal\" -c \"interface lo\" -c \"ip address fcbb:bbbb:1::1/128\"") + + # save exist asic db entries + tunnel_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") + nexthop_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") + route_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + sidlist_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") + + # configure routes to steer prefixes over the SID list + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ip route 192.0.2.0/24 sr0 vrf Vrf13 nexthop-vrf default segments fcbb:bbbb:2:3:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ip route 192.0.3.0/24 sr0 vrf Vrf13 nexthop-vrf default segments fcbb:bbbb:2:3:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ipv6 route 2001:db8:2:2::/64 sr0 vrf Vrf13 nexthop-vrf default segments fcbb:bbbb:2:3:e000::\"") + + time.sleep(3) + + # check application database + self.pdb.wait_for_entry("ROUTE_TABLE", "Vrf13:192.0.2.0/24") + expected_fields = {"segment": "fcbb:bbbb:2:3:e000::", "seg_src": "fcbb:bbbb:1::1"} + self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:192.0.2.0/24", expected_fields) + + self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fcbb:bbbb:2:3:e000::") + expected_fields = {"path": "fcbb:bbbb:2:3:e000::"} + self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fcbb:bbbb:2:3:e000::", expected_fields) + + # verify that the route has been programmed into the ASIC + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 3) + + # get created entries + route_keys = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries, 3) + nexthop_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries, 1) + tunnel_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries, 1) + sidlist_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries, 1) + + nexthop_id = nexthop_ids[0] + tunnel_id = tunnel_ids[0] + sidlist_id = sidlist_ids[0] + + # check ASIC SAI_OBJECT_TYPE_SRV6_SIDLIST database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") + (status, fvs) = tbl.get(sidlist_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": + assert fv[1] == "1:fcbb:bbbb:2:3:e000::" + elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": + assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" + + # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + for route_key in route_keys: + (status, fvs) = tbl.get(route_key) + assert status == True + for fv in fvs: + if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": + assert fv[1] == nexthop_id + + # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") + (status, fvs) = tbl.get(nexthop_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": + assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" + if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": + assert fv[1] == sidlist_id + elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": + assert fv[1] == tunnel_id + + # check ASIC SAI_OBJECT_TYPE_TUNNEL database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") + (status, fvs) = tbl.get(tunnel_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_TUNNEL_ATTR_TYPE": + assert fv[1] == "SAI_TUNNEL_TYPE_SRV6" + elif fv[0] == "SAI_TUNNEL_ATTR_ENCAP_SRC_IP": + assert fv[1] == "fcbb:bbbb:1::1" + + # remove routes + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ip route 192.0.2.0/24 sr0 vrf Vrf13 nexthop-vrf default segments fcbb:bbbb:2:3:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ip route 192.0.3.0/24 sr0 vrf Vrf13 nexthop-vrf default segments fcbb:bbbb:2:3:e000::\"") + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ipv6 route 2001:db8:2:2::/64 sr0 vrf Vrf13 nexthop-vrf default segments fcbb:bbbb:2:3:e000::\"") + + time.sleep(3) + + # check application database + self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:192.0.2.0/24") + self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:192.0.3.0/24") + self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:2001:db8:2:2::/64") + self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fcbb:bbbb:1:e000::") + + # verify that the route has been removed from the ASIC + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries)) + + # unconfigure srv6 locator + dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"no srv6\"") + + # unconfigure loopback source address + dvs.runcmd("vtysh -c \"configure terminal\" -c \"interface lo\" -c \"no ip address fcbb:bbbb:1::1/128\"") + + self.teardown_srv6(dvs) + class TestSrv6Vpn(object): def setup_db(self, dvs): self.pdb = dvs.get_app_db() From 9d839f8b855756dcb6c922c118589e29d63889d0 Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Thu, 27 Nov 2025 03:24:46 -0500 Subject: [PATCH 05/10] Address issues reported by CodeQL Signed-off-by: Carmine Scarpitta --- tests/test_srv6.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_srv6.py b/tests/test_srv6.py index 141df94d7eb..81a1ad88286 100644 --- a/tests/test_srv6.py +++ b/tests/test_srv6.py @@ -1632,6 +1632,8 @@ def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): assert False, "Sidlist %s not expected" % fv[1] elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" + assert sidlist_id_1 is not None + assert sidlist_id_2 is not None # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") @@ -1650,6 +1652,8 @@ def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): assert False, "Nexthop with sidlist %s not expected" % fv[1] elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": assert fv[1] == tunnel_id + assert nexthop_id_1 is not None + assert nexthop_id_2 is not None # check ASIC SAI_OBJECT_TYPE_NEXT_HOP_GROUP database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP") @@ -1674,6 +1678,8 @@ def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): nexthop_group_member_id_2 = nexthop_group_member_id else: assert False, "Nexthop group member for nexthop id %s not expected" % fv[1] + assert nexthop_group_member_id_1 is not None + assert nexthop_group_member_id_2 is not None # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") @@ -1689,6 +1695,8 @@ def test_AddRemoveSrv6SteeringRouteIpv4(self, dvs, testlog): assert fv[1] == nexthop_group_id else: assert False, "Route with destination %s not expected" % route_dest + assert nexthop_id_1 is not None + assert nexthop_group_id is not None # check ASIC SAI_OBJECT_TYPE_TUNNEL database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") @@ -1811,6 +1819,8 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): assert False, "Sidlist %s not expected" % fv[1] elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" + assert sidlist_id_1 is not None + assert sidlist_id_2 is not None # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") @@ -1829,6 +1839,8 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): assert False, "Nexthop with sidlist %s not expected" % fv[1] elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": assert fv[1] == tunnel_id + assert nexthop_id_1 is not None + assert nexthop_id_2 is not None # check ASIC SAI_OBJECT_TYPE_NEXT_HOP_GROUP database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP") @@ -1853,6 +1865,8 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): nexthop_group_member_id_2 = nexthop_group_member_id else: assert False, "Nexthop group member for nexthop id %s not expected" % fv[1] + assert nexthop_group_member_id_1 is not None + assert nexthop_group_member_id_2 is not None # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") @@ -1868,6 +1882,8 @@ def test_AddRemoveSrv6SteeringRouteIpv6(self, dvs, testlog): assert fv[1] == nexthop_group_id else: assert False, "Route with destination %s not expected" % route_dest + assert nexthop_id_1 is not None + assert nexthop_group_id is not None # check ASIC SAI_OBJECT_TYPE_TUNNEL database tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") From 71a7990f94f9e9ecd790f4fc4190ec355993fca9 Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Tue, 2 Dec 2025 11:38:37 -0500 Subject: [PATCH 06/10] [fpmsyncd]: Fix handling of SRv6 SID Lists with multiple SIDs There is a bug in fpmsyncd related to the processing of SRv6 SID lists containing multiple SIDs. When FRR attempts to program a SID list with multiple SRv6 SIDs, the FPM module correctly encodes all SIDs in the Netlink message. However, upon receiving this message, fpmsyncd processes only the first SID and installs only that SID into the ASIC. As a result, the ASIC is programmed with an incomplete and incorrect SID list. This pull request updates fpmsyncd to correctly handle and install SID lists containing multiple SRv6 SIDs. Signed-off-by: Carmine Scarpitta --- fpmsyncd/routesync.cpp | 44 +++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/fpmsyncd/routesync.cpp b/fpmsyncd/routesync.cpp index 31f180d0812..20cacfb74d3 100644 --- a/fpmsyncd/routesync.cpp +++ b/fpmsyncd/routesync.cpp @@ -112,10 +112,14 @@ enum { enum { ROUTE_ENCAP_SRV6_UNSPEC = 0, - ROUTE_ENCAP_SRV6_VPN_SID = 1, + ROUTE_ENCAP_SRV6_SIDS = 1, ROUTE_ENCAP_SRV6_ENCAP_SRC_ADDR = 2, - ROUTE_ENCAP_SRV6_PIC_ID = 3, - ROUTE_ENCAP_SRV6_NH_ID = 4, + ROUTE_ENCAP_SRV6_PIC_ID = 3, + ROUTE_ENCAP_SRV6_NH_ID = 4, + ROUTE_ENCAP_SRV6_ENCAP_SIDLIST_NAME = 5, + ROUTE_ENCAP_SRV6_ENCAP_SIDLIST_LEN = 6, + ROUTE_ENCAP_SRV6_ENCAP_SIDLIST = 7, + ROUTE_ENCAP_SRV6_NUM_SIDS = 8, }; #define MAX_MULTIPATH_NUM 514 @@ -277,13 +281,32 @@ void RouteSync::parseEncapSrv6SteerRoute(struct rtattr *tb, string &vpn_sid, struct rtattr *tb_encap[256] = {}; char vpn_sid_buf[MAX_ADDR_SIZE + 1] = {0}; char src_addr_buf[MAX_ADDR_SIZE + 1] = {0}; + uint8_t num_segs = 1; + struct in6_addr segs_buf[256] = {0}; + bool is_first_sid = true; parseRtAttrNested(tb_encap, 256, tb); - if (tb_encap[ROUTE_ENCAP_SRV6_VPN_SID]) + if (tb_encap[ROUTE_ENCAP_SRV6_NUM_SIDS]) { - vpn_sid += inet_ntop(AF_INET6, RTA_DATA(tb_encap[ROUTE_ENCAP_SRV6_VPN_SID]), - vpn_sid_buf, MAX_ADDR_SIZE); + num_segs = *(uint8_t *)RTA_DATA(tb_encap[ROUTE_ENCAP_SRV6_NUM_SIDS]); + } + + if (tb_encap[ROUTE_ENCAP_SRV6_SIDS]) + { + memset(segs_buf, 0, sizeof(segs_buf)); + memcpy(segs_buf, (char *)RTA_DATA(tb_encap[ROUTE_ENCAP_SRV6_SIDS]), + num_segs * 16); + + for (int i = 0; i < num_segs; i++) + { + if (!is_first_sid) + vpn_sid += "|"; + + vpn_sid += inet_ntop(AF_INET6, &segs_buf[i], vpn_sid_buf, MAX_ADDR_SIZE); + + is_first_sid = false; + } } if (tb_encap[ROUTE_ENCAP_SRV6_ENCAP_SRC_ADDR]) @@ -1512,13 +1535,16 @@ void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) } else { + string pathStr = sidlist; + boost::algorithm::replace_all(pathStr, "|", ","); + /* First time we see this SID list: program it into ApplDB and initialize the refcount to 1 */ - Srv6SidListTableFieldValueTupleWrapper fvw{sidlist, isNbZmqEnabled()}; - fvw.path = sidlist; + Srv6SidListTableFieldValueTupleWrapper fvw{sidlist}; + fvw.path = pathStr; setTable(fvw, m_srv6SidListTable); SWSS_LOG_DEBUG("Srv6SidListTable set msg: %s path: %s", - sidlist.c_str(), sidlist.c_str()); + sidlist.c_str(), pathStr.c_str()); m_srv6_sidlist_refcnt[sidlist] = 1; SWSS_LOG_INFO("SID list '%s' created and refcount initialized to 1", From 60e36b0df7a6065985e283f089bbdf3658e999f4 Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Fri, 20 Mar 2026 03:51:56 -0700 Subject: [PATCH 07/10] [tests]: Verify handling of SRv6 SID list with multiple SIDs Signed-off-by: Carmine Scarpitta --- tests/test_srv6.py | 246 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/tests/test_srv6.py b/tests/test_srv6.py index bea13d6c93b..add2a7d948c 100644 --- a/tests/test_srv6.py +++ b/tests/test_srv6.py @@ -2036,6 +2036,252 @@ def test_AddRemoveSrv6SteeringRouteSidlistSharing(self, dvs, testlog): self.teardown_srv6(dvs) + def test_AddRemoveSrv6SteeringRouteIpv4MultipleSids(self, dvs, testlog): + + _, output = dvs.runcmd(f"vtysh -c 'show zebra dplane providers'") + if 'dplane_fpm_sonic' not in output: + pytest.skip("'dplane_fpm_sonic' required for this test is not available, skipping", allow_module_level=True) + + self.setup_srv6(dvs) + + dvs.runcmd("vtysh -c \"configure terminal\" -c \"interface lo\" -c \"ip address fc00:0:2::1/128\"") + + # configure srv6 usid locator + dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"srv6\" -c \"locators\" -c \"locator loc1\" -c \"prefix fc00:0:2::/48 block-len 32 node-len 16 func-bits 16\" -c \"behavior usid\"") + + # save exist asic db entries + tunnel_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") + nexthop_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") + route_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + sidlist_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") + + # create v4 route with vpn sid + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ip route 192.0.4.0/24 sr0 vrf Vrf13 nexthop-vrf default segments fc00:0:3:4:5:6:7:8/fc00:0:9:a::\"") + + time.sleep(3) + + # check application database + self.pdb.wait_for_entry("ROUTE_TABLE", "Vrf13:192.0.4.0/24") + expected_fields = {"segment": "fc00:0:3:4:5:6:7:8|fc00:0:9:a::", "seg_src": "fc00:0:2::1"} + self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:192.0.4.0/24", expected_fields) + + self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::") + expected_fields = {"path": "fc00:0:3:4:5:6:7:8,fc00:0:9:a::"} + self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::", expected_fields) + + # verify that the route has been programmed into the ASIC + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 1) + + # get created entries + route_keys = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries, 1) + nexthop_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries, 1) + tunnel_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries) + sidlist_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries, 1) + + sidlist_id_1 = None + nexthop_id_1 = None + + # check ASIC SAI_OBJECT_TYPE_SRV6_SIDLIST database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") + for sidlist_id in sidlist_ids: + (status, fvs) = tbl.get(sidlist_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": + if fv[1] in "2:fc00:0:3:4:5:6:7:8,fc00:0:9:a::": + sidlist_id_1 = sidlist_id + else: + assert False, "Sidlist %s not expected" % fv[1] + elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": + assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" + assert sidlist_id_1 is not None + + # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") + for nexthop_id in nexthop_ids: + (status, fvs) = tbl.get(nexthop_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": + assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" + if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": + if fv[1] == sidlist_id_1: + nexthop_id_1 = nexthop_id + else: + assert False, "Nexthop with sidlist %s not expected" % fv[1] + elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": + assert fv[1] == tunnel_id + assert nexthop_id_1 is not None + + # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + for route_key in route_keys: + route_dest = json.loads(route_key)["dest"] + (status, fvs) = tbl.get(route_key) + assert status == True + for fv in fvs: + if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": + if route_dest == "192.0.4.0/24": + assert fv[1] == nexthop_id_1 + else: + assert False, "Route with destination %s not expected" % route_dest + + # check ASIC SAI_OBJECT_TYPE_TUNNEL database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") + (status, fvs) = tbl.get(tunnel_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_TUNNEL_ATTR_TYPE": + assert fv[1] == "SAI_TUNNEL_TYPE_SRV6" + elif fv[0] == "SAI_TUNNEL_ATTR_ENCAP_SRC_IP": + assert fv[1] == "fc00:0:2::1" + + # remove v4 route with vpn sid + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ip route 192.0.4.0/24 sr0 vrf Vrf13 nexthop-vrf default segments fc00:0:3:4:5:6:7:8/fc00:0:9:a::\"") + + time.sleep(3) + + # check application database + self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:192.0.4.0/24") + self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::") + + # verify that the route has been removed from the ASIC + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries)) + + # unconfigure srv6 locator + dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"no srv6\"") + + self.teardown_srv6(dvs) + + def test_AddRemoveSrv6SteeringRouteIpv6MultipleSids(self, dvs, testlog): + + _, output = dvs.runcmd(f"vtysh -c 'show zebra dplane providers'") + if 'dplane_fpm_sonic' not in output: + pytest.skip("'dplane_fpm_sonic' required for this test is not available, skipping", allow_module_level=True) + + self.setup_srv6(dvs) + + dvs.runcmd("vtysh -c \"configure terminal\" -c \"interface lo\" -c \"ip address fc00:0:2::1/128\"") + + # configure srv6 usid locator + dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"srv6\" -c \"locators\" -c \"locator loc1\" -c \"prefix fc00:0:2::/48 block-len 32 node-len 16 func-bits 16\" -c \"behavior usid\"") + + # save exist asic db entries + tunnel_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") + nexthop_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") + route_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + sidlist_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") + + # create v6 route with vpn sid + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ipv6 route 2001:db8:3:3::/64 sr0 vrf Vrf13 nexthop-vrf default segments fc00:0:3:4:5:6:7:8/fc00:0:9:a::\"") + + time.sleep(3) + + # check application database + self.pdb.wait_for_entry("ROUTE_TABLE", "Vrf13:2001:db8:3:3::/64") + expected_fields = {"segment": "fc00:0:3:4:5:6:7:8|fc00:0:9:a::", "seg_src": "fc00:0:2::1"} + self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:2001:db8:3:3::/64", expected_fields) + + self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::") + expected_fields = {"path": "fc00:0:3:4:5:6:7:8,fc00:0:9:a::"} + self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::", expected_fields) + + # verify that the route has been programmed into the ASIC + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 1) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 1) + + # get created entries + route_keys = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries, 1) + nexthop_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries, 1) + tunnel_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries) + sidlist_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries, 1) + + sidlist_id_1 = None + nexthop_id_1 = None + + # check ASIC SAI_OBJECT_TYPE_SRV6_SIDLIST database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") + for sidlist_id in sidlist_ids: + (status, fvs) = tbl.get(sidlist_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": + if fv[1] in "2:fc00:0:3:4:5:6:7:8,fc00:0:9:a::": + sidlist_id_1 = sidlist_id + else: + assert False, "Sidlist %s not expected" % fv[1] + elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": + assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" + assert sidlist_id_1 is not None + + # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") + for nexthop_id in nexthop_ids: + (status, fvs) = tbl.get(nexthop_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": + assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" + if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": + if fv[1] == sidlist_id_1: + nexthop_id_1 = nexthop_id + else: + assert False, "Nexthop with sidlist %s not expected" % fv[1] + elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": + assert fv[1] == tunnel_id + assert nexthop_id_1 is not None + + # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + for route_key in route_keys: + route_dest = json.loads(route_key)["dest"] + (status, fvs) = tbl.get(route_key) + assert status == True + for fv in fvs: + if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": + if route_dest == "2001:db8:3:3::/64": + assert fv[1] == nexthop_id_1 + else: + assert False, "Route with destination %s not expected" % route_dest + + # check ASIC SAI_OBJECT_TYPE_TUNNEL database + tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") + (status, fvs) = tbl.get(tunnel_id) + assert status == True + for fv in fvs: + if fv[0] == "SAI_TUNNEL_ATTR_TYPE": + assert fv[1] == "SAI_TUNNEL_TYPE_SRV6" + elif fv[0] == "SAI_TUNNEL_ATTR_ENCAP_SRC_IP": + assert fv[1] == "fc00:0:2::1" + + # remove v4 route with vpn sid + dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ipv6 route 2001:db8:3:3::/64 sr0 vrf Vrf13 nexthop-vrf default segments fc00:0:3:4:5:6:7:8/fc00:0:9:a::\"") + + time.sleep(3) + + # check application database + self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:2001:db8:3:3::/64") + self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::") + + # verify that the route has been removed from the ASIC + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries)) + self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries)) + + # unconfigure srv6 locator + dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"no srv6\"") + + self.teardown_srv6(dvs) + class TestSrv6Vpn(object): def setup_db(self, dvs): self.pdb = dvs.get_app_db() From cb84ffb956fb7e06973c87d08d5103d0d4707e09 Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Fri, 20 Mar 2026 04:04:54 -0700 Subject: [PATCH 08/10] Fix build errors Signed-off-by: Carmine Scarpitta --- fpmsyncd/routesync.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fpmsyncd/routesync.cpp b/fpmsyncd/routesync.cpp index 20cacfb74d3..82621f26488 100644 --- a/fpmsyncd/routesync.cpp +++ b/fpmsyncd/routesync.cpp @@ -1539,7 +1539,7 @@ void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) boost::algorithm::replace_all(pathStr, "|", ","); /* First time we see this SID list: program it into ApplDB and initialize the refcount to 1 */ - Srv6SidListTableFieldValueTupleWrapper fvw{sidlist}; + Srv6SidListTableFieldValueTupleWrapper fvw{sidlist, isNbZmqEnabled()}; fvw.path = pathStr; setTable(fvw, m_srv6SidListTable); From 93bdbb6d779b185bea6476038935e7cf26c92a1a Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Thu, 23 Jul 2026 12:08:57 +0200 Subject: [PATCH 09/10] Address review comments Signed-off-by: Carmine Scarpitta --- fpmsyncd/routesync.cpp | 349 ++++++++++++++----- fpmsyncd/routesync.h | 4 +- orchagent/routeorch.cpp | 2 +- tests/mock_tests/fpmsyncd/test_routesync.cpp | 7 +- 4 files changed, 278 insertions(+), 84 deletions(-) diff --git a/fpmsyncd/routesync.cpp b/fpmsyncd/routesync.cpp index 7ec31e3dc98..7199b01f64d 100644 --- a/fpmsyncd/routesync.cpp +++ b/fpmsyncd/routesync.cpp @@ -13,6 +13,7 @@ #include "fpmsyncd/fpm/fpm.h" #include "macaddress.h" #include "converter.h" +#include #include #include #include @@ -126,14 +127,10 @@ enum { enum { ROUTE_ENCAP_SRV6_UNSPEC = 0, - ROUTE_ENCAP_SRV6_SIDS = 1, + ROUTE_ENCAP_SRV6_VPN_SID = 1, ROUTE_ENCAP_SRV6_ENCAP_SRC_ADDR = 2, - ROUTE_ENCAP_SRV6_PIC_ID = 3, - ROUTE_ENCAP_SRV6_NH_ID = 4, - ROUTE_ENCAP_SRV6_ENCAP_SIDLIST_NAME = 5, - ROUTE_ENCAP_SRV6_ENCAP_SIDLIST_LEN = 6, - ROUTE_ENCAP_SRV6_ENCAP_SIDLIST = 7, - ROUTE_ENCAP_SRV6_NUM_SIDS = 8, + ROUTE_ENCAP_SRV6_PIC_ID = 3, + ROUTE_ENCAP_SRV6_NH_ID = 4, }; #define MAX_MULTIPATH_NUM 514 @@ -154,6 +151,105 @@ static string getProtocolString(int proto) return buffer; } +static bool parseRtAttrGateway(struct rtattr *gateway_attr, string &gateway) +{ + if (!gateway_attr) + { + return false; + } + + char gateway_buf[RouteSync::MAX_ADDR_SIZE + 1] = {}; + const void *gateway_data = RTA_DATA(gateway_attr); + const char *gateway_str = nullptr; + + if (RTA_PAYLOAD(gateway_attr) == IPV4_MAX_BYTE) + { + gateway_str = inet_ntop(AF_INET, gateway_data, gateway_buf, RouteSync::MAX_ADDR_SIZE); + } + else if (RTA_PAYLOAD(gateway_attr) == IPV6_MAX_BYTE) + { + gateway_str = inet_ntop(AF_INET6, gateway_data, gateway_buf, RouteSync::MAX_ADDR_SIZE); + } + else + { + SWSS_LOG_WARN("Received RTA_GATEWAY with unsupported payload length %zu", + static_cast(RTA_PAYLOAD(gateway_attr))); + return false; + } + + if (!gateway_str) + { + SWSS_LOG_WARN("Failed to parse RTA_GATEWAY address"); + return false; + } + + gateway = gateway_str; + return true; +} + +static bool parseRtAttrVia(struct rtattr *via_attr, string &gateway) +{ + if (!via_attr) + { + return false; + } + + size_t payload_len = static_cast(RTA_PAYLOAD(via_attr)); + size_t via_header_len = offsetof(struct rtvia, rtvia_addr); + size_t ipv4_addr_len = static_cast(IPV4_MAX_BYTE); + size_t ipv6_addr_len = static_cast(IPV6_MAX_BYTE); + if (payload_len <= via_header_len) + { + SWSS_LOG_WARN("Received RTA_VIA with unsupported payload length %zu", + payload_len); + return false; + } + + struct rtvia via = {}; + memcpy(&via, RTA_DATA(via_attr), via_header_len); + + char via_buf[RouteSync::MAX_ADDR_SIZE + 1] = {}; + const uint8_t *via_data = static_cast(RTA_DATA(via_attr)); + const void *via_addr = via_data + via_header_len; + size_t address_len = payload_len - via_header_len; + const char *via_str = nullptr; + + if (via.rtvia_family == AF_INET && address_len == ipv4_addr_len) + { + via_str = inet_ntop(AF_INET, via_addr, via_buf, RouteSync::MAX_ADDR_SIZE); + } + else if (via.rtvia_family == AF_INET6 && address_len == ipv6_addr_len) + { + via_str = inet_ntop(AF_INET6, via_addr, via_buf, RouteSync::MAX_ADDR_SIZE); + } + else + { + SWSS_LOG_WARN("Received RTA_VIA with unsupported family %d payload length %zu", + static_cast(via.rtvia_family), payload_len); + return false; + } + + if (!via_str) + { + SWSS_LOG_WARN("Failed to parse RTA_VIA address"); + return false; + } + + gateway = via_str; + return true; +} + +static bool parseRtAttrNexthop(struct rtattr *gateway_attr, struct rtattr *via_attr, + string &nexthop) +{ + if (parseRtAttrGateway(gateway_attr, nexthop)) + { + return true; + } + + return parseRtAttrVia(via_attr, nexthop); +} + /* Helper to create unique pointer with custom destructor */ template static decltype(auto) makeUniqueWithDestructor(T* ptr, F func) @@ -343,32 +439,13 @@ void RouteSync::parseEncapSrv6SteerRoute(struct rtattr *tb, string &vpn_sid, struct rtattr *tb_encap[256] = {}; char vpn_sid_buf[MAX_ADDR_SIZE + 1] = {0}; char src_addr_buf[MAX_ADDR_SIZE + 1] = {0}; - uint8_t num_segs = 1; - struct in6_addr segs_buf[256] = {0}; - bool is_first_sid = true; parseRtAttrNested(tb_encap, 256, tb); - if (tb_encap[ROUTE_ENCAP_SRV6_NUM_SIDS]) + if (tb_encap[ROUTE_ENCAP_SRV6_VPN_SID]) { - num_segs = *(uint8_t *)RTA_DATA(tb_encap[ROUTE_ENCAP_SRV6_NUM_SIDS]); - } - - if (tb_encap[ROUTE_ENCAP_SRV6_SIDS]) - { - memset(segs_buf, 0, sizeof(segs_buf)); - memcpy(segs_buf, (char *)RTA_DATA(tb_encap[ROUTE_ENCAP_SRV6_SIDS]), - num_segs * 16); - - for (int i = 0; i < num_segs; i++) - { - if (!is_first_sid) - vpn_sid += "|"; - - vpn_sid += inet_ntop(AF_INET6, &segs_buf[i], vpn_sid_buf, MAX_ADDR_SIZE); - - is_first_sid = false; - } + vpn_sid += inet_ntop(AF_INET6, RTA_DATA(tb_encap[ROUTE_ENCAP_SRV6_VPN_SID]), + vpn_sid_buf, MAX_ADDR_SIZE); } if (tb_encap[ROUTE_ENCAP_SRV6_ENCAP_SRC_ADDR]) @@ -1064,7 +1141,8 @@ void RouteSync::onEvpnRouteMsg(struct nlmsghdr *h, int len) bool RouteSync::getSrv6SteerRouteNextHop(struct nlmsghdr *h, int received_bytes, struct rtattr *tb[], string &vpn_sid, - string &src_addr) + string &src_addr, string &nexthops, + string &ifnames) { uint16_t encap = 0; @@ -1080,10 +1158,23 @@ bool RouteSync::getSrv6SteerRouteNextHop(struct nlmsghdr *h, int received_bytes, NH_ENCAP_SRV6_ROUTE) { parseEncapSrv6SteerRoute(tb[RTA_ENCAP], vpn_sid, src_addr); + parseRtAttrNexthop(tb[RTA_GATEWAY], tb[RTA_VIA], nexthops); + } + + if (tb[RTA_OIF]) + { + char if_name[IFNAMSIZ] = {0}; + int index = *(int *)RTA_DATA(tb[RTA_OIF]); + + if (getIfName(index, if_name, IFNAMSIZ)) + { + ifnames = if_name; + } } - SWSS_LOG_DEBUG("Rx MsgType:%d encap:%d vpn_sid:%s src_addr:%s", + + SWSS_LOG_DEBUG("Rx MsgType:%d encap:%d vpn_sid:%s src_addr:%s nexthops:%s ifnames:%s", h->nlmsg_type, encap, vpn_sid.c_str(), - src_addr.c_str()); + src_addr.c_str(), nexthops.c_str(), ifnames.c_str()); if (vpn_sid.empty()) { @@ -1102,9 +1193,11 @@ bool RouteSync::getSrv6SteerRouteNextHop(struct nlmsghdr *h, int received_bytes, for (;;) { - uint16_t encap = 0; + uint16_t nh_encap = 0; string nh_sid; string nh_src_addr; + string nh_nexthop; + string nh_ifname; if (len < (int)sizeof(*rtnh) || rtnh->rtnh_len > len) { @@ -1120,7 +1213,7 @@ bool RouteSync::getSrv6SteerRouteNextHop(struct nlmsghdr *h, int received_bytes, if (subtb[RTA_ENCAP_TYPE]) { - encap = *(uint16_t *)RTA_DATA(subtb[RTA_ENCAP_TYPE]); + nh_encap = *(uint16_t *)RTA_DATA(subtb[RTA_ENCAP_TYPE]); } if (subtb[RTA_ENCAP] && subtb[RTA_ENCAP_TYPE] && @@ -1128,25 +1221,44 @@ bool RouteSync::getSrv6SteerRouteNextHop(struct nlmsghdr *h, int received_bytes, NH_ENCAP_SRV6_ROUTE) { parseEncapSrv6SteerRoute(subtb[RTA_ENCAP], nh_sid, nh_src_addr); + parseRtAttrNexthop(subtb[RTA_GATEWAY], subtb[RTA_VIA], nh_nexthop); } - SWSS_LOG_DEBUG("Multipath nexthop encap:%d nh_sid:%s nh_src_addr:%s", - encap, nh_sid.c_str(), nh_src_addr.c_str()); - if (nh_sid.empty()) + if (rtnh->rtnh_ifindex) { - SWSS_LOG_ERROR("Received an invalid SRv6 nexthop: SID is missing. Skipping."); - continue; + char if_name[IFNAMSIZ] = {0}; + + if (getIfName(rtnh->rtnh_ifindex, if_name, IFNAMSIZ)) + { + nh_ifname = if_name; + } } - if (!first_nh) { - vpn_sid += ","; - src_addr += ","; + SWSS_LOG_DEBUG("Multipath nexthop encap:%d nh_sid:%s nh_src_addr:%s nh_nexthop:%s nh_ifname:%s", + nh_encap, nh_sid.c_str(), nh_src_addr.c_str(), + nh_nexthop.c_str(), nh_ifname.c_str()); + + if (nh_sid.empty()) + { + SWSS_LOG_ERROR("Received an invalid SRv6 nexthop: SID is missing. Skipping."); } + else + { + if (!first_nh) + { + vpn_sid += ","; + src_addr += ","; + nexthops += ","; + ifnames += ","; + } - vpn_sid += nh_sid; - src_addr += nh_src_addr; + vpn_sid += nh_sid; + src_addr += nh_src_addr; + nexthops += nh_nexthop; + ifnames += nh_ifname; - first_nh = false; + first_nh = false; + } } if (rtnh->rtnh_len == 0) @@ -1231,6 +1343,9 @@ RouteTableFieldValueTupleWrapper::fieldValueTupleVector() { if (nbZmqEnabled || includeEmptyFields || seg_src != string()) { fvVector.push_back(FieldValueTuple("seg_src", seg_src.c_str())); } + if (nbZmqEnabled || includeEmptyFields || vpn_sid != string()) { + fvVector.push_back(FieldValueTuple("vpn_sid", vpn_sid.c_str())); + } // Return value optimization will avoid copy of the following vector return fvVector; } @@ -1535,14 +1650,18 @@ void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) /* Get nexthop lists */ string vpn_sid_str; string src_addr_str; + string nexthop_str; + string ifname_str; bool ret; - ret = getSrv6SteerRouteNextHop(h, len, tb, vpn_sid_str, src_addr_str); + ret = getSrv6SteerRouteNextHop(h, len, tb, vpn_sid_str, src_addr_str, + nexthop_str, ifname_str); if (ret == false) { SWSS_LOG_NOTICE( - "SRv6 Route issue with RouteTable msg: %s vpn_sid:%s src_addr:%s", - destipprefix, vpn_sid_str.c_str(), src_addr_str.c_str()); + "SRv6 Route issue with RouteTable msg: %s vpn_sid:%s src_addr:%s nexthop:%s ifname:%s", + destipprefix, vpn_sid_str.c_str(), src_addr_str.c_str(), + nexthop_str.c_str(), ifname_str.c_str()); return; } @@ -1552,18 +1671,30 @@ void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) return; } + auto proto_str = getProtocolString(rtm->rtm_protocol); + bool is_bgp_srv6_route = (proto_str == "bgp"); + if (nlmsg_type == RTM_DELROUTE) { string routeTableKeyStr = string(routeTableKey); SWSS_LOG_INFO("SRV6 RouteTable del msg: %s", routeTableKeyStr.c_str()); + + if (!is_bgp_srv6_route) + { + getSrv6SidListsFromRoute(routeTableKeyStr, vpn_sid_str); + SWSS_LOG_INFO("Got Srv6 Sid list for route '%s' from route table: '%s'", + routeTableKeyStr.c_str(), vpn_sid_str.c_str()); + } + delWithWarmRestart( - RouteTableFieldValueTupleWrapper{std::move(routeTableKeyStr), std::string(), isNbZmqEnabled()}, + RouteTableFieldValueTupleWrapper{routeTableKeyStr, std::string(), isNbZmqEnabled()}, *m_routeTable); - getSrv6SidListsFromRoute(routeTableKeyStr, vpn_sid_str); - SWSS_LOG_INFO("Got Srv6 Sid list for route '%s' from route table: '%s'", - routeTableKeyStr.c_str(), vpn_sid_str.c_str()); + if (is_bgp_srv6_route) + { + return; + } /* Delete SID lists from SRV6_SID_LIST_TABLE */ vector sidlists = tokenize(vpn_sid_str, ','); @@ -1601,46 +1732,104 @@ void RouteSync::onSrv6SteerRouteMsg(struct nlmsghdr *h, int len) { string routeTableKeyStr = string(routeTableKey); - /* Write SID lists to SRV6_SID_LIST_TABLE */ - vector sidlists = tokenize(vpn_sid_str, ','); - for (auto sidlist : sidlists) + if (!is_bgp_srv6_route) { - vector fvVectorSidList; - - auto it = m_srv6_sidlist_refcnt.find(sidlist); - if (it != m_srv6_sidlist_refcnt.end()) + /* + * If this route already exists in APP_DB, it is an update rather than a new + * route (e.g. a multipath route losing one of its nexthops). Decrement the + * refcounts for the old SID lists before adding the new SID lists below. + */ + string old_sidlists_str; + if (getSrv6SidListsFromRoute(routeTableKeyStr, old_sidlists_str)) { - /* SID list already exists: just bump the refcount */ - (it->second)++; - SWSS_LOG_INFO("Refcount for SID list'%s' increased to %u", - sidlist.c_str(), it->second); + vector old_sidlists = tokenize(old_sidlists_str, ','); + for (auto& old_sidlist : old_sidlists) + { + auto it = m_srv6_sidlist_refcnt.find(old_sidlist); + if (it != m_srv6_sidlist_refcnt.end()) + { + assert (it->second > 0); + + /* Decrement the refcount for this SID list */ + (it->second)--; + SWSS_LOG_INFO("Route update: refcount for old SID list '%s' decreased to %u", + old_sidlist.c_str(), it->second); + + /* If the refcount drops to zero, remove the SID list from ApplDB */ + if (it->second == 0) + { + m_srv6SidListTable.del(old_sidlist); + SWSS_LOG_INFO("Route update: SID list '%s' removed from ApplDB (refcount zero)", + old_sidlist.c_str()); + + m_srv6_sidlist_refcnt.erase(it); + } + } + else + { + SWSS_LOG_WARN("Route update: old SID list '%s' not found in refcount map", + old_sidlist.c_str()); + } + } } - else - { - string pathStr = sidlist; - boost::algorithm::replace_all(pathStr, "|", ","); - /* First time we see this SID list: program it into ApplDB and initialize the refcount to 1 */ - Srv6SidListTableFieldValueTupleWrapper fvw{sidlist, isNbZmqEnabled()}; - fvw.path = pathStr; + /* Write SID lists to SRV6_SID_LIST_TABLE */ + vector sidlists = tokenize(vpn_sid_str, ','); + for (auto sidlist : sidlists) + { + auto it = m_srv6_sidlist_refcnt.find(sidlist); + if (it != m_srv6_sidlist_refcnt.end()) + { + /* SID list already exists: just bump the refcount */ + (it->second)++; + SWSS_LOG_INFO("Refcount for SID list'%s' increased to %u", + sidlist.c_str(), it->second); + } + else + { + /* First time we see this SID list: program it into ApplDB and initialize the refcount to 1 */ + Srv6SidListTableFieldValueTupleWrapper fvw{sidlist, isNbZmqEnabled()}; + fvw.path = sidlist; - setTable(fvw, m_srv6SidListTable); - SWSS_LOG_DEBUG("Srv6SidListTable set msg: %s path: %s", - sidlist.c_str(), pathStr.c_str()); + setTable(fvw, m_srv6SidListTable); + SWSS_LOG_DEBUG("Srv6SidListTable set msg: %s path: %s", + sidlist.c_str(), sidlist.c_str()); - m_srv6_sidlist_refcnt[sidlist] = 1; - SWSS_LOG_INFO("SID list '%s' created and refcount initialized to 1", - sidlist.c_str()); + m_srv6_sidlist_refcnt[sidlist] = 1; + SWSS_LOG_INFO("SID list '%s' created and refcount initialized to 1", + sidlist.c_str()); + } } } /* Write route to ROUTE_TABLE */ - SWSS_LOG_INFO("SRV6 RouteTable set msg: %s vpn_sid:%s src_addr:%s", + SWSS_LOG_INFO("SRV6 RouteTable set msg: %s vpn_sid:%s src_addr:%s nexthop:%s ifname:%s protocol:%s", routeTableKeyStr.c_str(), vpn_sid_str.c_str(), - src_addr_str.empty() ? "NONE" : src_addr_str.c_str()); - RouteTableFieldValueTupleWrapper rfvw{std::move(routeTableKeyStr), "", isNbZmqEnabled()}; - rfvw.segment = std::move(vpn_sid_str); + src_addr_str.empty() ? "NONE" : src_addr_str.c_str(), + nexthop_str.empty() ? "NONE" : nexthop_str.c_str(), + ifname_str.empty() ? "NONE" : ifname_str.c_str(), + proto_str.c_str()); + + RouteTableFieldValueTupleWrapper rfvw{std::move(routeTableKeyStr), + std::move(proto_str), + isNbZmqEnabled()}; + if (is_bgp_srv6_route) + { + rfvw.vpn_sid = std::move(vpn_sid_str); + if (!nexthop_str.empty()) + { + rfvw.nexthop = std::move(nexthop_str); + } + if (!ifname_str.empty()) + { + rfvw.ifname = std::move(ifname_str); + } + } + else + { + rfvw.segment = std::move(vpn_sid_str); + } if (!src_addr_str.empty()) { diff --git a/fpmsyncd/routesync.h b/fpmsyncd/routesync.h index 9320a9fb22d..bf88a874f4b 100644 --- a/fpmsyncd/routesync.h +++ b/fpmsyncd/routesync.h @@ -118,6 +118,7 @@ class RouteTableFieldValueTupleWrapper : public FieldValueTupleWrapperBase { string router_mac = string(); string segment = string(); string seg_src = string(); + string vpn_sid = string(); bool includeEmptyFields = false; }; @@ -367,7 +368,8 @@ class RouteSync : public NetMsg string& intf_list); bool getSrv6SteerRouteNextHop(struct nlmsghdr *h, int received_bytes, - struct rtattr *tb[], string &vpn_sid, string &src_addr); + struct rtattr *tb[], string &vpn_sid, string &src_addr, + string &nexthops, string &ifnames); bool getSrv6VpnRouteNextHop(struct nlmsghdr *h, int received_bytes, struct rtattr *tb[], uint32_t &pic_id,uint32_t &nhg_id); diff --git a/orchagent/routeorch.cpp b/orchagent/routeorch.cpp index 5ef3ba6f7ea..01a45c4b938 100644 --- a/orchagent/routeorch.cpp +++ b/orchagent/routeorch.cpp @@ -858,7 +858,7 @@ void RouteOrch::doTask(ConsumerBase& consumer) it = consumer.m_toSync.erase(it); continue; } - else if (alsv.size() != ipv.size()) + else if (!srv6_nh && (alsv.size() != ipv.size())) { SWSS_LOG_NOTICE("Route %s: resize ipv to match alsv, %zd -> %zd.", key.c_str(), ipv.size(), alsv.size()); ipv.resize(alsv.size()); diff --git a/tests/mock_tests/fpmsyncd/test_routesync.cpp b/tests/mock_tests/fpmsyncd/test_routesync.cpp index f22d462588c..a22361383ad 100644 --- a/tests/mock_tests/fpmsyncd/test_routesync.cpp +++ b/tests/mock_tests/fpmsyncd/test_routesync.cpp @@ -4655,8 +4655,8 @@ TEST_F(FpmSyncdResponseTest, TestRouteMsgWithZmqEnabled_AllFieldsIncluded) vector fvs; EXPECT_TRUE(route_table.get(test_destipprefix, fvs)); - // With ZMQ enabled, all 11 fields should be present (including empty ones) - EXPECT_EQ(fvs.size(), 11); + // With ZMQ enabled, all 12 fields should be present (including empty ones) + EXPECT_EQ(fvs.size(), 12); // Build a map for easier verification std::map fieldMap; @@ -4685,6 +4685,8 @@ TEST_F(FpmSyncdResponseTest, TestRouteMsgWithZmqEnabled_AllFieldsIncluded) EXPECT_EQ(fieldMap["segment"], ""); EXPECT_TRUE(fieldMap.count("seg_src") > 0); EXPECT_EQ(fieldMap["seg_src"], ""); + EXPECT_TRUE(fieldMap.count("vpn_sid") > 0); + EXPECT_EQ(fieldMap["vpn_sid"], ""); rtnl_route_put(test_route); @@ -4760,6 +4762,7 @@ TEST_F(FpmSyncdResponseTest, TestRouteMsgWithZmqDisabled_OnlyNonEmptyFields) EXPECT_EQ(fieldMap.count("router_mac"), 0); EXPECT_EQ(fieldMap.count("segment"), 0); EXPECT_EQ(fieldMap.count("seg_src"), 0); + EXPECT_EQ(fieldMap.count("vpn_sid"), 0); rtnl_route_put(test_route); } From 27f3d198f26e5b321a80abe8742ef1e87314faab Mon Sep 17 00:00:00 2001 From: Carmine Scarpitta Date: Thu, 23 Jul 2026 20:06:45 +0200 Subject: [PATCH 10/10] Address review comments Signed-off-by: Carmine Scarpitta --- tests/test_srv6.py | 246 --------------------------------------------- 1 file changed, 246 deletions(-) diff --git a/tests/test_srv6.py b/tests/test_srv6.py index add2a7d948c..bea13d6c93b 100644 --- a/tests/test_srv6.py +++ b/tests/test_srv6.py @@ -2036,252 +2036,6 @@ def test_AddRemoveSrv6SteeringRouteSidlistSharing(self, dvs, testlog): self.teardown_srv6(dvs) - def test_AddRemoveSrv6SteeringRouteIpv4MultipleSids(self, dvs, testlog): - - _, output = dvs.runcmd(f"vtysh -c 'show zebra dplane providers'") - if 'dplane_fpm_sonic' not in output: - pytest.skip("'dplane_fpm_sonic' required for this test is not available, skipping", allow_module_level=True) - - self.setup_srv6(dvs) - - dvs.runcmd("vtysh -c \"configure terminal\" -c \"interface lo\" -c \"ip address fc00:0:2::1/128\"") - - # configure srv6 usid locator - dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"srv6\" -c \"locators\" -c \"locator loc1\" -c \"prefix fc00:0:2::/48 block-len 32 node-len 16 func-bits 16\" -c \"behavior usid\"") - - # save exist asic db entries - tunnel_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") - nexthop_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") - route_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") - sidlist_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") - - # create v4 route with vpn sid - dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ip route 192.0.4.0/24 sr0 vrf Vrf13 nexthop-vrf default segments fc00:0:3:4:5:6:7:8/fc00:0:9:a::\"") - - time.sleep(3) - - # check application database - self.pdb.wait_for_entry("ROUTE_TABLE", "Vrf13:192.0.4.0/24") - expected_fields = {"segment": "fc00:0:3:4:5:6:7:8|fc00:0:9:a::", "seg_src": "fc00:0:2::1"} - self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:192.0.4.0/24", expected_fields) - - self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::") - expected_fields = {"path": "fc00:0:3:4:5:6:7:8,fc00:0:9:a::"} - self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::", expected_fields) - - # verify that the route has been programmed into the ASIC - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 1) - - # get created entries - route_keys = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries, 1) - nexthop_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries, 1) - tunnel_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries) - sidlist_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries, 1) - - sidlist_id_1 = None - nexthop_id_1 = None - - # check ASIC SAI_OBJECT_TYPE_SRV6_SIDLIST database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") - for sidlist_id in sidlist_ids: - (status, fvs) = tbl.get(sidlist_id) - assert status == True - for fv in fvs: - if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": - if fv[1] in "2:fc00:0:3:4:5:6:7:8,fc00:0:9:a::": - sidlist_id_1 = sidlist_id - else: - assert False, "Sidlist %s not expected" % fv[1] - elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": - assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" - assert sidlist_id_1 is not None - - # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") - for nexthop_id in nexthop_ids: - (status, fvs) = tbl.get(nexthop_id) - assert status == True - for fv in fvs: - if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": - assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" - if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": - if fv[1] == sidlist_id_1: - nexthop_id_1 = nexthop_id - else: - assert False, "Nexthop with sidlist %s not expected" % fv[1] - elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": - assert fv[1] == tunnel_id - assert nexthop_id_1 is not None - - # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") - for route_key in route_keys: - route_dest = json.loads(route_key)["dest"] - (status, fvs) = tbl.get(route_key) - assert status == True - for fv in fvs: - if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": - if route_dest == "192.0.4.0/24": - assert fv[1] == nexthop_id_1 - else: - assert False, "Route with destination %s not expected" % route_dest - - # check ASIC SAI_OBJECT_TYPE_TUNNEL database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") - (status, fvs) = tbl.get(tunnel_id) - assert status == True - for fv in fvs: - if fv[0] == "SAI_TUNNEL_ATTR_TYPE": - assert fv[1] == "SAI_TUNNEL_TYPE_SRV6" - elif fv[0] == "SAI_TUNNEL_ATTR_ENCAP_SRC_IP": - assert fv[1] == "fc00:0:2::1" - - # remove v4 route with vpn sid - dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ip route 192.0.4.0/24 sr0 vrf Vrf13 nexthop-vrf default segments fc00:0:3:4:5:6:7:8/fc00:0:9:a::\"") - - time.sleep(3) - - # check application database - self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:192.0.4.0/24") - self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::") - - # verify that the route has been removed from the ASIC - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries)) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries)) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries)) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries)) - - # unconfigure srv6 locator - dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"no srv6\"") - - self.teardown_srv6(dvs) - - def test_AddRemoveSrv6SteeringRouteIpv6MultipleSids(self, dvs, testlog): - - _, output = dvs.runcmd(f"vtysh -c 'show zebra dplane providers'") - if 'dplane_fpm_sonic' not in output: - pytest.skip("'dplane_fpm_sonic' required for this test is not available, skipping", allow_module_level=True) - - self.setup_srv6(dvs) - - dvs.runcmd("vtysh -c \"configure terminal\" -c \"interface lo\" -c \"ip address fc00:0:2::1/128\"") - - # configure srv6 usid locator - dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"srv6\" -c \"locators\" -c \"locator loc1\" -c \"prefix fc00:0:2::/48 block-len 32 node-len 16 func-bits 16\" -c \"behavior usid\"") - - # save exist asic db entries - tunnel_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") - nexthop_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") - route_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") - sidlist_entries = get_exist_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") - - # create v6 route with vpn sid - dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"ipv6 route 2001:db8:3:3::/64 sr0 vrf Vrf13 nexthop-vrf default segments fc00:0:3:4:5:6:7:8/fc00:0:9:a::\"") - - time.sleep(3) - - # check application database - self.pdb.wait_for_entry("ROUTE_TABLE", "Vrf13:2001:db8:3:3::/64") - expected_fields = {"segment": "fc00:0:3:4:5:6:7:8|fc00:0:9:a::", "seg_src": "fc00:0:2::1"} - self.pdb.wait_for_field_match("ROUTE_TABLE", "Vrf13:2001:db8:3:3::/64", expected_fields) - - self.pdb.wait_for_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::") - expected_fields = {"path": "fc00:0:3:4:5:6:7:8,fc00:0:9:a::"} - self.pdb.wait_for_field_match("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::", expected_fields) - - # verify that the route has been programmed into the ASIC - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries) + 1) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries) + 1) - - # get created entries - route_keys = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", route_entries, 1) - nexthop_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", nexthop_entries, 1) - tunnel_id = get_created_entry(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", tunnel_entries) - sidlist_ids = get_created_entries(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", sidlist_entries, 1) - - sidlist_id_1 = None - nexthop_id_1 = None - - # check ASIC SAI_OBJECT_TYPE_SRV6_SIDLIST database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST") - for sidlist_id in sidlist_ids: - (status, fvs) = tbl.get(sidlist_id) - assert status == True - for fv in fvs: - if fv[0] == "SAI_SRV6_SIDLIST_ATTR_SEGMENT_LIST": - if fv[1] in "2:fc00:0:3:4:5:6:7:8,fc00:0:9:a::": - sidlist_id_1 = sidlist_id - else: - assert False, "Sidlist %s not expected" % fv[1] - elif fv[0] == "SAI_SRV6_SIDLIST_ATTR_TYPE": - assert fv[1] == "SAI_SRV6_SIDLIST_TYPE_ENCAPS_RED" - assert sidlist_id_1 is not None - - # check ASIC SAI_OBJECT_TYPE_NEXT_HOP database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") - for nexthop_id in nexthop_ids: - (status, fvs) = tbl.get(nexthop_id) - assert status == True - for fv in fvs: - if fv[0] == "SAI_NEXT_HOP_ATTR_TYPE": - assert fv[1] == "SAI_NEXT_HOP_TYPE_SRV6_SIDLIST" - if fv[0] == "SAI_NEXT_HOP_ATTR_SRV6_SIDLIST_ID": - if fv[1] == sidlist_id_1: - nexthop_id_1 = nexthop_id - else: - assert False, "Nexthop with sidlist %s not expected" % fv[1] - elif fv[0] == "SAI_NEXT_HOP_ATTR_TUNNEL_ID": - assert fv[1] == tunnel_id - assert nexthop_id_1 is not None - - # check ASIC SAI_OBJECT_TYPE_ROUTE_ENTRY database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") - for route_key in route_keys: - route_dest = json.loads(route_key)["dest"] - (status, fvs) = tbl.get(route_key) - assert status == True - for fv in fvs: - if fv[0] == "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": - if route_dest == "2001:db8:3:3::/64": - assert fv[1] == nexthop_id_1 - else: - assert False, "Route with destination %s not expected" % route_dest - - # check ASIC SAI_OBJECT_TYPE_TUNNEL database - tbl = swsscommon.Table(self.adb.db_connection, "ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL") - (status, fvs) = tbl.get(tunnel_id) - assert status == True - for fv in fvs: - if fv[0] == "SAI_TUNNEL_ATTR_TYPE": - assert fv[1] == "SAI_TUNNEL_TYPE_SRV6" - elif fv[0] == "SAI_TUNNEL_ATTR_ENCAP_SRC_IP": - assert fv[1] == "fc00:0:2::1" - - # remove v4 route with vpn sid - dvs.runcmd("vtysh -c \"configure terminal\" vtysh -c \"no ipv6 route 2001:db8:3:3::/64 sr0 vrf Vrf13 nexthop-vrf default segments fc00:0:3:4:5:6:7:8/fc00:0:9:a::\"") - - time.sleep(3) - - # check application database - self.pdb.wait_for_deleted_entry("ROUTE_TABLE", "Vrf13:2001:db8:3:3::/64") - self.pdb.wait_for_deleted_entry("SRV6_SID_LIST_TABLE", "fc00:0:3:4:5:6:7:8|fc00:0:9:a::") - - # verify that the route has been removed from the ASIC - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP", len(nexthop_entries)) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_TUNNEL", len(tunnel_entries)) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", len(route_entries)) - self.adb.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SRV6_SIDLIST", len(sidlist_entries)) - - # unconfigure srv6 locator - dvs.runcmd("vtysh -c \"configure terminal\" -c \"segment-routing\" -c \"no srv6\"") - - self.teardown_srv6(dvs) - class TestSrv6Vpn(object): def setup_db(self, dvs): self.pdb = dvs.get_app_db()