/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <linux/nexthop.h>
#include <stdio.h>

#include "sd-netlink.h"

#include "alloc-util.h"
#include "conf-parser.h"
#include "extract-word.h"
#include "netlink-util.h"
#include "networkd-link.h"
#include "networkd-manager.h"
#include "networkd-network.h"
#include "networkd-nexthop.h"
#include "networkd-route.h"
#include "networkd-route-nexthop.h"
#include "networkd-route-util.h"
#include "ordered-set.h"
#include "parse-util.h"
#include "set.h"
#include "siphash24.h"
#include "socket-util.h"
#include "string-util.h"

void route_detach_from_nexthop(Route *route) {
        NextHop *nh;

        assert(route);
        assert(route->manager);

        if (route->nexthop_id == 0)
                return;

        if (nexthop_get_by_id(route->manager, route->nexthop_id, &nh) < 0)
                return;

        route_unref(set_remove(nh->routes, route));
}

void route_attach_to_nexthop(Route *route) {
        NextHop *nh;
        int r;

        assert(route);
        assert(route->manager);

        if (route->nexthop_id == 0)
                return;

        r = nexthop_get_by_id(route->manager, route->nexthop_id, &nh);
        if (r < 0) {
                if (route->manager->manage_foreign_nexthops)
                        log_debug_errno(r, "Route has unknown nexthop ID (%"PRIu32"), ignoring.",
                                        route->nexthop_id);
                return;
        }

        r = set_ensure_put(&nh->routes, &route_hash_ops_unref, route);
        if (r < 0)
                return (void) log_debug_errno(r, "Failed to save route to nexthop, ignoring: %m");
        if (r == 0)
                return (void) log_debug("Duplicated route assigned to nexthop, ignoring.");

        route_ref(route);
}

static void route_nexthop_done(RouteNextHop *nh) {
        assert(nh);

        free(nh->ifname);
}

RouteNextHop* route_nexthop_free(RouteNextHop *nh) {
        if (!nh)
                return NULL;

        route_nexthop_done(nh);

        return mfree(nh);
}

void route_nexthops_done(Route *route) {
        assert(route);

        route_nexthop_done(&route->nexthop);
        ordered_set_free(route->nexthops);
}

static void route_nexthop_hash_func_full(const RouteNextHop *nh, struct siphash *state, bool with_weight) {
        assert(nh);
        assert(state);

        /* See nh_comp() in net/ipv4/fib_semantics.c of the kernel. */

        siphash24_compress_typesafe(nh->family, state);

        /* For device-only nexthops parsed from config, family is AF_UNSPEC until verification.
         * We still need to hash weight/ifindex/ifname to distinguish different device-only entries. */
        if (IN_SET(nh->family, AF_INET, AF_INET6))
                in_addr_hash_func(&nh->gw, nh->family, state);

        if (with_weight)
                siphash24_compress_typesafe(nh->weight, state);
        siphash24_compress_typesafe(nh->ifindex, state);
        if (nh->ifindex == 0)
                siphash24_compress_string(nh->ifname, state); /* For Network or Request object. */
}

static int route_nexthop_compare_func_full(const RouteNextHop *a, const RouteNextHop *b, bool with_weight) {
        int r;

        assert(a);
        assert(b);

        r = CMP(a->family, b->family);
        if (r != 0)
                return r;

        /* For device-only nexthops parsed from config, family is AF_UNSPEC until verification.
         * We still need to compare weight/ifindex/ifname to distinguish different device-only entries. */
        if (IN_SET(a->family, AF_INET, AF_INET6)) {
                r = memcmp(&a->gw, &b->gw, FAMILY_ADDRESS_SIZE(a->family));
                if (r != 0)
                        return r;
        }

        if (with_weight) {
                r = CMP(a->weight, b->weight);
                if (r != 0)
                        return r;
        }

        r = CMP(a->ifindex, b->ifindex);
        if (r != 0)
                return r;

        if (a->ifindex == 0) {
                r = strcmp_ptr(a->ifname, b->ifname);
                if (r != 0)
                        return r;
        }

        return 0;
}

static void route_nexthop_hash_func(const RouteNextHop *nh, struct siphash *state) {
        route_nexthop_hash_func_full(nh, state, /* with_weight= */ true);
}

static int route_nexthop_compare_func(const RouteNextHop *a, const RouteNextHop *b) {
        return route_nexthop_compare_func_full(a, b, /* with_weight= */ true);
}

DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
        route_nexthop_hash_ops,
        RouteNextHop,
        route_nexthop_hash_func,
        route_nexthop_compare_func,
        route_nexthop_free);

static size_t route_n_nexthops(const Route *route) {
        if (route->nexthop_id != 0 || route_is_reject(route))
                return 0;

        if (ordered_set_isempty(route->nexthops))
                return 1;

        return ordered_set_size(route->nexthops);
}

void route_nexthops_hash_func(const Route *route, struct siphash *state) {
        assert(route);

        size_t nhs = route_n_nexthops(route);
        siphash24_compress_typesafe(nhs, state);

        switch (nhs) {
        case 0:
                siphash24_compress_typesafe(route->nexthop_id, state);
                return;

        case 1:
                route_nexthop_hash_func_full(&route->nexthop, state, /* with_weight= */ false);
                return;

        default: {
                RouteNextHop *nh;
                ORDERED_SET_FOREACH(nh, route->nexthops)
                        route_nexthop_hash_func(nh, state);
        }}
}

int route_nexthops_compare_func(const Route *a, const Route *b) {
        int r;

        assert(a);
        assert(b);

        size_t a_nhs = route_n_nexthops(a);
        size_t b_nhs = route_n_nexthops(b);
        r = CMP(a_nhs, b_nhs);
        if (r != 0)
                return r;

        switch (a_nhs) {
        case 0:
                return CMP(a->nexthop_id, b->nexthop_id);

        case 1:
                return route_nexthop_compare_func_full(&a->nexthop, &b->nexthop, /* with_weight= */ false);

        default: {
                RouteNextHop *nh;
                ORDERED_SET_FOREACH(nh, a->nexthops) {
                        r = CMP(nh, (RouteNextHop*) ordered_set_get(a->nexthops, nh));
                        if (r != 0)
                                return r;
                }
                return 0;
        }}
}

static int route_nexthop_copy(const RouteNextHop *src, RouteNextHop *dest) {
        assert(src);
        assert(dest);

        *dest = *src;

        /* unset pointer copied in the above. */
        dest->ifname = NULL;

        return strdup_to(&dest->ifname, src->ifindex > 0 ? NULL : src->ifname);
}

static int route_nexthop_dup(const RouteNextHop *src, RouteNextHop **ret) {
        _cleanup_(route_nexthop_freep) RouteNextHop *dest = NULL;
        int r;

        assert(src);
        assert(ret);

        dest = new(RouteNextHop, 1);
        if (!dest)
                return -ENOMEM;

        r = route_nexthop_copy(src, dest);
        if (r < 0)
                return r;

        *ret = TAKE_PTR(dest);
        return 0;
}

int route_nexthops_copy(const Route *src, const RouteNextHop *nh, Route *dest) {
        int r;

        assert(src);
        assert(dest);

        if (src->nexthop_id != 0 || route_is_reject(src))
                return 0;

        if (nh)
                return route_nexthop_copy(nh, &dest->nexthop);

        if (ordered_set_isempty(src->nexthops))
                return route_nexthop_copy(&src->nexthop, &dest->nexthop);

        ORDERED_SET_FOREACH(nh, src->nexthops) {
                _cleanup_(route_nexthop_freep) RouteNextHop *nh_dup = NULL;

                r = route_nexthop_dup(nh, &nh_dup);
                if (r < 0)
                        return r;

                r = ordered_set_ensure_put(&dest->nexthops, &route_nexthop_hash_ops, nh_dup);
                if (r < 0)
                        return r;
                assert(r > 0);

                TAKE_PTR(nh_dup);
        }

        return 0;
}

static bool multipath_routes_needs_adjust(const Route *route) {
        assert(route);

        RouteNextHop *nh;
        ORDERED_SET_FOREACH(nh, route->nexthops)
                if (route->nexthop.ifindex == 0)
                        return true;

        return false;
}

bool route_nexthops_needs_adjust(const Route *route) {
        assert(route);

        if (route->nexthop_id != 0)
                /* At this stage, the nexthop may not be configured, or may be under reconfiguring.
                 * Hence, we cannot know if the nexthop is blackhole or not. */
                return route->type != RTN_BLACKHOLE;

        if (route_is_reject(route))
                return false;

        if (ordered_set_isempty(route->nexthops))
                return route->nexthop.ifindex == 0;

        return multipath_routes_needs_adjust(route);
}

static bool route_nexthop_set_ifindex(RouteNextHop *nh, Link *link) {
        assert(nh);
        assert(link);
        assert(link->manager);

        if (nh->ifindex > 0) {
                nh->ifname = mfree(nh->ifname);
                return false;
        }

        /* If an interface name is specified, use it. Otherwise, use the interface that requests this route. */
        if (nh->ifname && link_get_by_name(link->manager, nh->ifname, &link) < 0)
                return false;

        nh->ifindex = link->ifindex;
        nh->ifname = mfree(nh->ifname);
        return true; /* updated */
}

int route_adjust_nexthops(Route *route, Link *link) {
        int r;

        assert(route);
        assert(link);
        assert(link->manager);

        /* When an IPv4 route has nexthop id and the nexthop type is blackhole, even though kernel sends
         * RTM_NEWROUTE netlink message with blackhole type, kernel's internal route type fib_rt_info::type
         * may not be blackhole. Thus, we cannot know the internal value. Moreover, on route removal, the
         * matching is done with the hidden value if we set non-zero type in RTM_DELROUTE message. So,
         * here let's set route type to BLACKHOLE when the nexthop is blackhole. */
        if (route->nexthop_id != 0) {
                NextHop *nexthop;

                r = nexthop_is_ready(link->manager, route->nexthop_id, &nexthop);
                if (r <= 0)
                        return r; /* r == 0 means the nexthop is under (re-)configuring.
                                   * We cannot use the currently remembered information. */

                if (!nexthop->blackhole)
                        return false;

                if (route->type == RTN_BLACKHOLE)
                        return false;

                route->type = RTN_BLACKHOLE;
                return true; /* updated */
        }

        if (route_is_reject(route))
                return false;

        if (ordered_set_isempty(route->nexthops))
                return route_nexthop_set_ifindex(&route->nexthop, link);

        if (!multipath_routes_needs_adjust(route))
                return false;

        _cleanup_ordered_set_free_ OrderedSet *nexthops = NULL;
        for (;;) {
                _cleanup_(route_nexthop_freep) RouteNextHop *nh = NULL;

                nh = ordered_set_steal_first(route->nexthops);
                if (!nh)
                        break;

                (void) route_nexthop_set_ifindex(nh, link);

                r = ordered_set_ensure_put(&nexthops, &route_nexthop_hash_ops, nh);
                if (r == -EEXIST)
                        continue; /* Duplicated? Let's drop the nexthop. */
                if (r < 0)
                        return r;
                assert(r > 0);

                TAKE_PTR(nh);
        }

        ordered_set_free(route->nexthops);
        route->nexthops = TAKE_PTR(nexthops);
        return true; /* updated */
}

int route_nexthop_get_link(Manager *manager, const RouteNextHop *nh, Link **ret) {
        assert(manager);
        assert(nh);

        if (nh->ifindex > 0)
                return link_get_by_index(manager, nh->ifindex, ret);
        if (nh->ifname)
                return link_get_by_name(manager, nh->ifname, ret);

        return -ENOENT;
}

static bool route_nexthop_is_ready_to_configure(const RouteNextHop *nh, Manager *manager, bool onlink) {
        Link *link;

        assert(nh);
        assert(manager);

        if (route_nexthop_get_link(manager, nh, &link) < 0)
                return false;

        if (!link_is_ready_to_configure(link, /* allow_unmanaged= */ true))
                return false;

        /* If the interface is not managed by us, we request that the interface has carrier.
         * That is, ConfigureWithoutCarrier=no is the default even for unamanaged interfaces. */
        if (!link->network && !link_has_carrier(link))
                return false;

        return gateway_is_ready(link, onlink, nh->family, &nh->gw);
}

int route_nexthops_is_ready_to_configure(const Route *route, Manager *manager) {
        int r;

        assert(route);
        assert(manager);

        if (route->nexthop_id != 0) {
                struct nexthop_grp *nhg;
                NextHop *nh;

                r = nexthop_is_ready(manager, route->nexthop_id, &nh);
                if (r <= 0)
                        return r;

                HASHMAP_FOREACH(nhg, nh->group) {
                        r = nexthop_is_ready(manager, nhg->id, NULL);
                        if (r <= 0)
                                return r;
                }

                return true;
        }

        if (route_is_reject(route))
                return true;

        if (ordered_set_isempty(route->nexthops))
                return route_nexthop_is_ready_to_configure(&route->nexthop, manager, FLAGS_SET(route->flags, RTNH_F_ONLINK));

        RouteNextHop *nh;
        ORDERED_SET_FOREACH(nh, route->nexthops)
                if (!route_nexthop_is_ready_to_configure(nh, manager, FLAGS_SET(route->flags, RTNH_F_ONLINK)))
                        return false;

        return true;
}

int route_nexthops_to_string(const Route *route, char **ret) {
        _cleanup_free_ char *buf = NULL;
        int r;

        assert(route);
        assert(ret);

        if (route->nexthop_id != 0) {
                if (asprintf(&buf, "nexthop: %"PRIu32, route->nexthop_id) < 0)
                        return -ENOMEM;

                *ret = TAKE_PTR(buf);
                return 0;
        }

        if (route_is_reject(route)) {
                buf = strdup("gw: n/a");
                if (!buf)
                        return -ENOMEM;

                *ret = TAKE_PTR(buf);
                return 0;
        }

        if (ordered_set_isempty(route->nexthops)) {
                if (in_addr_is_set(route->nexthop.family, &route->nexthop.gw)) {
                        if (asprintf(&buf, "gw: %s:%"PRIu32, IN_ADDR_TO_STRING(route->nexthop.family, &route->nexthop.gw), route->nexthop.weight + 1) < 0)
                                return -ENOMEM;
                } else if (route->gateway_from_dhcp_or_ra) {
                        if (route->nexthop.family == AF_INET)
                                buf = strdup("gw: _dhcp4");
                        else if (route->nexthop.family == AF_INET6)
                                buf = strdup("gw: _ipv6ra");
                        else
                                buf = strdup("gw: _dhcp");
                } else
                        buf = strdup("gw: n/a");
                if (!buf)
                        return -ENOMEM;

                *ret = TAKE_PTR(buf);
                return 0;
        }

        RouteNextHop *nh;
        ORDERED_SET_FOREACH(nh, route->nexthops) {
                const char *s = in_addr_is_set(nh->family, &nh->gw) ? IN_ADDR_TO_STRING(nh->family, &nh->gw) : NULL;

                if (nh->ifindex > 0)
                        r = strextendf_with_separator(&buf, ",", "%s@%i:%"PRIu32, strempty(s), nh->ifindex, nh->weight + 1);
                else if (nh->ifname)
                        r = strextendf_with_separator(&buf, ",", "%s@%s:%"PRIu32, strempty(s), nh->ifname, nh->weight + 1);
                else
                        r = strextendf_with_separator(&buf, ",", "%s:%"PRIu32, strempty(s), nh->weight + 1);
                if (r < 0)
                        return r;
        }

        char *p = strjoin("gw: ", strna(buf));
        if (!p)
                return -ENOMEM;

        *ret = p;
        return 0;
}

static int append_nexthop_one(const Route *route, const RouteNextHop *nh, struct rtattr **rta, size_t offset) {
        struct rtnexthop *rtnh;
        struct rtattr *new_rta;
        int r;

        assert(route);
        assert(IN_SET(route->family, AF_INET, AF_INET6));
        assert(nh);
        assert(rta);
        assert(*rta);

        new_rta = realloc(*rta, RTA_ALIGN((*rta)->rta_len) + RTA_SPACE(sizeof(struct rtnexthop)));
        if (!new_rta)
                return -ENOMEM;
        *rta = new_rta;

        rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset);
        *rtnh = (struct rtnexthop) {
                .rtnh_len = sizeof(*rtnh),
                .rtnh_ifindex = nh->ifindex,
                .rtnh_hops = nh->weight,
        };

        (*rta)->rta_len += sizeof(struct rtnexthop);

        /* For device-only nexthops, skip RTA_GATEWAY entirely. The kernel will use the
         * interface specified in rtnh_ifindex without requiring a gateway address. */
        if (in_addr_is_set(nh->family, &nh->gw)) {
                if (nh->family == route->family) {
                        r = rtattr_append_attribute(rta, RTA_GATEWAY, &nh->gw, FAMILY_ADDRESS_SIZE(nh->family));
                        if (r < 0)
                                goto clear;

                        rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset);
                        rtnh->rtnh_len += RTA_SPACE(FAMILY_ADDRESS_SIZE(nh->family));

                } else if (nh->family == AF_INET6) {
                        assert(route->family == AF_INET);

                        r = rtattr_append_attribute(rta, RTA_VIA,
                                                    &(RouteVia) {
                                                            .family = nh->family,
                                                            .address = nh->gw,
                                                    }, sizeof(RouteVia));
                        if (r < 0)
                                goto clear;

                        rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset);
                        rtnh->rtnh_len += RTA_SPACE(sizeof(RouteVia));

                } else if (nh->family == AF_INET)
                        assert_not_reached();
        }

        return 0;

clear:
        (*rta)->rta_len -= sizeof(struct rtnexthop);
        return r;
}

static int netlink_message_append_multipath_route(const Route *route, sd_netlink_message *message) {
        _cleanup_free_ struct rtattr *rta = NULL;
        size_t offset;
        int r;

        assert(route);
        assert(message);

        rta = new(struct rtattr, 1);
        if (!rta)
                return -ENOMEM;

        *rta = (struct rtattr) {
                .rta_type = RTA_MULTIPATH,
                .rta_len = RTA_LENGTH(0),
        };
        offset = (uint8_t *) RTA_DATA(rta) - (uint8_t *) rta;

        if (ordered_set_isempty(route->nexthops)) {
                r = append_nexthop_one(route, &route->nexthop, &rta, offset);
                if (r < 0)
                        return r;

        } else {
                RouteNextHop *nh;
                ORDERED_SET_FOREACH(nh, route->nexthops) {
                        struct rtnexthop *rtnh;

                        r = append_nexthop_one(route, nh, &rta, offset);
                        if (r < 0)
                                return r;

                        rtnh = (struct rtnexthop *)((uint8_t *) rta + offset);
                        offset = (uint8_t *) RTNH_NEXT(rtnh) - (uint8_t *) rta;
                }
        }

        return sd_netlink_message_append_data(message, RTA_MULTIPATH, RTA_DATA(rta), RTA_PAYLOAD(rta));
}

int route_nexthops_set_netlink_message(const Route *route, sd_netlink_message *message) {
        int r;

        assert(route);
        assert(IN_SET(route->family, AF_INET, AF_INET6));
        assert(message);

        if (route->nexthop_id != 0)
                return sd_netlink_message_append_u32(message, RTA_NH_ID, route->nexthop_id);

        if (route_is_reject(route))
                return 0;

        /* We request IPv6 multipath routes separately. Even though, if weight is non-zero, we need to use
         * RTA_MULTIPATH, as we have no way to specify the weight of the nexthop. */
        if (ordered_set_isempty(route->nexthops) && route->nexthop.weight == 0) {

                if (in_addr_is_set(route->nexthop.family, &route->nexthop.gw)) {
                        if (route->nexthop.family == route->family)
                                r = netlink_message_append_in_addr_union(message, RTA_GATEWAY, route->nexthop.family, &route->nexthop.gw);
                        else {
                                assert(route->family == AF_INET);
                                r = sd_netlink_message_append_data(message, RTA_VIA,
                                                                   &(const RouteVia) {
                                                                           .family = route->nexthop.family,
                                                                           .address = route->nexthop.gw,
                                                                   }, sizeof(RouteVia));
                        }
                        if (r < 0)
                                return r;
                }

                assert(route->nexthop.ifindex > 0);
                return sd_netlink_message_append_u32(message, RTA_OIF, route->nexthop.ifindex);
        }

        return netlink_message_append_multipath_route(route, message);
}

static int route_parse_nexthops(Route *route, const struct rtnexthop *rtnh, size_t size) {
        _cleanup_ordered_set_free_ OrderedSet *nexthops = NULL;
        int r;

        assert(route);
        assert(IN_SET(route->family, AF_INET, AF_INET6));
        assert(rtnh);

        if (size < sizeof(struct rtnexthop))
                return -EBADMSG;

        for (; size >= sizeof(struct rtnexthop); ) {
                _cleanup_(route_nexthop_freep) RouteNextHop *nh = NULL;

                if (NLMSG_ALIGN(rtnh->rtnh_len) > size)
                        return -EBADMSG;

                if (rtnh->rtnh_len < sizeof(struct rtnexthop))
                        return -EBADMSG;

                nh = new(RouteNextHop, 1);
                if (!nh)
                        return -ENOMEM;

                *nh = (RouteNextHop) {
                        .ifindex = rtnh->rtnh_ifindex,
                        .weight = rtnh->rtnh_hops,
                };

                if (rtnh->rtnh_len > sizeof(struct rtnexthop)) {
                        size_t len = rtnh->rtnh_len - sizeof(struct rtnexthop);
                        bool have_gw = false;

                        for (struct rtattr *attr = RTNH_DATA(rtnh); RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) {

                                switch (attr->rta_type) {
                                case RTA_GATEWAY:
                                        if (have_gw)
                                                return -EBADMSG;

                                        if (attr->rta_len != RTA_LENGTH(FAMILY_ADDRESS_SIZE(route->family)))
                                                return -EBADMSG;

                                        nh->family = route->family;
                                        memcpy(&nh->gw, RTA_DATA(attr), FAMILY_ADDRESS_SIZE(nh->family));
                                        have_gw = true;
                                        break;

                                case RTA_VIA:
                                        if (have_gw)
                                                return -EBADMSG;

                                        if (route->family != AF_INET)
                                                return -EBADMSG;

                                        if (attr->rta_len != RTA_LENGTH(sizeof(RouteVia)))
                                                return -EBADMSG;

                                        RouteVia *via = RTA_DATA(attr);
                                        if (via->family != AF_INET6)
                                                return -EBADMSG;

                                        nh->family = via->family;
                                        nh->gw = via->address;
                                        have_gw = true;
                                        break;
                                }
                        }
                }

                r = ordered_set_ensure_put(&nexthops, &route_nexthop_hash_ops, nh);
                assert(r != 0);
                if (r > 0)
                        TAKE_PTR(nh);
                else if (r != -EEXIST)
                        return r;

                size -= NLMSG_ALIGN(rtnh->rtnh_len);
                rtnh = RTNH_NEXT(rtnh);
        }

        ordered_set_free(route->nexthops);
        route->nexthops = TAKE_PTR(nexthops);
        return 0;
}

int route_nexthops_read_netlink_message(Route *route, sd_netlink_message *message) {
        int r;

        assert(route);
        assert(message);

        r = sd_netlink_message_read_u32(message, RTA_NH_ID, &route->nexthop_id);
        if (r < 0 && r != -ENODATA)
                return log_warning_errno(r, "rtnl: received route message with invalid nexthop id, ignoring: %m");

        if (route->nexthop_id != 0 || route_is_reject(route))
                /* IPv6 routes with reject type are always assigned to the loopback interface. See kernel's
                 * fib6_nh_init() in net/ipv6/route.c. However, we'd like to make it consistent with IPv4
                 * routes. Hence, skip reading of RTA_OIF. */
                return 0;

        uint32_t ifindex;
        r = sd_netlink_message_read_u32(message, RTA_OIF, &ifindex);
        if (r >= 0)
                route->nexthop.ifindex = (int) ifindex;
        else if (r != -ENODATA)
                return log_warning_errno(r, "rtnl: could not get ifindex from route message, ignoring: %m");

        if (route->nexthop.ifindex > 0) {
                r = netlink_message_read_in_addr_union(message, RTA_GATEWAY, route->family, &route->nexthop.gw);
                if (r >= 0) {
                        route->nexthop.family = route->family;
                        return 0;
                }
                if (r != -ENODATA)
                        return log_warning_errno(r, "rtnl: received route message without valid gateway, ignoring: %m");

                if (route->family != AF_INET)
                        return 0;

                RouteVia via;
                r = sd_netlink_message_read(message, RTA_VIA, sizeof(via), &via);
                if (r >= 0) {
                        route->nexthop.family = via.family;
                        route->nexthop.gw = via.address;
                        return 0;
                }
                if (r != -ENODATA)
                        return log_warning_errno(r, "rtnl: received route message without valid gateway, ignoring: %m");

                return 0;
        }

        size_t rta_len;
        _cleanup_free_ void *rta = NULL;
        r = sd_netlink_message_read_data(message, RTA_MULTIPATH, &rta_len, &rta);
        if (r == -ENODATA)
                return 0;
        if (r < 0)
                return log_warning_errno(r, "rtnl: failed to read RTA_MULTIPATH attribute, ignoring: %m");

        r = route_parse_nexthops(route, rta, rta_len);
        if (r < 0)
                return log_warning_errno(r, "rtnl: failed to parse RTA_MULTIPATH attribute, ignoring: %m");

        return 0;
}

#define log_route_section(route, fmt, ...)                              \
        ({                                                              \
                const Route *_route = (route);                          \
                log_section_warning_errno(                              \
                                _route ? _route->section : NULL,        \
                                SYNTHETIC_ERRNO(EINVAL),                \
                                fmt " Ignoring [Route] section.",       \
                                ##__VA_ARGS__);                         \
        })

int route_section_verify_nexthops(Route *route) {
        assert(route);
        assert(route->section);

        if (route->gateway_from_dhcp_or_ra) {
                assert(route->network);

                if (route->nexthop.family == AF_UNSPEC)
                        /* When deprecated Gateway=_dhcp is set, then assume gateway family based on other settings. */
                        switch (route->family) {
                        case AF_UNSPEC:
                                log_section_warning(route->section,
                                                    "Deprecated value \"_dhcp\" is specified for Gateway=. "
                                                    "Please use \"_dhcp4\" or \"_ipv6ra\" instead. Assuming \"_dhcp4\".");

                                route->nexthop.family = route->family = AF_INET;
                                break;
                        case AF_INET:
                        case AF_INET6:
                                log_section_warning(route->section,
                                                    "Deprecated value \"_dhcp\" is specified for Gateway=. "
                                                    "Assuming \"%s\" based on Destination=, Source=, or PreferredSource= setting.",
                                                    route->family == AF_INET ? "_dhcp4" : "_ipv6ra");

                                route->nexthop.family = route->family;
                                break;
                        default:
                                return log_route_section(route, "Invalid route family.");
                        }

                switch (route->nexthop.family) {
                case AF_INET:
                        if (!FLAGS_SET(route->network->dhcp, ADDRESS_FAMILY_IPV4))
                                return log_route_section(route, "Gateway=\"_dhcp4\" is specified but DHCPv4 client is disabled.");

                        route->source = NETWORK_CONFIG_SOURCE_DHCP4;
                        break;

                case AF_INET6:
                        if (route->network->ndisc == 0)
                                return log_route_section(route, "Gateway=\"_ipv6ra\" is specified but IPv6AcceptRA= is disabled.");

                        route->source = NETWORK_CONFIG_SOURCE_NDISC;
                        break;

                default:
                        assert_not_reached();
                }
        }

        /* When only Gateway= is specified, assume the route family based on the Gateway address. */
        if (route->family == AF_UNSPEC)
                route->family = route->nexthop.family;

        if (route->family == AF_UNSPEC) {
                assert(route->section);

                return log_route_section(route, "Route section without Gateway=, Destination=, Source=, or PreferredSource= field configured.");
        }

        if (route->gateway_onlink < 0 && in_addr_is_set(route->nexthop.family, &route->nexthop.gw) &&
            route->network && ordered_hashmap_isempty(route->network->addresses_by_section)) {
                /* If no address is configured, in most cases the gateway cannot be reachable.
                 * TODO: we may need to improve the condition above. */
                log_section_warning(route->section, "Gateway= without static address configured. Enabling GatewayOnLink= option.");
                route->gateway_onlink = true;
        }

        if (route->gateway_onlink >= 0)
                SET_FLAG(route->flags, RTNH_F_ONLINK, route->gateway_onlink);

        if (route->family == AF_INET6) {
                if (route->nexthop.family == AF_INET)
                        return log_route_section(route, "IPv4 gateway is configured for IPv6 route.");

                RouteNextHop *nh;
                ORDERED_SET_FOREACH(nh, route->nexthops)
                        if (nh->family == AF_INET)
                                return log_route_section(route, "IPv4 multipath route is specified for IPv6 route.");
        }

        if (route->nexthop_id != 0 &&
            (route->gateway_from_dhcp_or_ra ||
             in_addr_is_set(route->nexthop.family, &route->nexthop.gw) ||
             !ordered_set_isempty(route->nexthops)))
                return log_route_section(route, "NextHopId= cannot be specified with Gateway= or MultiPathRoute=.");

        if (route_is_reject(route) &&
            (route->gateway_from_dhcp_or_ra ||
             in_addr_is_set(route->nexthop.family, &route->nexthop.gw) ||
             !ordered_set_isempty(route->nexthops)))
                return log_route_section(route, "Reject type route cannot be specified with Gateway= or MultiPathRoute=.");

        if ((route->gateway_from_dhcp_or_ra ||
             in_addr_is_set(route->nexthop.family, &route->nexthop.gw)) &&
            !ordered_set_isempty(route->nexthops))
                return log_route_section(route, "Gateway= cannot be specified with MultiPathRoute=.");

        if (ordered_set_size(route->nexthops) == 1) {
                _cleanup_(route_nexthop_freep) RouteNextHop *nh = ordered_set_steal_first(route->nexthops);

                route_nexthop_done(&route->nexthop);
                route->nexthop = TAKE_STRUCT(*nh);

                assert(ordered_set_isempty(route->nexthops));
                route->nexthops = ordered_set_free(route->nexthops);
        }

        return 0;
}

int config_parse_gateway(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype, /* 0 : only address is accepted, 1 : also supports an empty string, _dhcp, and friends. */
                const char *rvalue,
                void *data,
                void *userdata) {

        Route *route = ASSERT_PTR(userdata);
        int r;

        if (ltype) {
                if (isempty(rvalue)) {
                        route->gateway_from_dhcp_or_ra = false;
                        route->nexthop.family = AF_UNSPEC;
                        route->nexthop.gw = IN_ADDR_NULL;
                        return 1;
                }

                if (streq(rvalue, "_dhcp")) {
                        route->gateway_from_dhcp_or_ra = true;
                        route->nexthop.family = AF_UNSPEC;
                        route->nexthop.gw = IN_ADDR_NULL;
                        return 1;
                }

                if (streq(rvalue, "_dhcp4")) {
                        route->gateway_from_dhcp_or_ra = true;
                        route->nexthop.family = AF_INET;
                        route->nexthop.gw = IN_ADDR_NULL;
                        return 1;
                }

                if (streq(rvalue, "_ipv6ra")) {
                        route->gateway_from_dhcp_or_ra = true;
                        route->nexthop.family = AF_INET6;
                        route->nexthop.gw = IN_ADDR_NULL;
                        return 1;
                }
        }

        assert(rvalue);

        r = in_addr_from_string_auto(rvalue, &route->nexthop.family, &route->nexthop.gw);
        if (r < 0)
                return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue);

        route->gateway_from_dhcp_or_ra = false;
        return 1;
}

int config_parse_route_nexthop(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        uint32_t id, *p = ASSERT_PTR(data);
        int r;

        if (isempty(rvalue)) {
                *p = 0;
                return 1;
        }

        r = safe_atou32(rvalue, &id);
        if (r < 0)
                return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue);
        if (id == 0) {
                log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid nexthop ID, ignoring assignment: %s", rvalue);
                return 0;
        }

        *p = id;
        return 1;
}

int config_parse_multipath_route(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        _cleanup_(route_nexthop_freep) RouteNextHop *nh = NULL;
        _cleanup_free_ char *word = NULL;
        OrderedSet **nexthops = ASSERT_PTR(data);
        const char *p;
        char *dev;
        int r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        if (isempty(rvalue)) {
                *nexthops = ordered_set_free(*nexthops);
                return 1;
        }

        nh = new0(RouteNextHop, 1);
        if (!nh)
                return log_oom();

        p = rvalue;
        r = extract_first_word(&p, &word, NULL, 0);
        if (r <= 0)
                return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue);

        dev = strchr(word, '@');
        if (dev) {
                *dev++ = '\0';

                r = parse_ifindex(dev);
                if (r > 0)
                        nh->ifindex = r;
                else {
                        if (!ifname_valid_full(dev, IFNAME_VALID_ALTERNATIVE)) {
                                log_syntax(unit, LOG_WARNING, filename, line, 0,
                                           "Invalid interface name '%s' in %s=, ignoring: %s", dev, lvalue, rvalue);
                                return 0;
                        }

                        nh->ifname = strdup(dev);
                        if (!nh->ifname)
                                return log_oom();
                }
        }

        if (isempty(word)) {
                if (!dev)
                        return log_syntax_parse_error(unit, filename, line, SYNTHETIC_ERRNO(EINVAL), lvalue, rvalue);
        } else {
                r = in_addr_from_string_auto(word, &nh->family, &nh->gw);
                if (r < 0) {
                        log_syntax(unit, LOG_WARNING, filename, line, r,
                                   "Invalid multipath route gateway '%s', ignoring assignment: %m", rvalue);
                        return 0;
                }
        }

        if (!isempty(p)) {
                r = safe_atou32(p, &nh->weight);
                if (r < 0) {
                        log_syntax(unit, LOG_WARNING, filename, line, r,
                                   "Invalid multipath route weight, ignoring assignment: %s", p);
                        return 0;
                }
                /* ip command takes weight in the range 1…255, while kernel takes the value in the
                 * range 0…254. MultiPathRoute= setting also takes weight in the same range which ip
                 * command uses, then networkd decreases by one and stores it to match the range which
                 * kernel uses. */
                if (nh->weight == 0 || nh->weight > 256) {
                        log_syntax(unit, LOG_WARNING, filename, line, 0,
                                   "Invalid multipath route weight, ignoring assignment: %s", p);
                        return 0;
                }
                nh->weight--;
        }

        r = ordered_set_ensure_put(nexthops, &route_nexthop_hash_ops, nh);
        if (r == -ENOMEM)
                return log_oom();
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to store multipath route, ignoring assignment: %m");
                return 0;
        }

        TAKE_PTR(nh);
        return 1;
}
