diff --git a/fpmsyncd/fpmlink.cpp b/fpmsyncd/fpmlink.cpp index 1ed888d292..d90ea8aed6 100644 --- a/fpmsyncd/fpmlink.cpp +++ b/fpmsyncd/fpmlink.cpp @@ -281,6 +281,11 @@ void FpmLink::processFpmMessage(fpm_msg_hdr_t* hdr) /* EVPN Type5 Add route processing */ processRawMsg(nl_hdr); } + else if(nl_hdr->nlmsg_type == RTM_NEWNEXTHOP || nl_hdr->nlmsg_type == RTM_DELNEXTHOP) + { + /* rtnl api dont support RTM_NEWNEXTHOP/RTM_DELNEXTHOP yet. Processing as raw message*/ + processRawMsg(nl_hdr); + } else { NetDispatcher::getInstance().onNetlinkMessage(msg); @@ -320,4 +325,4 @@ bool FpmLink::send(nlmsghdr* nl_hdr) } return true; -} +} \ No newline at end of file diff --git a/fpmsyncd/routesync.cpp b/fpmsyncd/routesync.cpp index cea63dc42f..7c2afe5597 100644 --- a/fpmsyncd/routesync.cpp +++ b/fpmsyncd/routesync.cpp @@ -13,6 +13,7 @@ #include "converter.h" #include #include +#include using namespace std; using namespace swss; @@ -34,6 +35,11 @@ using namespace swss; ((struct rtattr *)(((char *)(r)) + NLMSG_ALIGN(sizeof(struct ndmsg)))) #endif +#ifndef NHA__RTA +#define NHA_RTA(r) \ + ((struct rtattr *)(((char *)(r)) + NLMSG_ALIGN(sizeof(struct nhmsg)))) +#endif + #define VXLAN_VNI 0 #define VXLAN_RMAC 1 #define NH_ENCAP_VXLAN 100 @@ -106,6 +112,8 @@ enum { ROUTE_ENCAP_SRV6_ENCAP_SRC_ADDR = 2, }; +#define MAX_MULTIPATH_NUM 514 + /* Returns name of the protocol passed number represents */ static string getProtocolString(int proto) { @@ -138,6 +146,7 @@ static decltype(auto) makeNlAddr(const T& ip) RouteSync::RouteSync(RedisPipeline *pipeline) : m_routeTable(pipeline, APP_ROUTE_TABLE_NAME, true), + m_nexthop_groupTable(pipeline, APP_NEXTHOP_GROUP_TABLE_NAME, true), m_label_routeTable(pipeline, APP_LABEL_ROUTE_TABLE_NAME, true), m_vnet_routeTable(pipeline, APP_VNET_RT_TABLE_NAME, true), m_vnet_tunnelTable(pipeline, APP_VNET_RT_TUNNEL_TABLE_NAME, true), @@ -1444,11 +1453,21 @@ void RouteSync::onMsgRaw(struct nlmsghdr *h) if ((h->nlmsg_type != RTM_NEWROUTE) && (h->nlmsg_type != RTM_DELROUTE) && (h->nlmsg_type != RTM_NEWSRV6LOCALSID) - && (h->nlmsg_type != RTM_DELSRV6LOCALSID)) + && (h->nlmsg_type != RTM_DELSRV6LOCALSID) + && (h->nlmsg_type != RTM_NEWNEXTHOP) + && (h->nlmsg_type != RTM_DELNEXTHOP) + ) return; + if(h->nlmsg_type == RTM_NEWNEXTHOP || h->nlmsg_type == RTM_DELNEXTHOP) + { + len = (int)(h->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg))); + } + else + { + len = (int)(h->nlmsg_len - NLMSG_LENGTH(sizeof(struct ndmsg))); + } /* Length validity. */ - len = (int)(h->nlmsg_len - NLMSG_LENGTH(sizeof(struct ndmsg))); if (len < 0) { SWSS_LOG_ERROR("%s: Message received from netlink is of a broken size %d %zu", @@ -1457,6 +1476,12 @@ void RouteSync::onMsgRaw(struct nlmsghdr *h) return; } + if(h->nlmsg_type == RTM_NEWNEXTHOP || h->nlmsg_type == RTM_DELNEXTHOP) + { + onNextHopMsg(h, len); + return; + } + if ((h->nlmsg_type == RTM_NEWSRV6LOCALSID) || (h->nlmsg_type == RTM_DELSRV6LOCALSID)) { @@ -1481,8 +1506,6 @@ void RouteSync::onMsgRaw(struct nlmsghdr *h) onEvpnRouteMsg(h, len); break; } - - return; } void RouteSync::onMsg(int nlmsg_type, struct nl_object *obj) @@ -1637,88 +1660,148 @@ void RouteSync::onRouteMsg(int nlmsg_type, struct nl_object *obj, char *vrf) return; } - struct nl_list_head *nhs = rtnl_route_get_nexthops(route_obj); - if (!nhs) - { - SWSS_LOG_INFO("Nexthop list is empty for %s", destipprefix); - return; - } - - /* Get nexthop lists */ + vector fvVector; string gw_list; string intf_list; string mpls_list; - getNextHopList(route_obj, gw_list, mpls_list, intf_list); - string weights = getNextHopWt(route_obj); - vector alsv = tokenize(intf_list, NHG_DELIMITER); - for (auto alias : alsv) + string nhg_id_key; + uint32_t nhg_id = rtnl_route_get_nh_id(route_obj); + if(nhg_id) { - /* - * An FRR behavior change from 7.2 to 7.5 makes FRR update default route to eth0 in interface - * up/down events. Skipping routes to eth0 or docker0 to avoid such behavior - */ - if (alias == "eth0" || alias == "docker0") - { - SWSS_LOG_DEBUG("Skip routes to eth0 or docker0: %s %s %s", - destipprefix, gw_list.c_str(), intf_list.c_str()); - // If intf_list has only this interface, that means all of the next hops of this route - // have been removed and the next hop on the eth0/docker0 has become the only next hop. - // In this case since we do not want the route with next hop on eth0/docker0, we return. - // But still we need to clear the route from the APPL_DB. Otherwise the APPL_DB and data - // path will be left with stale route entry - if(alsv.size() == 1) + const auto itg = m_nh_groups.find(nhg_id); + if(itg == m_nh_groups.end()) + { + SWSS_LOG_ERROR("NextHop group id %d not found. Dropping the route %s", nhg_id, destipprefix); + return; + } + NextHopGroup& nhg = itg->second; + if(nhg.group.size() == 0) + { + // Using route-table only for single next-hop + string nexthops = nhg.nexthop.empty() ? (rtnl_route_get_family(route_obj) == AF_INET ? "0.0.0.0" : "::") : nhg.nexthop; + string ifnames, weights; + + getNextHopGroupFields(nhg, nexthops, ifnames, weights, rtnl_route_get_family(route_obj)); + + FieldValueTuple gw("nexthop", nexthops.c_str()); + FieldValueTuple intf("ifname", ifnames.c_str()); + fvVector.push_back(gw); + fvVector.push_back(intf); + + SWSS_LOG_DEBUG("NextHop group id %d is a single nexthop address. Filling the route table %s with nexthop and ifname", nhg_id, destipprefix); + } + else + { + nhg_id_key = getNextHopGroupKeyAsString(nhg_id); + FieldValueTuple nhg("nexthop_group", nhg_id_key.c_str()); + fvVector.push_back(nhg); + installNextHopGroup(nhg_id); + } + + auto proto_num = rtnl_route_get_protocol(route_obj); + auto proto_str = getProtocolString(proto_num); + FieldValueTuple proto("protocol", proto_str); + fvVector.push_back(proto); + + } + else + { + struct nl_list_head *nhs = rtnl_route_get_nexthops(route_obj); + if (!nhs) + { + SWSS_LOG_INFO("Nexthop list is empty for %s", destipprefix); + return; + } + + /* Get nexthop lists */ + + getNextHopList(route_obj, gw_list, mpls_list, intf_list); + string weights = getNextHopWt(route_obj); + + vector alsv = tokenize(intf_list, NHG_DELIMITER); + + if (alsv.size() == 1) + { + if (alsv[0] == "eth0" || alsv[0] == "docker0") { + SWSS_LOG_DEBUG("Skip routes to eth0 or docker0: %s %s %s", + destipprefix, gw_list.c_str(), intf_list.c_str()); + if (!warmRestartInProgress) { SWSS_LOG_NOTICE("RouteTable del msg for route with only one nh on eth0/docker0: %s %s %s %s", - destipprefix, gw_list.c_str(), intf_list.c_str(), mpls_list.c_str()); + destipprefix, gw_list.c_str(), intf_list.c_str(), mpls_list.c_str()); m_routeTable.del(destipprefix); } else { SWSS_LOG_NOTICE("Warm-Restart mode: Receiving delete msg for route with only nh on eth0/docker0: %s %s %s %s", - destipprefix, gw_list.c_str(), intf_list.c_str(), mpls_list.c_str()); + destipprefix, gw_list.c_str(), intf_list.c_str(), mpls_list.c_str()); vector fvVector; const KeyOpFieldsValuesTuple kfv = std::make_tuple(destipprefix, - DEL_COMMAND, - fvVector); + DEL_COMMAND, + fvVector); m_warmStartHelper.insertRefreshMap(kfv); } + return; + } + } + else + { + for (auto alias : alsv) + { + /* + * A change in FRR behavior from version 7.2 to 7.5 causes the default route to be updated to eth0 + * during interface up/down events. This skips routes to eth0 or docker0 to avoid such behavior. + */ + if (alias == "eth0" || alias == "docker0") + { + SWSS_LOG_DEBUG("Skip routes to eth0 or docker0: %s %s %s", + destipprefix, gw_list.c_str(), intf_list.c_str()); + continue; + } } - return; } - } - auto proto_num = rtnl_route_get_protocol(route_obj); - auto proto_str = getProtocolString(proto_num); + auto proto_num = rtnl_route_get_protocol(route_obj); + auto proto_str = getProtocolString(proto_num); - vector fvVector; - FieldValueTuple proto("protocol", proto_str); - FieldValueTuple gw("nexthop", gw_list); - FieldValueTuple intf("ifname", intf_list); - fvVector.push_back(proto); - fvVector.push_back(gw); - fvVector.push_back(intf); - if (!mpls_list.empty()) - { - FieldValueTuple mpls_nh("mpls_nh", mpls_list); - fvVector.push_back(mpls_nh); - } - if (!weights.empty()) - { - FieldValueTuple wt("weight", weights); - fvVector.push_back(wt); + FieldValueTuple proto("protocol", proto_str); + FieldValueTuple gw("nexthop", gw_list); + FieldValueTuple intf("ifname", intf_list); + + fvVector.push_back(proto); + fvVector.push_back(gw); + fvVector.push_back(intf); + if (!mpls_list.empty()) + { + FieldValueTuple mpls_nh("mpls_nh", mpls_list); + fvVector.push_back(mpls_nh); + } + if (!weights.empty()) + { + FieldValueTuple wt("weight", weights); + fvVector.push_back(wt); + } } if (!warmRestartInProgress) { - m_routeTable.set(destipprefix, fvVector); - SWSS_LOG_DEBUG("RouteTable set msg: %s %s %s %s", destipprefix, + if(nhg_id) + { + m_routeTable.set(destipprefix, fvVector); + SWSS_LOG_INFO("RouteTable set msg: %s %d ", destipprefix, nhg_id); + } + else + { + m_routeTable.set(destipprefix, fvVector); + SWSS_LOG_INFO("RouteTable set msg: %s %s %s %s", destipprefix, gw_list.c_str(), intf_list.c_str(), mpls_list.c_str()); + } } /* @@ -1737,7 +1820,131 @@ void RouteSync::onRouteMsg(int nlmsg_type, struct nl_object *obj, char *vrf) } } -/* +/* + * Handle Nexthop msg + * @arg nlmsghdr Netlink messaged + */ +void RouteSync::onNextHopMsg(struct nlmsghdr *h, int len) +{ + int nlmsg_type = h->nlmsg_type; + uint32_t id = 0; + unsigned char addr_family; + int32_t ifindex = -1, grp_count = 0; + string ifname; + struct nhmsg *nhm = NULL; + struct rtattr *tb[NHA_MAX + 1] = {}; + struct in_addr ipv4 = {0}; + struct in6_addr ipv6 = {0}; + char gateway[INET6_ADDRSTRLEN] = {0}; + char ifname_unknown[IFNAMSIZ] = "unknown"; + + nhm = (struct nhmsg *)NLMSG_DATA(h); + + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wcast-align" + struct rtattr* rta = NHA_RTA(nhm); + #pragma GCC diagnostic pop + + netlink_parse_rtattr(tb, NHA_MAX, rta, len); + + if (!tb[NHA_ID]) { + SWSS_LOG_ERROR( + "Nexthop group without an ID received from the zebra"); + return; + } + + /* We use the ID key'd nhg table for kernel updates */ + id = *((uint32_t *)RTA_DATA(tb[NHA_ID])); + + addr_family = nhm->nh_family; + + if (nlmsg_type == RTM_NEWNEXTHOP) + { + if (tb[NHA_GROUP]) + { + SWSS_LOG_INFO("New nexthop group message!"); + + struct nexthop_grp *nha_grp = (struct nexthop_grp *)RTA_DATA(tb[NHA_GROUP]); + grp_count = (int)(RTA_PAYLOAD(tb[NHA_GROUP]) / sizeof(*nha_grp)); + + if (grp_count > MAX_MULTIPATH_NUM) + { + SWSS_LOG_ERROR("Nexthop group count (%d) exceeds the maximum allowed (%d). Clamping to maximum.", grp_count, MAX_MULTIPATH_NUM); + grp_count = MAX_MULTIPATH_NUM; + } + + vector> group(grp_count); + for (int i = 0; i < grp_count; i++) + { + group[i] = std::make_pair(nha_grp[i].id, nha_grp[i].weight + 1); + } + + auto it = m_nh_groups.find(id); + if (it != m_nh_groups.end()) + { + NextHopGroup &nhg = it->second; + nhg.group = group; + if (nhg.installed) + { + updateNextHopGroupDb(nhg); + } + } + else + { + m_nh_groups.insert({id, NextHopGroup(id, group)}); + } + } + else + { + if (tb[NHA_GATEWAY]) + { + if (addr_family == AF_INET) + { + memcpy(&ipv4, (void *)RTA_DATA(tb[NHA_GATEWAY]), 4); + inet_ntop(AF_INET, &ipv4, gateway, INET_ADDRSTRLEN); + } + else if (addr_family == AF_INET6) + { + memcpy(&ipv6, (void *)RTA_DATA(tb[NHA_GATEWAY]), 16); + inet_ntop(AF_INET6, &ipv6, gateway, INET6_ADDRSTRLEN); + } + else + { + SWSS_LOG_ERROR("Unexpected nexthop address family"); + return; + } + } + + if (tb[NHA_OIF]) + { + ifindex = *((int32_t *)RTA_DATA(tb[NHA_OIF])); + char if_name[IFNAMSIZ] = {0}; + if (!getIfName(ifindex, if_name, IFNAMSIZ)) + { + strcpy(if_name, ifname_unknown); + } + ifname = string(if_name); + if (ifname == "eth0" || ifname == "docker0") + { + SWSS_LOG_DEBUG("Skip routes to interface: %s id[%d]", ifname.c_str(), id); + return; + } + } + + SWSS_LOG_DEBUG("Received: id[%d], if[%d/%s] address[%s]", id, ifindex, ifname.c_str(), gateway); + m_nh_groups.insert({id, NextHopGroup(id, string(gateway), ifname)}); + } + } + else if (nlmsg_type == RTM_DELNEXTHOP) + { + SWSS_LOG_DEBUG("NextHopGroup del event: %d", id); + deleteNextHopGroup(id); + } + + return; +} + +/* * Handle label route * @arg nlmsg_type Netlink message type * @arg obj Netlink object @@ -2404,3 +2611,140 @@ void RouteSync::onWarmStartEnd(DBConnector& applStateDb) SWSS_LOG_NOTICE("Warm-Restart reconciliation processed."); } } + +/* + * Get nexthop group key as string + * @arg id next hop group id + * + * Return nexthop group key + */ +const string RouteSync::getNextHopGroupKeyAsString(uint32_t id) const +{ + return to_string(id); +} + +/* + * update the nexthop group entry + * @arg nh_id nexthop group id + * + */ +void RouteSync::installNextHopGroup(uint32_t nh_id) +{ + auto git = m_nh_groups.find(nh_id); + if(git == m_nh_groups.end()) + { + SWSS_LOG_ERROR("Nexthop not found: %d", nh_id); + return; + } + + NextHopGroup& nhg = git->second; + + if(nhg.installed) + { + //Nexthop group already installed + return; + } + nhg.installed = true; + updateNextHopGroupDb(nhg); +} + +/* + * delete the nexthop group entry + * @arg nh_id nexthop group id + * + */ +void RouteSync::deleteNextHopGroup(uint32_t nh_id) +{ + auto git = m_nh_groups.find(nh_id); + if(git == m_nh_groups.end()) + { + SWSS_LOG_ERROR("Nexthop not found: %d", nh_id); + return; + } + + NextHopGroup& nhg = git->second; + + if(nhg.installed) + { + string key = getNextHopGroupKeyAsString(nh_id); + m_nexthop_groupTable.del(key.c_str()); + SWSS_LOG_DEBUG("NextHopGroup table del: key [%s]", key.c_str()); + } + m_nh_groups.erase(git); +} + +/* + * update the nexthop group table in database + * @arg nhg the nexthop group + * + */ +void RouteSync::updateNextHopGroupDb(const NextHopGroup& nhg) +{ + vector fvVector; + string nexthops; + string ifnames; + string weights; + string key = getNextHopGroupKeyAsString(nhg.id); + getNextHopGroupFields(nhg, nexthops, ifnames, weights); + + FieldValueTuple nh("nexthop", nexthops.c_str()); + FieldValueTuple ifname("ifname", ifnames.c_str()); + fvVector.push_back(nh); + fvVector.push_back(ifname); + if(!weights.empty()) + { + FieldValueTuple wg("weight", weights.c_str()); + fvVector.push_back(wg); + } + SWSS_LOG_INFO("NextHopGroup table set: key [%s] nexthop[%s] ifname[%s] weight[%s]", key.c_str(), nexthops.c_str(), ifnames.c_str(), weights.c_str()); + + m_nexthop_groupTable.set(key.c_str(), fvVector); +} + +/* + * generate the database fields. + * @arg nhg the nexthop group + * + */ +void RouteSync::getNextHopGroupFields(const NextHopGroup& nhg, string& nexthops, string& ifnames, string& weights, uint8_t af /*= AF_INET*/) +{ + if(nhg.group.size() == 0) + { + if(!nhg.nexthop.empty()) + { + nexthops = nhg.nexthop; + } + else + { + nexthops = af == AF_INET ? "0.0.0.0" : "::"; + } + ifnames = nhg.intf; + } + else + { + int i = 0; + for(const auto& nh : nhg.group) + { + uint32_t id = nh.first; + auto itr = m_nh_groups.find(id); + if(itr == m_nh_groups.end()) + { + SWSS_LOG_ERROR("NextHop group is incomplete: %d", nhg.id); + return; + } + + NextHopGroup& nhgr = itr->second; + string weight = to_string(nh.second); + if(i) + { + nexthops += NHG_DELIMITER; + ifnames += NHG_DELIMITER; + weights += NHG_DELIMITER; + } + nexthops += nhgr.nexthop.empty() ? (af == AF_INET ? "0.0.0.0" : "::") : nhgr.nexthop; + ifnames += nhgr.intf; + weights += weight; + ++i; + } + } +} \ No newline at end of file diff --git a/fpmsyncd/routesync.h b/fpmsyncd/routesync.h index fe67f6acfc..fbc042d009 100644 --- a/fpmsyncd/routesync.h +++ b/fpmsyncd/routesync.h @@ -9,6 +9,7 @@ #include "warmRestartHelper.h" #include #include +#include #include @@ -26,6 +27,16 @@ extern void netlink_parse_rtattr(struct rtattr **tb, int max, struct rtattr *rta namespace swss { +struct NextHopGroup { + uint32_t id; + vector> group; + string nexthop; + string intf; + bool installed; + NextHopGroup(uint32_t id, const string& nexthop, const string& interface) : installed(false), id(id), nexthop(nexthop), intf(interface) {}; + NextHopGroup(uint32_t id, const vector>& group) : installed(false), id(id), group(group) {}; +}; + /* Path to protocol name database provided by iproute2 */ constexpr auto DefaultRtProtoPath = "/etc/iproute2/rt_protos"; @@ -81,6 +92,9 @@ class RouteSync : public NetMsg ProducerStateTable m_srv6SidListTable; struct nl_cache *m_link_cache; struct nl_sock *m_nl_sock; + /* nexthop group table */ + ProducerStateTable m_nexthop_groupTable; + map m_nh_groups; bool m_isSuppressionEnabled{false}; FpmInterface* m_fpmInterface {nullptr}; @@ -123,7 +137,7 @@ class RouteSync : public NetMsg void onVnetRouteMsg(int nlmsg_type, struct nl_object *obj, string vnet); /* Get interface name based on interface index */ - bool getIfName(int if_index, char *if_name, size_t name_len); + virtual bool getIfName(int if_index, char *if_name, size_t name_len); /* Get interface if_index based on interface name */ rtnl_link* getLinkByName(const char *name); @@ -169,8 +183,17 @@ class RouteSync : public NetMsg uint16_t getEncapType(struct nlmsghdr *h); const char *mySidAction2Str(uint32_t action); + + /* Handle Nexthop message */ + void onNextHopMsg(struct nlmsghdr *h, int len); + /* Get next hop group key */ + const string getNextHopGroupKeyAsString(uint32_t id) const; + void installNextHopGroup(uint32_t nh_id); + void deleteNextHopGroup(uint32_t nh_id); + void updateNextHopGroupDb(const NextHopGroup& nhg); + void getNextHopGroupFields(const NextHopGroup& nhg, string& nexthops, string& ifnames, string& weights, uint8_t af = AF_INET); }; } -#endif +#endif \ No newline at end of file diff --git a/tests/mock_tests/fpmsyncd/test_routesync.cpp b/tests/mock_tests/fpmsyncd/test_routesync.cpp index b1c23aca85..147900a48e 100644 --- a/tests/mock_tests/fpmsyncd/test_routesync.cpp +++ b/tests/mock_tests/fpmsyncd/test_routesync.cpp @@ -1,5 +1,5 @@ #include "redisutility.h" - +#include "ut_helpers_fpmsyncd.h" #include #include #include "mock_table.h" @@ -7,13 +7,25 @@ #include "fpmsyncd/routesync.h" #undef private +#include +#include +#include +#include +#include + +#include + using namespace swss; +using namespace testing; + #define MAX_PAYLOAD 1024 using ::testing::_; int rt_build_ret = 0; bool nlmsg_alloc_ret = true; +#pragma GCC diagnostic ignored "-Wcast-align" + class MockRouteSync : public RouteSync { public: @@ -28,6 +40,7 @@ class MockRouteSync : public RouteSync rtattr *[], std::string&, std::string& , std::string&, std::string&), (override)); + MOCK_METHOD(bool, getIfName, (int, char *, size_t), (override)); }; class MockFpm : public FpmInterface { @@ -69,6 +82,10 @@ class FpmSyncdResponseTest : public ::testing::Test RouteSync m_routeSync{m_pipeline.get()}; MockFpm m_mockFpm{&m_routeSync}; MockRouteSync m_mockRouteSync{m_pipeline.get()}; + + const char* test_gateway = "192.168.1.1"; + const char* test_gateway_ = "192.168.1.2"; + const char* test_gateway__ = "192.168.1.3"; }; TEST_F(FpmSyncdResponseTest, RouteResponseFeedbackV4) @@ -224,6 +241,7 @@ TEST_F(FpmSyncdResponseTest, testEvpn) return true; }); m_mockRouteSync.onMsgRaw(nlh); + vector keys; vector fieldValues; app_route_table.getKeys(keys); @@ -237,15 +255,652 @@ TEST_F(FpmSyncdResponseTest, testEvpn) TEST_F(FpmSyncdResponseTest, testSendOffloadReply) { - rt_build_ret = 1; rtnl_route* routeObject{}; - ASSERT_EQ(m_routeSync.sendOffloadReply(routeObject), false); rt_build_ret = 0; nlmsg_alloc_ret = false; ASSERT_EQ(m_routeSync.sendOffloadReply(routeObject), false); nlmsg_alloc_ret = true; +} + +struct nlmsghdr* createNewNextHopMsgHdr(int32_t ifindex, const char* gateway, uint32_t id, unsigned char nh_family=AF_INET) { + struct nlmsghdr *nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); + memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); + + // Set header + nlh->nlmsg_type = RTM_NEWNEXTHOP; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE; + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)); + + // Set nhmsg + struct nhmsg *nhm = (struct nhmsg *)NLMSG_DATA(nlh); + nhm->nh_family = nh_family; + + // Add NHA_ID + struct rtattr *rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = NHA_ID; + rta->rta_len = RTA_LENGTH(sizeof(uint32_t)); + *(uint32_t *)RTA_DATA(rta) = id; + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Add NHA_OIF + rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = NHA_OIF; + rta->rta_len = RTA_LENGTH(sizeof(int32_t)); + *(int32_t *)RTA_DATA(rta) = ifindex; + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Add NHA_GATEWAY + rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = NHA_GATEWAY; + if (nh_family == AF_INET6) + { + struct in6_addr gw_addr6; + inet_pton(AF_INET6, gateway, &gw_addr6); + rta->rta_len = RTA_LENGTH(sizeof(struct in6_addr)); + memcpy(RTA_DATA(rta), &gw_addr6, sizeof(struct in6_addr)); + } + else + { + struct in_addr gw_addr; + inet_pton(AF_INET, gateway, &gw_addr); + rta->rta_len = RTA_LENGTH(sizeof(struct in_addr)); + memcpy(RTA_DATA(rta), &gw_addr, sizeof(struct in_addr)); + } + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + return nlh; +} + +TEST_F(FpmSyncdResponseTest, TestNoNHAId) +{ + struct nlmsghdr *nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); + memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); + + nlh->nlmsg_type = RTM_NEWNEXTHOP; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE; + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)); + struct nhmsg *nhm = (struct nhmsg *)NLMSG_DATA(nlh); + nhm->nh_family = AF_INET; + + EXPECT_CALL(m_mockRouteSync, getIfName(_, _, _)) + .Times(0); + + m_mockRouteSync.onNextHopMsg(nlh, 0); + + free(nlh); +} + +TEST_F(FpmSyncdResponseTest, TestNextHopAdd) +{ + uint32_t test_id = 10; + int32_t test_ifindex = 5; + + struct nlmsghdr* nlh = createNewNextHopMsgHdr(test_ifindex, test_gateway, test_id); + int expected_length = (int)(nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg))); + + EXPECT_CALL(m_mockRouteSync, getIfName(test_ifindex, _, _)) + .WillOnce(DoAll( + [](int32_t, char* ifname, size_t size) { + strncpy(ifname, "Ethernet1", size); + ifname[size-1] = '\0'; + }, + Return(true) + )); + + m_mockRouteSync.onNextHopMsg(nlh, expected_length); + + auto it = m_mockRouteSync.m_nh_groups.find(test_id); + ASSERT_NE(it, m_mockRouteSync.m_nh_groups.end()) << "Failed to add new nexthop"; + + free(nlh); +} + +TEST_F(FpmSyncdResponseTest, TestIPv6NextHopAdd) +{ + uint32_t test_id = 20; + const char* test_gateway = "2001:db8::1"; + int32_t test_ifindex = 7; + + struct nlmsghdr* nlh = createNewNextHopMsgHdr(test_ifindex, test_gateway, test_id, AF_INET6); + int expected_length = (int)(nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg))); + + EXPECT_CALL(m_mockRouteSync, getIfName(test_ifindex, _, _)) + .WillOnce(DoAll( + [](int32_t, char* ifname, size_t size) { + strncpy(ifname, "Ethernet2", size); + ifname[size-1] = '\0'; + }, + Return(true) + )); + + m_mockRouteSync.onNextHopMsg(nlh, expected_length); + + Table nexthop_group_table(m_db.get(), APP_NEXTHOP_GROUP_TABLE_NAME); + + vector fieldValues; + string key = to_string(test_id); + nexthop_group_table.get(key, fieldValues); + + // onNextHopMsg only updates m_nh_groups unless the nhg is marked as installed + ASSERT_TRUE(fieldValues.empty()); + + // Update the nexthop group to mark it as installed and write to DB + m_mockRouteSync.installNextHopGroup(test_id); + nexthop_group_table.get(key, fieldValues); + + string nexthop, ifname; + for (const auto& fv : fieldValues) { + if (fvField(fv) == "nexthop") { + nexthop = fvValue(fv); + } else if (fvField(fv) == "ifname") { + ifname = fvValue(fv); + } + } + + EXPECT_EQ(nexthop, test_gateway); + EXPECT_EQ(ifname, "Ethernet2"); + + free(nlh); +} + + +TEST_F(FpmSyncdResponseTest, TestGetIfNameFailure) +{ + uint32_t test_id = 22; + int32_t test_ifindex = 9; + + struct nlmsghdr* nlh = createNewNextHopMsgHdr(test_ifindex, test_gateway, test_id); + int expected_length = (int)(nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg))); + + EXPECT_CALL(m_mockRouteSync, getIfName(test_ifindex, _, _)) + .WillOnce(Return(false)); + + m_mockRouteSync.onNextHopMsg(nlh, expected_length); + + auto it = m_mockRouteSync.m_nh_groups.find(test_id); + ASSERT_NE(it, m_mockRouteSync.m_nh_groups.end()); + EXPECT_EQ(it->second.intf, "unknown"); + + free(nlh); +} +TEST_F(FpmSyncdResponseTest, TestSkipSpecialInterfaces) +{ + uint32_t test_id = 11; + int32_t test_ifindex = 6; + + EXPECT_CALL(m_mockRouteSync, getIfName(test_ifindex, _, _)) + .WillOnce(DoAll( + [](int32_t ifidx, char* ifname, size_t size) { + strncpy(ifname, "eth0", size); + }, + Return(true) + )); + + struct nlmsghdr* nlh = createNewNextHopMsgHdr(test_ifindex, test_gateway, test_id); + int expected_length = (int)(nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg))); + + m_mockRouteSync.onNextHopMsg(nlh, expected_length); + + auto it = m_mockRouteSync.m_nh_groups.find(test_id); + EXPECT_EQ(it, m_mockRouteSync.m_nh_groups.end()) << "Should skip eth0 interface"; + + free(nlh); +} + +TEST_F(FpmSyncdResponseTest, TestNextHopGroupKeyString) +{ + EXPECT_EQ(m_mockRouteSync.getNextHopGroupKeyAsString(1), "1"); + EXPECT_EQ(m_mockRouteSync.getNextHopGroupKeyAsString(1234), "1234"); +} + +TEST_F(FpmSyncdResponseTest, TestGetNextHopGroupFields) +{ + // Test single next hop case + { + NextHopGroup nhg(1, test_gateway, "Ethernet0"); + m_mockRouteSync.m_nh_groups.insert({1, nhg}); + + string nexthops, ifnames, weights; + m_mockRouteSync.getNextHopGroupFields(nhg, nexthops, ifnames, weights); + + EXPECT_EQ(nexthops, test_gateway); + EXPECT_EQ(ifnames, "Ethernet0"); + EXPECT_TRUE(weights.empty()); + } + + // Test multiple next hops with weights + { + // Create the component next hops first + NextHopGroup nhg1(1, test_gateway, "Ethernet0"); + NextHopGroup nhg2(2, test_gateway_, "Ethernet1"); + m_mockRouteSync.m_nh_groups.insert({1, nhg1}); + m_mockRouteSync.m_nh_groups.insert({2, nhg2}); + + // Create the group with multiple next hops + vector> group_members; + group_members.push_back(make_pair(1, 1)); // id=1, weight=1 + group_members.push_back(make_pair(2, 2)); // id=2, weight=2 + + NextHopGroup nhg(3, group_members); + m_mockRouteSync.m_nh_groups.insert({3, nhg}); + + string nexthops, ifnames, weights; + m_mockRouteSync.getNextHopGroupFields(nhg, nexthops, ifnames, weights); + + EXPECT_EQ(nexthops, "192.168.1.1,192.168.1.2"); + EXPECT_EQ(ifnames, "Ethernet0,Ethernet1"); + EXPECT_EQ(weights, "1,2"); + } + + // Test IPv6 default case + { + NextHopGroup nhg(4, "", "Ethernet0"); + m_mockRouteSync.m_nh_groups.insert({4, nhg}); + + string nexthops, ifnames, weights; + m_mockRouteSync.getNextHopGroupFields(nhg, nexthops, ifnames, weights, AF_INET6); + + EXPECT_EQ(nexthops, "::"); + EXPECT_EQ(ifnames, "Ethernet0"); + EXPECT_TRUE(weights.empty()); + } + + // Both empty + { + NextHopGroup nhg(5, "", ""); + string nexthops, ifnames, weights; + m_mockRouteSync.getNextHopGroupFields(nhg, nexthops, ifnames, weights, AF_INET); + + EXPECT_EQ(nexthops, "0.0.0.0"); + EXPECT_TRUE(ifnames.empty()); + EXPECT_TRUE(weights.empty()); + } +} + +TEST_F(FpmSyncdResponseTest, TestUpdateNextHopGroupDb) +{ + Table nexthop_group_table(m_db.get(), APP_NEXTHOP_GROUP_TABLE_NAME); + + // Test single next hop group + { + NextHopGroup nhg(1, test_gateway, "Ethernet0"); + m_mockRouteSync.updateNextHopGroupDb(nhg); + + vector fieldValues; + nexthop_group_table.get("1", fieldValues); + + EXPECT_EQ(fieldValues.size(), 2); + EXPECT_EQ(fvField(fieldValues[0]), "nexthop"); + EXPECT_EQ(fvValue(fieldValues[0]), test_gateway); + EXPECT_EQ(fvField(fieldValues[1]), "ifname"); + EXPECT_EQ(fvValue(fieldValues[1]), "Ethernet0"); + } + + // Test group with multiple next hops + { + vector> group_members; + group_members.push_back(make_pair(1, 1)); + group_members.push_back(make_pair(2, 2)); + + NextHopGroup nhg1(1, test_gateway, "Ethernet0"); + NextHopGroup nhg2(2, test_gateway_, "Ethernet1"); + NextHopGroup group(3, group_members); + + m_mockRouteSync.m_nh_groups.insert({1, nhg1}); + m_mockRouteSync.m_nh_groups.insert({2, nhg2}); + m_mockRouteSync.m_nh_groups.insert({3, group}); + + m_mockRouteSync.installNextHopGroup(3); + + auto it = m_mockRouteSync.m_nh_groups.find(3); + ASSERT_NE(it, m_mockRouteSync.m_nh_groups.end()); + EXPECT_TRUE(it->second.installed); + vector fieldValues; + nexthop_group_table.get("3", fieldValues); + EXPECT_EQ(fieldValues.size(), 3); + EXPECT_EQ(fvField(fieldValues[0]), "nexthop"); + EXPECT_EQ(fvValue(fieldValues[0]), "192.168.1.1,192.168.1.2"); + EXPECT_EQ(fvField(fieldValues[1]), "ifname"); + EXPECT_EQ(fvValue(fieldValues[1]), "Ethernet0,Ethernet1"); + EXPECT_EQ(fvField(fieldValues[2]), "weight"); + EXPECT_EQ(fvValue(fieldValues[2]), "1,2"); + } + + // Empty nexthop (default route case) + { + NextHopGroup nhg(4, "", "Ethernet0"); + m_mockRouteSync.updateNextHopGroupDb(nhg); + + vector fieldValues; + nexthop_group_table.get("4", fieldValues); + + EXPECT_EQ(fieldValues.size(), 2); + EXPECT_EQ(fvField(fieldValues[0]), "nexthop"); + EXPECT_EQ(fvValue(fieldValues[0]), "0.0.0.0"); + EXPECT_EQ(fvField(fieldValues[1]), "ifname"); + EXPECT_EQ(fvValue(fieldValues[1]), "Ethernet0"); + } + + // Empty interface name + { + NextHopGroup nhg(5, test_gateway, ""); + m_mockRouteSync.updateNextHopGroupDb(nhg); + + vector fieldValues; + nexthop_group_table.get("5", fieldValues); + + EXPECT_EQ(fieldValues.size(), 2); + EXPECT_EQ(fvField(fieldValues[0]), "nexthop"); + EXPECT_EQ(fvValue(fieldValues[0]), test_gateway); + EXPECT_EQ(fvField(fieldValues[1]), "ifname"); + EXPECT_EQ(fvValue(fieldValues[1]), ""); + } +} + +TEST_F(FpmSyncdResponseTest, TestDeleteNextHopGroup) +{ + // Setup test groups + NextHopGroup nhg1(1, test_gateway, "Ethernet0"); + NextHopGroup nhg2(2, test_gateway_, "Ethernet1"); + nhg1.installed = true; + nhg2.installed = true; + + m_mockRouteSync.m_nh_groups.insert({1, nhg1}); + m_mockRouteSync.m_nh_groups.insert({2, nhg2}); + + // Test deletion + m_mockRouteSync.deleteNextHopGroup(1); + EXPECT_EQ(m_mockRouteSync.m_nh_groups.find(1), m_mockRouteSync.m_nh_groups.end()); + EXPECT_NE(m_mockRouteSync.m_nh_groups.find(2), m_mockRouteSync.m_nh_groups.end()); + + // Test deleting non-existent group + m_mockRouteSync.deleteNextHopGroup(999); + EXPECT_EQ(m_mockRouteSync.m_nh_groups.find(999), m_mockRouteSync.m_nh_groups.end()); +} + +struct nlmsghdr* createNewNextHopMsgHdr(const vector>& group_members, uint32_t id) { + struct nlmsghdr *nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); + memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); + + // Set header + nlh->nlmsg_type = RTM_NEWNEXTHOP; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE; + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)); + + // Set nhmsg + struct nhmsg *nhm = (struct nhmsg *)NLMSG_DATA(nlh); + nhm->nh_family = AF_INET; + + // Add NHA_ID + struct rtattr *rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = NHA_ID; + rta->rta_len = RTA_LENGTH(sizeof(uint32_t)); + *(uint32_t *)RTA_DATA(rta) = id; + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Add NHA_GROUP + rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = NHA_GROUP; + struct nexthop_grp* grp = (struct nexthop_grp*)malloc(group_members.size() * sizeof(struct nexthop_grp)); + + for (size_t i = 0; i < group_members.size(); i++) { + grp[i].id = group_members[i].first; + grp[i].weight = group_members[i].second - 1; // kernel stores weight-1 + } + + size_t payload_size = group_members.size() * sizeof(struct nexthop_grp); + if (payload_size > USHRT_MAX - RTA_LENGTH(0)) { + free(nlh); + return nullptr; + } + + rta->rta_len = static_cast(RTA_LENGTH(group_members.size() * sizeof(struct nexthop_grp))); + memcpy(RTA_DATA(rta), grp, group_members.size() * sizeof(struct nexthop_grp)); + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + free(grp); + return nlh; +} + +TEST_F(FpmSyncdResponseTest, TestNextHopGroupAdd) +{ + // 1. create nexthops + uint32_t nh1_id = 1; + uint32_t nh2_id = 2; + uint32_t nh3_id = 3; + + struct nlmsghdr* nlh1 = createNewNextHopMsgHdr(1, test_gateway, nh1_id); + struct nlmsghdr* nlh2 = createNewNextHopMsgHdr(2, test_gateway_, nh2_id); + struct nlmsghdr* nlh3 = createNewNextHopMsgHdr(3, test_gateway__, nh3_id); + + EXPECT_CALL(m_mockRouteSync, getIfName(1, _, _)) + .WillOnce(DoAll( + [](int32_t, char* ifname, size_t size) { + strncpy(ifname, "Ethernet1", size); + ifname[size-1] = '\0'; + }, + Return(true) + )); + + EXPECT_CALL(m_mockRouteSync, getIfName(2, _, _)) + .WillOnce(DoAll( + [](int32_t, char* ifname, size_t size) { + strncpy(ifname, "Ethernet2", size); + ifname[size-1] = '\0'; + }, + Return(true) + )); + + EXPECT_CALL(m_mockRouteSync, getIfName(3, _, _)) + .WillOnce(DoAll( + [](int32_t, char* ifname, size_t size) { + strncpy(ifname, "Ethernet3", size); + ifname[size-1] = '\0'; + }, + Return(true) + )); + + m_mockRouteSync.onNextHopMsg(nlh1, (int)(nlh1->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg)))); + m_mockRouteSync.onNextHopMsg(nlh2, (int)(nlh2->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg)))); + m_mockRouteSync.onNextHopMsg(nlh3, (int)(nlh3->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg)))); + + // 2. create a nexthop group with these nexthops + uint32_t group_id = 10; + vector> group_members = { + {nh1_id, 1}, // id=1, weight=1 + {nh2_id, 2}, // id=2, weight=2 + {nh3_id, 3} // id=3, weight=3 + }; + + struct nlmsghdr* group_nlh = createNewNextHopMsgHdr(group_members, group_id); + ASSERT_NE(group_nlh, nullptr) << "Failed to create group nexthop message"; + m_mockRouteSync.onNextHopMsg(group_nlh, (int)(group_nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg)))); + + // Verify the group was added correctly + auto it = m_mockRouteSync.m_nh_groups.find(group_id); + ASSERT_NE(it, m_mockRouteSync.m_nh_groups.end()) << "Failed to add nexthop group"; + + // Verify group members + const auto& group = it->second.group; + ASSERT_EQ(group.size(), 3) << "Wrong number of group members"; + + // Check each member's ID and weight + EXPECT_EQ(group[0].first, nh1_id); + EXPECT_EQ(group[0].second, 1); + EXPECT_EQ(group[1].first, nh2_id); + EXPECT_EQ(group[1].second, 2); + EXPECT_EQ(group[2].first, nh3_id); + EXPECT_EQ(group[2].second, 3); + + // Mark the group as installed and verify DB update + m_mockRouteSync.installNextHopGroup(group_id); + + Table nexthop_group_table(m_db.get(), APP_NEXTHOP_GROUP_TABLE_NAME); + vector fieldValues; + string key = to_string(group_id); + nexthop_group_table.get(key, fieldValues); + + ASSERT_EQ(fieldValues.size(), 3) << "Wrong number of fields in DB"; + + // Verify the DB fields + string nexthops, ifnames, weights; + for (const auto& fv : fieldValues) { + if (fvField(fv) == "nexthop") { + nexthops = fvValue(fv); + } else if (fvField(fv) == "ifname") { + ifnames = fvValue(fv); + } else if (fvField(fv) == "weight") { + weights = fvValue(fv); + } + } + + EXPECT_EQ(nexthops, "192.168.1.1,192.168.1.2,192.168.1.3"); + EXPECT_EQ(ifnames, "Ethernet1,Ethernet2,Ethernet3"); + EXPECT_EQ(weights, "1,2,3"); + + // Cleanup + free(nlh1); + free(nlh2); + free(nlh3); + free(group_nlh); +} + +TEST_F(FpmSyncdResponseTest, TestRouteMsgWithNHG) +{ + Table route_table(m_db.get(), APP_ROUTE_TABLE_NAME); + auto createRoute = [](const char* prefix, uint8_t prefixlen) -> rtnl_route* { + rtnl_route* route = rtnl_route_alloc(); + nl_addr* dst_addr; + nl_addr_parse(prefix, AF_INET, &dst_addr); + rtnl_route_set_dst(route, dst_addr); + rtnl_route_set_type(route, RTN_UNICAST); + rtnl_route_set_protocol(route, RTPROT_STATIC); + rtnl_route_set_family(route, AF_INET); + rtnl_route_set_scope(route, RT_SCOPE_UNIVERSE); + rtnl_route_set_table(route, RT_TABLE_MAIN); + nl_addr_put(dst_addr); + return route; + }; + + uint32_t test_nh_id = 1; + uint32_t test_nhg_id = 2; + uint32_t test_nh_id_ = 3; + uint32_t test_nh_id__ = 4; + + // create a route + const char* test_destipprefix = "10.1.1.0"; + rtnl_route* test_route = createRoute(test_destipprefix, 24); + + // Test 1: use a non-existent nh_id + { + rtnl_route_set_nh_id(test_route, test_nh_id); + + m_mockRouteSync.onRouteMsg(RTM_NEWROUTE, (nl_object*)test_route, nullptr); + + vector keys; + route_table.getKeys(keys); + + // verify the route is discarded + EXPECT_TRUE(std::find(keys.begin(), keys.end(), test_destipprefix) == keys.end()); + } + + // Test 2: using a nexthop + { + // create the nexthop + struct nlmsghdr* nlh = createNewNextHopMsgHdr(1, test_gateway, test_nh_id); + + EXPECT_CALL(m_mockRouteSync, getIfName(1, _, _)) + .WillOnce(DoAll( + [](int32_t, char* ifname, size_t size) { + strncpy(ifname, "Ethernet1", size); + ifname[size-1] = '\0'; + }, + Return(true) + )); + + m_mockRouteSync.onNextHopMsg(nlh, (int)(nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg)))); + + free(nlh); + + rtnl_route_set_nh_id(test_route, test_nh_id); + + m_mockRouteSync.onRouteMsg(RTM_NEWROUTE, (nl_object*)test_route, nullptr); + + vector fvs; + EXPECT_TRUE(route_table.get(test_destipprefix, fvs)); + EXPECT_EQ(fvs.size(), 3); + for (const auto& fv : fvs) { + if (fvField(fv) == "nexthop") { + EXPECT_EQ(fvValue(fv), test_gateway); + } else if (fvField(fv) == "ifname") { + EXPECT_EQ(fvValue(fv), "Ethernet1"); + } else if (fvField(fv) == "protocol") { + EXPECT_EQ(fvValue(fv), "static"); + } + } + } + + // Test 3: using an nhg + { + struct nlmsghdr* nlh1 = createNewNextHopMsgHdr(2, test_gateway_, test_nh_id_); + struct nlmsghdr* nlh2 = createNewNextHopMsgHdr(3, test_gateway__, test_nh_id__); + + EXPECT_CALL(m_mockRouteSync, getIfName(2, _, _)) + .WillOnce(DoAll( + [](int32_t, char* ifname, size_t size) { + strncpy(ifname, "Ethernet2", size); + ifname[size-1] = '\0'; + }, + Return(true) + )); + + EXPECT_CALL(m_mockRouteSync, getIfName(3, _, _)) + .WillOnce(DoAll( + [](int32_t, char* ifname, size_t size) { + strncpy(ifname, "Ethernet3", size); + ifname[size-1] = '\0'; + }, + Return(true) + )); + + m_mockRouteSync.onNextHopMsg(nlh1, (int)(nlh1->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg)))); + m_mockRouteSync.onNextHopMsg(nlh2, (int)(nlh2->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg)))); + + vector> group_members = { + {test_nh_id_, 1}, + {test_nh_id__, 2} + }; + + struct nlmsghdr* group_nlh = createNewNextHopMsgHdr(group_members, test_nhg_id); + m_mockRouteSync.onNextHopMsg(group_nlh, (int)(group_nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct nhmsg)))); + + // create the route object referring to this next hop group + rtnl_route_set_nh_id(test_route, test_nhg_id); + m_mockRouteSync.onRouteMsg(RTM_NEWROUTE, (nl_object*)test_route, nullptr); + + vector fvs; + EXPECT_TRUE(route_table.get(test_destipprefix, fvs)); + + for (const auto& fv : fvs) { + if (fvField(fv) == "nexthop_group") { + EXPECT_EQ(fvValue(fv), "2"); + } else if (fvField(fv) == "protocol") { + EXPECT_EQ(fvValue(fv), "static"); + } + } + + vector group_fvs; + Table nexthop_group_table(m_db.get(), APP_NEXTHOP_GROUP_TABLE_NAME); + EXPECT_TRUE(nexthop_group_table.get("2", group_fvs)); + + // clean up + free(nlh1); + free(nlh2); + free(group_nlh); + } + rtnl_route_put(test_route); }