diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c index 871263c61f..c441f58410 100644 --- a/src/core/platform/tests/test-route.c +++ b/src/core/platform/tests/test-route.c @@ -333,30 +333,33 @@ test_ip4_route(void) /* Test route listing */ routes = nmtstp_ip4_route_get_all(NM_PLATFORM_GET, ifindex); memset(rts, 0, sizeof(rts)); - rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER); - rts[0].network = gateway; - rts[0].plen = 32; - rts[0].ifindex = ifindex; - rts[0].gateway = INADDR_ANY; - rts[0].metric = metric; - rts[0].mss = mss; - rts[0].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_LINK); - rts[1].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER); - rts[1].network = network; - rts[1].plen = plen; - rts[1].ifindex = ifindex; - rts[1].gateway = gateway; - rts[1].metric = metric; - rts[1].mss = mss; - rts[1].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE); - rts[2].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER); - rts[2].network = 0; - rts[2].plen = 0; - rts[2].ifindex = ifindex; - rts[2].gateway = gateway; - rts[2].metric = metric; - rts[2].mss = mss; - rts[2].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE); + rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER); + rts[0].network = gateway; + rts[0].plen = 32; + rts[0].ifindex = ifindex; + rts[0].gateway = INADDR_ANY; + rts[0].metric = metric; + rts[0].mss = mss; + rts[0].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_LINK); + rts[0].n_nexthops = 1; + rts[1].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER); + rts[1].network = network; + rts[1].plen = plen; + rts[1].ifindex = ifindex; + rts[1].gateway = gateway; + rts[1].metric = metric; + rts[1].mss = mss; + rts[1].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE); + rts[1].n_nexthops = 1; + rts[2].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER); + rts[2].network = 0; + rts[2].plen = 0; + rts[2].ifindex = ifindex; + rts[2].gateway = gateway; + rts[2].metric = metric; + rts[2].mss = mss; + rts[2].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE); + rts[2].n_nexthops = 1; g_assert_cmpint(routes->len, ==, 3); nmtst_platform_ip4_routes_equal_aptr((const NMPObject *const *) routes->pdata, rts, @@ -635,21 +638,22 @@ test_ip4_route_options(gconstpointer test_data) switch (TEST_IDX) { case 1: rts_add[rts_n++] = ((NMPlatformIP4Route){ - .ifindex = IFINDEX, - .rt_source = NM_IP_CONFIG_SOURCE_USER, - .network = nmtst_inet4_from_string("172.16.1.0"), - .plen = 24, - .metric = 20, - .tos = 0x28, - .window = 10000, - .cwnd = 16, - .initcwnd = 30, - .initrwnd = 50, - .mtu = 1350, - .lock_cwnd = TRUE, - .mss = 1300, - .quickack = TRUE, - .rto_min = 1000, + .ifindex = IFINDEX, + .rt_source = NM_IP_CONFIG_SOURCE_USER, + .network = nmtst_inet4_from_string("172.16.1.0"), + .plen = 24, + .metric = 20, + .tos = 0x28, + .window = 10000, + .cwnd = 16, + .initcwnd = 30, + .initrwnd = 50, + .mtu = 1350, + .lock_cwnd = TRUE, + .mss = 1300, + .quickack = TRUE, + .rto_min = 1000, + .n_nexthops = 1, }); break; case 2: @@ -663,12 +667,13 @@ test_ip4_route_options(gconstpointer test_data) .n_ifa_flags = 0, }); rts_add[rts_n++] = ((NMPlatformIP4Route){ - .ifindex = IFINDEX, - .rt_source = NM_IP_CONFIG_SOURCE_USER, - .network = nmtst_inet4_from_string("172.17.1.0"), - .gateway = nmtst_inet4_from_string("172.16.1.1"), - .plen = 24, - .metric = 20, + .ifindex = IFINDEX, + .rt_source = NM_IP_CONFIG_SOURCE_USER, + .network = nmtst_inet4_from_string("172.17.1.0"), + .gateway = nmtst_inet4_from_string("172.16.1.1"), + .plen = 24, + .metric = 20, + .n_nexthops = 1, }); rts_add[rts_n++] = ((NMPlatformIP4Route){ .ifindex = IFINDEX, @@ -678,6 +683,7 @@ test_ip4_route_options(gconstpointer test_data) .r_rtm_flags = RTNH_F_ONLINK, .plen = 24, .metric = 20, + .n_nexthops = 1, }); break; default: diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c index cc57ce4045..4ea67c1791 100644 --- a/src/libnm-platform/nm-linux-platform.c +++ b/src/libnm-platform/nm-linux-platform.c @@ -3640,21 +3640,27 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter struct { gboolean found; gboolean has_more; + guint8 weight; int ifindex; NMIPAddr gateway; } nh = { .found = FALSE, .has_more = FALSE, }; - guint32 mss; - guint32 window = 0; - guint32 cwnd = 0; - guint32 initcwnd = 0; - guint32 initrwnd = 0; - guint32 mtu = 0; - guint32 rto_min = 0; - guint32 lock = 0; - gboolean quickack = FALSE; + guint v4_n_nexthops = 0; + NMPlatformIP4RtNextHop v4_nh_extra_nexthops_stack[10]; + gs_free NMPlatformIP4RtNextHop *v4_nh_extra_nexthops_heap = NULL; + NMPlatformIP4RtNextHop *v4_nh_extra_nexthops = v4_nh_extra_nexthops_stack; + guint v4_nh_extra_alloc = G_N_ELEMENTS(v4_nh_extra_nexthops_stack); + guint32 mss; + guint32 window = 0; + guint32 cwnd = 0; + guint32 initcwnd = 0; + guint32 initrwnd = 0; + guint32 mtu = 0; + guint32 rto_min = 0; + guint32 lock = 0; + gboolean quickack = FALSE; nm_assert((parse_nlmsg_iter->iter_more && parse_nlmsg_iter->ip6_route.next_multihop > 0) || (!parse_nlmsg_iter->iter_more && parse_nlmsg_iter->ip6_route.next_multihop == 0)); @@ -3712,9 +3718,57 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter idx = 0; while (TRUE) { - if (idx == multihop_idx) { + if (nh.found && IS_IPv4) { + NMPlatformIP4RtNextHop *new_nexthop; + + /* we parsed the first IPv4 nexthop in "nh", let's parse the following ones. + * + * At this point, v4_n_nexthops still counts how many hops we already added, + * now we are about to add the (v4_n_nexthops+1) hop. + * + * Note that the first hop (of then v4_n_nexthops) is tracked in "nh". + * v4_nh_extra_nexthops tracks the additional hops. + * + * v4_nh_extra_alloc is how many space is allocated for + * v4_nh_extra_nexthops (note that in the end we will only add (v4_n_nexthops-1) + * hops in this list). */ + nm_assert(v4_n_nexthops > 0u); + if (v4_n_nexthops - 1u >= v4_nh_extra_alloc) { + v4_nh_extra_alloc = NM_MAX(4, v4_nh_extra_alloc * 2u); + if (!v4_nh_extra_nexthops_heap) { + v4_nh_extra_nexthops_heap = + g_new(NMPlatformIP4RtNextHop, v4_nh_extra_alloc); + memcpy(v4_nh_extra_nexthops_heap, + v4_nh_extra_nexthops_stack, + G_N_ELEMENTS(v4_nh_extra_nexthops_stack)); + } else { + v4_nh_extra_nexthops_heap = g_renew(NMPlatformIP4RtNextHop, + v4_nh_extra_nexthops_heap, + v4_nh_extra_alloc); + } + v4_nh_extra_nexthops = v4_nh_extra_nexthops_heap; + } + nm_assert(v4_n_nexthops - 1u < v4_nh_extra_alloc); + new_nexthop = &v4_nh_extra_nexthops[v4_n_nexthops - 1u]; + new_nexthop->ifindex = rtnh->rtnh_ifindex; + new_nexthop->weight = NM_MAX(rtnh->rtnh_hops, 1u); + if (rtnh->rtnh_len > sizeof(*rtnh)) { + struct nlattr *ntb[RTA_MAX + 1]; + + if (nla_parse_arr(ntb, + (struct nlattr *) RTNH_DATA(rtnh), + rtnh->rtnh_len - sizeof(*rtnh), + NULL) + < 0) + return NULL; + + if (_check_addr_or_return_null(ntb, RTA_GATEWAY, addr_len)) + memcpy(&new_nexthop->gateway, nla_data(ntb[RTA_GATEWAY]), addr_len); + } + } else if (IS_IPv4 || idx == multihop_idx) { nh.found = TRUE; nh.ifindex = rtnh->rtnh_ifindex; + nh.weight = NM_MAX(rtnh->rtnh_hops, 1u); if (rtnh->rtnh_len > sizeof(*rtnh)) { struct nlattr *ntb[RTA_MAX + 1]; @@ -3731,15 +3785,6 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter } else if (nh.found) { /* we just parsed a nexthop, but there is yet another hop afterwards. */ nm_assert(idx == multihop_idx + 1); - if (IS_IPv4) { - /* for IPv4, multihop routes are currently not supported. - * - * If we ever support them, then the next-hop list is part of the NMPlatformIPRoute, - * that is, for IPv4 we truly have multihop routes. Unlike for IPv6. - * - * For now, just error out. */ - return NULL; - } /* For IPv6 multihop routes, we need to remember to iterate again. * For each next-hop, we will create a distinct single-hop NMPlatformIP6Route. */ @@ -3747,6 +3792,9 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter break; } + if (IS_IPv4) + v4_n_nexthops++; + if (tlen < RTNH_ALIGN(rtnh->rtnh_len) + sizeof(*rtnh)) break; @@ -3780,6 +3828,9 @@ rta_multipath_done: nh.ifindex = ifindex; nh.gateway = gateway; nh.found = TRUE; + nm_assert(v4_n_nexthops == 0); + if (IS_IPv4) + v4_n_nexthops = 1; } else { /* Kernel supports new style nexthop configuration, * verify that it is a duplicate and ignore old-style nexthop. */ @@ -3870,6 +3921,23 @@ rta_multipath_done: obj->ip_route.ifindex = nh.ifindex; + if (IS_IPv4) { + nm_assert((!!nh.found) == (v4_n_nexthops > 0u)); + obj->ip4_route.n_nexthops = v4_n_nexthops; + if (v4_n_nexthops > 1) { + /* We only set the weight for multihop routes. I think that corresponds to what kernel + * does. The weight is mostly undefined for single-hop. */ + obj->ip4_route.weight = NM_MAX(nh.weight, 1u); + + obj->_ip4_route.extra_nexthops = + (v4_nh_extra_alloc == v4_n_nexthops - 1u + && v4_nh_extra_nexthops == v4_nh_extra_nexthops_heap) + ? g_steal_pointer(&v4_nh_extra_nexthops_heap) + : nm_memdup(v4_nh_extra_nexthops, + sizeof(v4_nh_extra_nexthops[0]) * (v4_n_nexthops - 1u)); + } + } + if (_check_addr_or_return_null(tb, RTA_DST, addr_len)) memcpy(obj->ip_route.network_ptr, nla_data(tb[RTA_DST]), addr_len); @@ -5180,6 +5248,39 @@ _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *ob NLA_PUT(msg, RTA_PREFSRC, addr_len, &obj->ip6_route.pref_src); } + if (IS_IPv4 && obj->ip4_route.n_nexthops > 1u) { + struct nlattr *multipath; + guint i; + + if (!(multipath = nla_nest_start(msg, RTA_MULTIPATH))) + goto nla_put_failure; + + for (i = 0u; i < obj->ip4_route.n_nexthops; i++) { + struct rtnexthop *rtnh; + + rtnh = nlmsg_reserve(msg, sizeof(*rtnh), NLMSG_ALIGNTO); + if (!rtnh) + goto nla_put_failure; + + if (i == 0u) { + rtnh->rtnh_hops = NM_MAX(obj->ip4_route.weight, 1u); + rtnh->rtnh_ifindex = obj->ip4_route.ifindex; + NLA_PUT_U32(msg, RTA_GATEWAY, obj->ip4_route.gateway); + } else { + const NMPlatformIP4RtNextHop *n = &obj->_ip4_route.extra_nexthops[i - 1u]; + + rtnh->rtnh_hops = NM_MAX(n->weight, 1u); + rtnh->rtnh_ifindex = n->ifindex; + NLA_PUT_U32(msg, RTA_GATEWAY, n->gateway); + } + + rtnh->rtnh_flags = 0; + rtnh->rtnh_len = (char *) nlmsg_tail(nlmsg_hdr(msg)) - (char *) rtnh; + } + + nla_nest_end(msg, multipath); + } + if (obj->ip_route.mss || obj->ip_route.window || obj->ip_route.cwnd || obj->ip_route.initcwnd || obj->ip_route.initrwnd || obj->ip_route.mtu || obj->ip_route.quickack || obj->ip_route.rto_min || lock) { diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c index f8f715bc23..188db58ed0 100644 --- a/src/libnm-platform/nm-platform.c +++ b/src/libnm-platform/nm-platform.c @@ -5206,7 +5206,30 @@ nm_platform_ip_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPObject *r int nm_platform_ip4_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformIP4Route *route) { - return _ip_route_add(self, flags, AF_INET, route); + gs_free NMPlatformIP4RtNextHop *extra_nexthops_free = NULL; + NMPObject obj; + + nm_assert(route); + nm_assert(route->n_nexthops <= 1u || extra_nexthops); + + nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP4_ROUTE, (const NMPlatformObject *) route); + + if (route->n_nexthops > 1u) { + nm_assert(extra_nexthops); + /* we need to ensure that @extra_nexthops stays alive until the function returns. + * Copy the buffer. + * + * This is probably not necessary, because likely the caller will somehow ensure that + * the extra_nexthops stay alive. Still do it, because it is a very unusual case and + * likely cheap. */ + obj._ip4_route.extra_nexthops = + nm_memdup_maybe_a(500u, + extra_nexthops, + sizeof(extra_nexthops[0]) * (route->n_nexthops - 1u), + &extra_nexthops_free); + } + + return _ip_route_add(self, flags, &obj); } int @@ -6606,6 +6629,10 @@ _rtm_flags_to_string_full(char *buf, gsize buf_size, unsigned rtm_flags) /** * nm_platform_ip4_route_to_string: * @route: pointer to NMPlatformIP4Route route structure + * @extra_nexthops: (allow-none): the route might be a ECMP multihop route + * (with n_nexthops > 1). In that case, provide the list of extra hops + * to print too. It is allowed for a multihop route to omit the extra hops + * by passing NULL. * @buf: (allow-none): an optional buffer. If %NULL, a static buffer is used. * @len: the size of the @buf. If @buf is %NULL, this argument is ignored. * @@ -6616,30 +6643,40 @@ _rtm_flags_to_string_full(char *buf, gsize buf_size, unsigned rtm_flags) * Returns: a string representation of the route. */ const char * -nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsize len) +nm_platform_ip4_route_to_string_full(const NMPlatformIP4Route *route, + const NMPlatformIP4RtNextHop *extra_nexthops, + char *buf, + gsize len) { - char s_network[INET_ADDRSTRLEN]; - char s_gateway[INET_ADDRSTRLEN]; - char s_pref_src[INET_ADDRSTRLEN]; - char str_dev[30]; - char str_mss[32]; - char str_table[30]; - char str_scope[30]; - char s_source[50]; - char str_tos[32]; - char str_window[32]; - char str_cwnd[32]; - char str_initcwnd[32]; - char str_initrwnd[32]; - char str_rto_min[32]; - char str_mtu[32]; - char str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN]; - char str_type[30]; - char str_metric[30]; + char *buf0; + char s_network[INET_ADDRSTRLEN]; + char s_gateway[INET_ADDRSTRLEN]; + char s_pref_src[INET_ADDRSTRLEN]; + char str_dev[30]; + char str_mss[32]; + char str_table[30]; + char str_scope[30]; + char s_source[50]; + char str_tos[32]; + char str_window[32]; + char str_cwnd[32]; + char str_initcwnd[32]; + char str_initrwnd[32]; + char str_rto_min[32]; + char str_mtu[32]; + char str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN]; + char str_type[30]; + char str_metric[30]; + char weight_str[20]; + guint n_nexthops; if (!nm_utils_to_string_buffer_init_null(route, &buf, &len)) return buf; + buf0 = buf; + + n_nexthops = nm_platform_ip4_route_get_n_nexthops(route); + inet_ntop(AF_INET, &route->network, s_network, sizeof(s_network)); if (route->gateway == 0) @@ -6647,14 +6684,15 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz else inet_ntop(AF_INET, &route->gateway, s_gateway, sizeof(s_gateway)); - g_snprintf( - buf, - len, + nm_strbuf_append( + &buf, + &len, "type %s " /* type */ "%s" /* table */ "%s/%d" "%s%s" /* gateway */ - "%s" + "%s%s" /* weight */ + "%s" /* dev/ifindex */ " metric %s" "%s" /* mss */ " rt-src %s" /* protocol */ @@ -6682,9 +6720,13 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz : ""), s_network, route->plen, - s_gateway[0] ? " via " : "", - s_gateway, - _to_string_dev(str_dev, route->ifindex), + n_nexthops <= 1 && s_gateway[0] ? " via " : "", + n_nexthops <= 1 ? s_gateway : "", + NM_PRINT_FMT_QUOTED2(n_nexthops <= 1 && route->weight != 0, + " weight ", + nm_sprintf_buf(weight_str, "%u", route->weight), + ""), + n_nexthops <= 1 ? _to_string_dev(str_dev, route->ifindex) : "", route->metric_any ? (route->metric ? nm_sprintf_buf(str_metric, "??+%u", route->metric) : "??") : nm_sprintf_buf(str_metric, "%u", route->metric), @@ -6734,7 +6776,56 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz route->mtu) : "", route->r_force_commit ? " force-commit" : ""); - return buf; + + if ((n_nexthops == 1 && route->ifindex > 0) || n_nexthops == 0) { + /* A plain single hop route. Nothing extra to remark. */ + } else { + nm_strbuf_append(&buf, &len, " n_nexthops %u", n_nexthops); + if (n_nexthops > 1) { + nm_strbuf_append(&buf, + &len, + " nexthop" + "%s%s" /* gateway */ + "%s%s" /* weight */ + "%s" /* dev/ifindex */ + "", + s_gateway[0] ? " via " : "", + s_gateway, + NM_PRINT_FMT_QUOTED2(route->weight != 1, + " weight ", + nm_sprintf_buf(weight_str, "%u", route->weight), + ""), + _to_string_dev(str_dev, route->ifindex)); + if (!extra_nexthops) + nm_strbuf_append_str(&buf, &len, " nexthops [...]"); + else { + guint i; + + for (i = 1; i < n_nexthops; i++) { + const NMPlatformIP4RtNextHop *nexthop = &extra_nexthops[i - 1]; + + nm_strbuf_append( + &buf, + &len, + " nexthop" + "%s" /* ifindex */ + "%s%s" /* gateway */ + "%s%s" /* weight */ + "", + NM_PRINT_FMT_QUOTED2(nexthop->gateway != 0 || nexthop->ifindex <= 0, + " via ", + nm_inet4_ntop(nexthop->gateway, s_gateway), + ""), + _to_string_dev(str_dev, nexthop->ifindex), + NM_PRINT_FMT_QUOTED2(nexthop->weight != 1, + " weight ", + nm_sprintf_buf(weight_str, "%u", nexthop->weight), + "")); + } + } + } + } + return buf0; } /** @@ -8072,6 +8163,20 @@ nm_platform_lnk_wireguard_cmp(const NMPlatformLnkWireGuard *a, const NMPlatformL return 0; } +void +nm_platform_ip4_rt_nexthop_hash_update(const NMPlatformIP4RtNextHop *obj, + gboolean for_id, + NMHashState *h) +{ + guint8 w; + + nm_assert(obj); + + w = for_id ? NM_MAX(obj->weight, 1u) : obj->weight; + + nm_hash_update_vals(h, obj->ifindex, obj->gateway, w); +} + void nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj, NMPlatformIPRouteCmpType cmp_type, @@ -8101,7 +8206,9 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj, obj->ifindex, nmp_utils_ip_config_source_round_trip_rtprot(obj->rt_source), _ip_route_scope_inv_get_normalized(obj), + nm_platform_ip4_route_get_n_nexthops(obj), obj->gateway, + (guint8) NM_MAX(obj->weight, 1u), obj->mss, obj->pref_src, obj->window, @@ -8131,7 +8238,9 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj, nm_ip4_addr_clear_host_address(obj->network, obj->plen), obj->plen, obj->metric, + nm_platform_ip4_route_get_n_nexthops(obj), obj->gateway, + (guint8) NM_MAX(obj->weight, 1u), nmp_utils_ip_config_source_round_trip_rtprot(obj->rt_source), _ip_route_scope_inv_get_normalized(obj), obj->tos, @@ -8164,6 +8273,8 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj, obj->plen, obj->metric, obj->gateway, + obj->n_nexthops, + obj->weight, obj->rt_source, obj->scope_inv, obj->tos, @@ -8191,6 +8302,30 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj, } } +int +nm_platform_ip4_rt_nexthop_cmp(const NMPlatformIP4RtNextHop *a, + const NMPlatformIP4RtNextHop *b, + gboolean for_id) +{ + guint8 w_a; + guint8 w_b; + + /* Note that weight zero is not valid (in kernel). We thus treat + * weight zero usually the same as 1. + * + * Not here for cmp/hash_update functions. These functions check for the exact + * bit-pattern, and not the it means at other places. */ + NM_CMP_SELF(a, b); + NM_CMP_FIELD(a, b, ifindex); + NM_CMP_FIELD(a, b, gateway); + + w_a = for_id ? NM_MAX(a->weight, 1u) : a->weight; + w_b = for_id ? NM_MAX(b->weight, 1u) : b->weight; + NM_CMP_DIRECT(w_a, w_b); + + return 0; +} + int nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a, const NMPlatformIP4Route *b, @@ -8215,7 +8350,10 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a, nmp_utils_ip_config_source_round_trip_rtprot(b->rt_source)); NM_CMP_DIRECT(_ip_route_scope_inv_get_normalized(a), _ip_route_scope_inv_get_normalized(b)); + NM_CMP_DIRECT(nm_platform_ip4_route_get_n_nexthops(a), + nm_platform_ip4_route_get_n_nexthops(b)); NM_CMP_FIELD(a, b, gateway); + NM_CMP_DIRECT(NM_MAX(a->weight, 1u), NM_MAX(b->weight, 1u)); NM_CMP_FIELD(a, b, mss); NM_CMP_FIELD(a, b, pref_src); NM_CMP_FIELD(a, b, window); @@ -8251,7 +8389,16 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a, NM_CMP_FIELD(a, b, plen); NM_CMP_FIELD_UNSAFE(a, b, metric_any); NM_CMP_FIELD(a, b, metric); + if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) { + NM_CMP_DIRECT(nm_platform_ip4_route_get_n_nexthops(a), + nm_platform_ip4_route_get_n_nexthops(b)); + } else + NM_CMP_FIELD(a, b, n_nexthops); NM_CMP_FIELD(a, b, gateway); + if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) + NM_CMP_DIRECT(NM_MAX(a->weight, 1u), NM_MAX(b->weight, 1u)); + else + NM_CMP_FIELD(a, b, weight); if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) { NM_CMP_DIRECT(nmp_utils_ip_config_source_round_trip_rtprot(a->rt_source), nmp_utils_ip_config_source_round_trip_rtprot(b->rt_source)); @@ -8913,7 +9060,10 @@ log_ip4_route(NMPlatform *self, _LOG3D("signal: route 4 %7s: %s", nm_platform_signal_change_type_to_string(change_type), - nm_platform_ip4_route_to_string(route, sbuf, sizeof(sbuf))); + nmp_object_to_string(NMP_OBJECT_UP_CAST(route), + NMP_OBJECT_TO_STRING_PUBLIC, + sbuf, + sizeof(sbuf))); } static void @@ -8928,7 +9078,10 @@ log_ip6_route(NMPlatform *self, _LOG3D("signal: route 6 %7s: %s", nm_platform_signal_change_type_to_string(change_type), - nm_platform_ip6_route_to_string(route, sbuf, sizeof(sbuf))); + nmp_object_to_string(NMP_OBJECT_UP_CAST(route), + NMP_OBJECT_TO_STRING_PUBLIC, + sbuf, + sizeof(sbuf))); } static void diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h index 9b711254c9..6429e1d923 100644 --- a/src/libnm-platform/nm-platform.h +++ b/src/libnm-platform/nm-platform.h @@ -382,9 +382,27 @@ typedef struct { struct _NMPlatformIP4Route { __NMPlatformIPRoute_COMMON; + in_addr_t network; - /* RTA_GATEWAY. The gateway is part of the primary key for a route */ + /* If n_nexthops is zero, the the address has no next hops. That applies + * to certain route types like blackhole. + * If n_nexthops is 1, then the fields "ifindex", "gateway" and "weight" + * are the first next-hop. There are no further nexthops. + * If n_nexthops is greater than 1, the first next hop is in the fields + * "ifindex", "gateway", "weight", and the (n_nexthops-1) hops are in + * NMPObjectIP4Route.extra_nexthops field (outside the NMPlatformIP4Route + * struct). + * + * For convenience, if ifindex > 0 and n_nexthops == 0, we assume that n_nexthops + * is in fact 1. If ifindex is <= 0, n_nexthops must be zero. + * See nm_platform_ip4_route_get_n_nexthops(). */ + guint n_nexthops; + + /* RTA_GATEWAY. The gateway is part of the primary key for a route. + * If n_nexthops is zero, this value is undefined (should be zero). + * If n_nexthops is greater or equal to one, this is the gateway of + * the first hop. */ in_addr_t gateway; /* RTA_PREFSRC (called "src" by iproute2). @@ -412,6 +430,21 @@ struct _NMPlatformIP4Route { * For IPv6 routes, the scope is ignored and kernel always assumes global scope. * Hence, this field is only in NMPlatformIP4Route. */ guint8 scope_inv; + + /* This is the weight of for the first next-hop, in case of n_nexthops > 1. + * + * If n_nexthops is zero, this value is undefined (should be zero). + * If n_nexthops is 1, this also doesn't matter, but it's usually set to + * zero. + * If n_nexthops is greater or equal to one, this is the weight of + * the first hop. + * + * Note that upper layers use this flag to indicate whether this is a multihop route. + * Single-hop, non-ECMP routes will have a weight of zero. + * + * The valid range for weight is 1-255. For convenience, we treat 0 the same + * as 1 for multihop routes. */ + guint8 weight; }; struct _NMPlatformIP6Route { @@ -636,6 +669,14 @@ typedef struct { bool proto_ad : 1; } NMPlatformVFVlan; +typedef struct { + int ifindex; + in_addr_t gateway; + /* The valid range for weight is 1-255. For convenience, we treat 0 the same + * as 1 for multihop routes. */ + guint8 weight; +} NMPlatformIP4RtNextHop; + typedef struct { guint num_vlans; guint32 index; @@ -2129,6 +2170,23 @@ nm_platform_ip4_route_get_effective_metric(const NMPlatformIP4Route *r) : r->metric; } +static inline guint +nm_platform_ip4_route_get_n_nexthops(const NMPlatformIP4Route *r) +{ + /* The first hop of the "n_nexthops" is in NMPlatformIP4Route + * itself. Thus, if the caller only sets ifindex and leaves + * n_nexthops at zero, the number of next hops is still 1 + * (for convenience of the user who wants to initialize a + * single hop route). */ + if (r->n_nexthops >= 1) { + nm_assert(r->ifindex > 0); + return r->n_nexthops; + } + if (r->ifindex > 0) + return 1; + return 0; +} + static inline guint32 nm_platform_ip6_route_get_effective_metric(const NMPlatformIP6Route *r) { @@ -2216,7 +2274,18 @@ const char *nm_platform_lnk_vrf_to_string(const NMPlatformLnkVrf *lnk, char *buf const char *nm_platform_lnk_vxlan_to_string(const NMPlatformLnkVxlan *lnk, char *buf, gsize len); const char * nm_platform_lnk_wireguard_to_string(const NMPlatformLnkWireGuard *lnk, char *buf, gsize len); -const char *nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsize len); + +const char *nm_platform_ip4_route_to_string_full(const NMPlatformIP4Route *route, + const NMPlatformIP4RtNextHop *extra_nexthops, + char *buf, + gsize len); + +static inline const char * +nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsize len) +{ + return nm_platform_ip4_route_to_string_full(route, NULL, buf, len); +} + const char *nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsize len); const char * nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, char *buf, gsize len); @@ -2260,6 +2329,9 @@ GHashTable *nm_platform_ip4_address_addr_to_hash(NMPlatform *self, int ifindex); int nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a, const NMPlatformIP4Route *b, NMPlatformIPRouteCmpType cmp_type); +int nm_platform_ip4_rt_nexthop_cmp(const NMPlatformIP4RtNextHop *a, + const NMPlatformIP4RtNextHop *b, + gboolean for_id); int nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a, const NMPlatformIP6Route *b, NMPlatformIPRouteCmpType cmp_type); @@ -2298,6 +2370,9 @@ void nm_platform_link_hash_update(const NMPlatformLink *obj, NMHashState *h); void nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj, NMPlatformIPRouteCmpType cmp_type, NMHashState *h); +void nm_platform_ip4_rt_nexthop_hash_update(const NMPlatformIP4RtNextHop *obj, + gboolean for_id, + NMHashState *h); void nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj, NMPlatformIPRouteCmpType cmp_type, NMHashState *h); diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c index a6ad5f65b0..06457e3927 100644 --- a/src/libnm-platform/nmp-object.c +++ b/src/libnm-platform/nmp-object.c @@ -734,6 +734,12 @@ _vt_cmd_obj_dispose_link(NMPObject *obj) nmp_object_unref(obj->_link.netlink.lnk); } +static void +_vt_cmd_obj_dispose_ip4_route(NMPObject *obj) +{ + nm_clear_g_free((gpointer *) &obj->_ip4_route.extra_nexthops); +} + static void _vt_cmd_obj_dispose_lnk_vlan(NMPObject *obj) { @@ -848,14 +854,12 @@ nmp_object_stackinit_id(NMPObject *obj, const NMPObject *src) if (klass->cmd_plobj_id_copy) klass->cmd_plobj_id_copy(&obj->object, &src->object); else { - /* This object must not implement cmd_obj_copy(). - * If it would, it would mean that we require a deep copy - * of the data. As @obj is stack-allocated, it cannot track - * ownership. The caller must not use nmp_object_stackinit_id() - * with an object of such a type. */ - nm_assert(!klass->cmd_obj_copy); - - /* plain memcpy of the public part suffices. */ + /* plain memcpy. + * + * Note that for NMPObjectIP4Route this also copies extra_nexthops + * pointer, aliasing it without taking ownership. That is potentially + * dangerous, but when using a stack allocated instance, you must + * always take care of ownership. */ memcpy(&obj->object, &src->object, klass->sizeof_data); } return obj; @@ -992,6 +996,41 @@ _vt_cmd_obj_to_string_link(const NMPObject *obj, } } +static const char * +_vt_cmd_obj_to_string_ip4_route(const NMPObject *obj, + NMPObjectToStringMode to_string_mode, + char *buf, + gsize buf_size) +{ + const NMPClass *klass; + char buf2[NM_UTILS_TO_STRING_BUFFER_SIZE]; + + klass = NMP_OBJECT_GET_CLASS(obj); + + switch (to_string_mode) { + case NMP_OBJECT_TO_STRING_PUBLIC: + case NMP_OBJECT_TO_STRING_ID: + nm_platform_ip4_route_to_string_full(&obj->ip4_route, + obj->_ip4_route.extra_nexthops, + buf, + buf_size); + return buf; + case NMP_OBJECT_TO_STRING_ALL: + g_snprintf(buf, + buf_size, + "[%s," NM_HASH_OBFUSCATE_PTR_FMT ",%u,%calive,%cvisible; %s]", + klass->obj_type_name, + NM_HASH_OBFUSCATE_PTR(obj), + obj->parent._ref_count, + nmp_object_is_alive(obj) ? '+' : '-', + nmp_object_is_visible(obj) ? '+' : '-', + nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, buf2, sizeof(buf2))); + return buf; + default: + g_return_val_if_reached("ERROR"); + } +} + static const char * _vt_cmd_obj_to_string_lnk_vlan(const NMPObject *obj, NMPObjectToStringMode to_string_mode, @@ -1197,6 +1236,21 @@ _vt_cmd_obj_hash_update_link(const NMPObject *obj, gboolean for_id, NMHashState nmp_object_hash_update(obj->_link.netlink.lnk, h); } +static void +_vt_cmd_obj_hash_update_ip4_route(const NMPObject *obj, gboolean for_id, NMHashState *h) +{ + guint i; + + nm_assert(NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_IP4_ROUTE); + + nm_platform_ip4_route_hash_update(&obj->ip4_route, + for_id ? NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID + : NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL, + h); + for (i = 1u; i < obj->ip4_route.n_nexthops; i++) + nm_platform_ip4_rt_nexthop_hash_update(&obj->_ip4_route.extra_nexthops[i - 1u], for_id, h); +} + static void _vt_cmd_obj_hash_update_lnk_vlan(const NMPObject *obj, gboolean for_id, NMHashState *h) { @@ -1298,7 +1352,9 @@ nmp_object_cmp_full(const NMPObject *obj1, const NMPObject *obj2, NMPObjectCmpFl } else if (obj1->obj_with_ifindex.ifindex != obj2->obj_with_ifindex.ifindex) { nmp_object_stackinit(&obj_stackcopy, klass->obj_type, &obj2->obj_with_ifindex); obj_stackcopy.obj_with_ifindex.ifindex = obj1->obj_with_ifindex.ifindex; - obj2 = &obj_stackcopy; + if (klass->obj_type == NMP_OBJECT_TYPE_IP4_ROUTE) + obj_stackcopy._ip4_route.extra_nexthops = obj2->_ip4_route.extra_nexthops; + obj2 = &obj_stackcopy; } } @@ -1335,6 +1391,28 @@ _vt_cmd_obj_cmp_link(const NMPObject *obj1, const NMPObject *obj2, gboolean for_ return 0; } +static int +_vt_cmd_obj_cmp_ip4_route(const NMPObject *obj1, const NMPObject *obj2, gboolean for_id) +{ + int c; + guint i; + + c = nm_platform_ip4_route_cmp(&obj1->ip4_route, + &obj2->ip4_route, + for_id ? NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID + : NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL); + NM_CMP_RETURN_DIRECT(c); + + for (i = 1u; i < obj1->ip4_route.n_nexthops; i++) { + c = nm_platform_ip4_rt_nexthop_cmp(&obj1->_ip4_route.extra_nexthops[i - 1u], + &obj2->_ip4_route.extra_nexthops[i - 1u], + for_id); + NM_CMP_RETURN_DIRECT(c); + } + + return 0; +} + static int _vt_cmd_obj_cmp_lnk_vlan(const NMPObject *obj1, const NMPObject *obj2, gboolean for_id) { @@ -1438,6 +1516,28 @@ _vt_cmd_obj_copy_link(NMPObject *dst, const NMPObject *src) dst->_link = src->_link; } +static void +_vt_cmd_obj_copy_ip4_route(NMPObject *dst, const NMPObject *src) +{ + nm_assert(dst != src); + + if (src->ip4_route.n_nexthops <= 1) { + nm_clear_g_free((gpointer *) &dst->_ip4_route.extra_nexthops); + } else if (src->ip4_route.n_nexthops != dst->ip4_route.n_nexthops + || !nm_memeq_n(src->_ip4_route.extra_nexthops, + src->ip4_route.n_nexthops - 1u, + dst->_ip4_route.extra_nexthops, + dst->ip4_route.n_nexthops - 1u, + sizeof(NMPlatformIP4RtNextHop))) { + nm_clear_g_free((gpointer *) &dst->_ip4_route.extra_nexthops); + dst->_ip4_route.extra_nexthops = + nm_memdup(src->_ip4_route.extra_nexthops, + sizeof(NMPlatformIP4RtNextHop) * (src->ip4_route.n_nexthops - 1u)); + } + + dst->ip4_route = src->ip4_route; +} + static void _vt_cmd_obj_copy_lnk_vlan(NMPObject *dst, const NMPObject *src) { @@ -1577,14 +1677,6 @@ _vt_cmd_plobj_id_cmp(tfilter, NMPlatformTfilter, { NM_CMP_FIELD(obj1, obj2, handle); }); -static int -_vt_cmd_plobj_id_cmp_ip4_route(const NMPlatformObject *obj1, const NMPlatformObject *obj2) -{ - return nm_platform_ip4_route_cmp((const NMPlatformIP4Route *) obj1, - (const NMPlatformIP4Route *) obj2, - NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID); -} - static int _vt_cmd_plobj_id_cmp_ip6_route(const NMPlatformObject *obj1, const NMPlatformObject *obj2) { @@ -1673,10 +1765,6 @@ _vt_cmd_plobj_id_hash_update(ip6_address, NMPlatformIP6Address, { obj->address); }); -_vt_cmd_plobj_id_hash_update(ip4_route, NMPlatformIP4Route, { - nm_platform_ip4_route_hash_update(obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID, h); -}); - _vt_cmd_plobj_id_hash_update(ip6_route, NMPlatformIP6Route, { nm_platform_ip6_route_hash_update(obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID, h); }); @@ -1699,14 +1787,6 @@ _vt_cmd_plobj_id_hash_update(mptcp_addr, NMPlatformMptcpAddr, { nm_hash_update(h, &obj->addr, nm_utils_addr_family_to_size_untrusted(obj->addr_family)); }); -static void -_vt_cmd_plobj_hash_update_ip4_route(const NMPlatformObject *obj, NMHashState *h) -{ - return nm_platform_ip4_route_hash_update((const NMPlatformIP4Route *) obj, - NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL, - h); -} - static void _vt_cmd_plobj_hash_update_ip6_route(const NMPlatformObject *obj, NMHashState *h) { @@ -3218,23 +3298,22 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = { }, [NMP_OBJECT_TYPE_IP4_ROUTE - 1] = { - .parent = DEDUP_MULTI_OBJ_CLASS_INIT(), - .obj_type = NMP_OBJECT_TYPE_IP4_ROUTE, - .sizeof_data = sizeof(NMPObjectIP4Route), - .sizeof_public = sizeof(NMPlatformIP4Route), - .obj_type_name = "ip4-route", - .addr_family = AF_INET, - .rtm_gettype = RTM_GETROUTE, - .signal_type_id = NM_PLATFORM_SIGNAL_ID_IP4_ROUTE, - .signal_type = NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, - .supported_cache_ids = _supported_cache_ids_ipx_route, - .cmd_obj_is_alive = _vt_cmd_obj_is_alive_ipx_route, - .cmd_plobj_id_cmp = _vt_cmd_plobj_id_cmp_ip4_route, - .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_ip4_route, - .cmd_plobj_to_string_id = (CmdPlobjToStringIdFunc) nm_platform_ip4_route_to_string, - .cmd_plobj_to_string = (CmdPlobjToStringFunc) nm_platform_ip4_route_to_string, - .cmd_plobj_hash_update = _vt_cmd_plobj_hash_update_ip4_route, - .cmd_plobj_cmp = (CmdPlobjCmpFunc) nm_platform_ip4_route_cmp_full, + .parent = DEDUP_MULTI_OBJ_CLASS_INIT(), + .obj_type = NMP_OBJECT_TYPE_IP4_ROUTE, + .sizeof_data = sizeof(NMPObjectIP4Route), + .sizeof_public = sizeof(NMPlatformIP4Route), + .obj_type_name = "ip4-route", + .addr_family = AF_INET, + .rtm_gettype = RTM_GETROUTE, + .signal_type_id = NM_PLATFORM_SIGNAL_ID_IP4_ROUTE, + .signal_type = NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, + .supported_cache_ids = _supported_cache_ids_ipx_route, + .cmd_obj_is_alive = _vt_cmd_obj_is_alive_ipx_route, + .cmd_obj_hash_update = _vt_cmd_obj_hash_update_ip4_route, + .cmd_obj_cmp = _vt_cmd_obj_cmp_ip4_route, + .cmd_obj_copy = _vt_cmd_obj_copy_ip4_route, + .cmd_obj_dispose = _vt_cmd_obj_dispose_ip4_route, + .cmd_obj_to_string = _vt_cmd_obj_to_string_ip4_route, }, [NMP_OBJECT_TYPE_IP6_ROUTE - 1] = { diff --git a/src/libnm-platform/nmp-object.h b/src/libnm-platform/nmp-object.h index 090436d217..9fa260b57f 100644 --- a/src/libnm-platform/nmp-object.h +++ b/src/libnm-platform/nmp-object.h @@ -314,6 +314,13 @@ typedef struct { typedef struct { NMPlatformIP4Route _public; + + /* The first hop is embedded in _public (in the + * ifindex, gateway and weight fields). + * Only if _public.n_nexthops is greater than 1, then + * this contains the remaining(!!) (_public.n_nexthops - 1) + * extra hops for ECMP multihop routes. */ + const NMPlatformIP4RtNextHop *extra_nexthops; } NMPObjectIP4Route; typedef struct {