/* SPDX-License-Identifier: LGPL-2.1-or-later */ /*** Copyright © 2014 Intel Corporation. All rights reserved. ***/ #include #include #include #include #include "sd-ndisc.h" #include "event-util.h" #include "missing_network.h" #include "ndisc-router-internal.h" #include "networkd-address-generation.h" #include "networkd-address.h" #include "networkd-dhcp6.h" #include "networkd-manager.h" #include "networkd-ndisc.h" #include "networkd-queue.h" #include "networkd-route.h" #include "networkd-state-file.h" #include "networkd-sysctl.h" #include "string-table.h" #include "string-util.h" #include "strv.h" #include "sysctl-util.h" #define NDISC_DNSSL_MAX 64U #define NDISC_RDNSS_MAX 64U /* Not defined in the RFC, but let's set an upper limit to make not consume much memory. * This should be safe as typically there should be at most 1 portal per network. */ #define NDISC_CAPTIVE_PORTAL_MAX 64U /* Neither defined in the RFC. Just for safety. Otherwise, malformed messages can make clients trigger OOM. * Not sure if the threshold is high enough. Let's adjust later if not. */ #define NDISC_PREF64_MAX 64U static int ndisc_drop_outdated(Link *link, const struct in6_addr *router, usec_t timestamp_usec); bool link_ndisc_enabled(Link *link) { assert(link); if (!socket_ipv6_is_supported()) return false; if (link->flags & IFF_LOOPBACK) return false; if (link->iftype == ARPHRD_CAN) return false; if (!link->network) return false; if (!link_may_have_ipv6ll(link, /* check_multicast = */ true)) return false; /* Honor explicitly specified value. */ if (link->network->ndisc >= 0) return link->network->ndisc; /* Disable if RADV is enabled. */ if (link_radv_enabled(link)) return false; /* Accept RAs if IPv6 forwarding is disabled, and ignore RAs if IPv6 forwarding is enabled. */ int t = link_get_ip_forwarding(link, AF_INET6); if (t >= 0) return !t; /* Otherwise, defaults to true. */ return true; } void network_adjust_ndisc(Network *network) { assert(network); if (!FLAGS_SET(network->link_local, ADDRESS_FAMILY_IPV6)) { if (network->ndisc > 0) log_warning("%s: IPv6AcceptRA= is enabled but IPv6 link-local addressing is disabled or not supported. " "Disabling IPv6AcceptRA=.", network->filename); network->ndisc = false; } /* When RouterAllowList=, PrefixAllowList= or RouteAllowList= are specified, then * RouterDenyList=, PrefixDenyList= or RouteDenyList= are ignored, respectively. */ if (!set_isempty(network->ndisc_allow_listed_router)) network->ndisc_deny_listed_router = set_free_free(network->ndisc_deny_listed_router); if (!set_isempty(network->ndisc_allow_listed_prefix)) network->ndisc_deny_listed_prefix = set_free_free(network->ndisc_deny_listed_prefix); if (!set_isempty(network->ndisc_allow_listed_route_prefix)) network->ndisc_deny_listed_route_prefix = set_free_free(network->ndisc_deny_listed_route_prefix); } static int ndisc_check_ready(Link *link); static int ndisc_address_ready_callback(Address *address) { Address *a; assert(address); assert(address->link); SET_FOREACH(a, address->link->addresses) if (a->source == NETWORK_CONFIG_SOURCE_NDISC) a->callback = NULL; return ndisc_check_ready(address->link); } static int ndisc_check_ready(Link *link) { bool found = false, ready = false; Address *address; assert(link); if (link->ndisc_messages > 0) { log_link_debug(link, "%s(): SLAAC addresses and routes are not set.", __func__); return 0; } SET_FOREACH(address, link->addresses) { if (address->source != NETWORK_CONFIG_SOURCE_NDISC) continue; found = true; if (address_is_ready(address)) { ready = true; break; } } if (found && !ready) { SET_FOREACH(address, link->addresses) if (address->source == NETWORK_CONFIG_SOURCE_NDISC) address->callback = ndisc_address_ready_callback; log_link_debug(link, "%s(): no SLAAC address is ready.", __func__); return 0; } link->ndisc_configured = true; log_link_debug(link, "SLAAC addresses and routes set."); link_check_ready(link); return 0; } static int ndisc_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Route *route) { int r; assert(link); r = route_configure_handler_internal(rtnl, m, link, route, "Could not set NDisc route"); if (r <= 0) return r; r = ndisc_check_ready(link); if (r < 0) link_enter_failed(link); return 1; } static void ndisc_set_route_priority(Link *link, Route *route) { assert(link); assert(route); if (route->priority_set) return; /* explicitly configured. */ switch (route->pref) { case SD_NDISC_PREFERENCE_LOW: route->priority = link->network->ndisc_route_metric_low; break; case SD_NDISC_PREFERENCE_MEDIUM: route->priority = link->network->ndisc_route_metric_medium; break; case SD_NDISC_PREFERENCE_HIGH: route->priority = link->network->ndisc_route_metric_high; break; default: assert_not_reached(); } } static int ndisc_request_route(Route *route, Link *link) { int r; assert(route); assert(link); assert(link->manager); assert(link->network); route->source = NETWORK_CONFIG_SOURCE_NDISC; if (!route->table_set) route->table = link_get_ndisc_route_table(link); r = route_metric_set(&route->metric, RTAX_QUICKACK, link->network->ndisc_quickack); if (r < 0) return r; r = route_adjust_nexthops(route, link); if (r < 0) return r; uint8_t pref, pref_original = route->pref; FOREACH_ARGUMENT(pref, SD_NDISC_PREFERENCE_LOW, SD_NDISC_PREFERENCE_MEDIUM, SD_NDISC_PREFERENCE_HIGH) { Route *existing; Request *req; /* If the preference is specified by the user config (that is, for semi-static routes), * rather than RA, then only search conflicting routes that have the same preference. */ if (route->pref_set && pref != pref_original) continue; route->pref = pref; ndisc_set_route_priority(link, route); /* Note, here do not call route_remove_and_cancel() with 'route' directly, otherwise * existing route(s) may be removed needlessly. */ if (route_get(link->manager, route, &existing) >= 0) { /* Found an existing route that may conflict with this route. */ if (!route_can_update(existing, route)) { log_link_debug(link, "Found an existing route that conflicts with new route based on a received RA, removing."); r = route_remove_and_cancel(existing, link->manager); if (r < 0) return r; } } if (route_get_request(link->manager, route, &req) >= 0) { existing = ASSERT_PTR(req->userdata); if (!route_can_update(existing, route)) { log_link_debug(link, "Found a pending route request that conflicts with new request based on a received RA, cancelling."); r = route_remove_and_cancel(existing, link->manager); if (r < 0) return r; } } } /* The preference (and priority) may be changed in the above loop. Restore it. */ route->pref = pref_original; ndisc_set_route_priority(link, route); bool is_new = route_get(link->manager, route, NULL) < 0; r = link_request_route(link, route, &link->ndisc_messages, ndisc_route_handler); if (r < 0) return r; if (r > 0 && is_new) link->ndisc_configured = false; return 0; } static int ndisc_request_router_route(Route *route, Link *link, sd_ndisc_router *rt) { int r; assert(route); assert(link); assert(rt); r = sd_ndisc_router_get_sender_address(rt, &route->provider.in6); if (r < 0) return r; if (!route->protocol_set) route->protocol = RTPROT_RA; return ndisc_request_route(route, link); } static int ndisc_remove_route(Route *route, Link *link) { int r; assert(route); assert(link); assert(link->manager); ndisc_set_route_priority(link, route); if (!route->table_set) route->table = link_get_ndisc_route_table(link); r = route_adjust_nexthops(route, link); if (r < 0) return r; if (route->pref_set) { ndisc_set_route_priority(link, route); return route_remove_and_cancel(route, link->manager); } uint8_t pref; FOREACH_ARGUMENT(pref, SD_NDISC_PREFERENCE_LOW, SD_NDISC_PREFERENCE_MEDIUM, SD_NDISC_PREFERENCE_HIGH) { route->pref = pref; ndisc_set_route_priority(link, route); r = route_remove_and_cancel(route, link->manager); if (r < 0) return r; } return 0; } static int ndisc_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) { int r; assert(link); r = address_configure_handler_internal(rtnl, m, link, "Could not set NDisc address"); if (r <= 0) return r; r = ndisc_check_ready(link); if (r < 0) link_enter_failed(link); return 1; } static int ndisc_request_address(Address *address, Link *link) { bool is_new; int r; assert(address); assert(link); address->source = NETWORK_CONFIG_SOURCE_NDISC; r = free_and_strdup_warn(&address->netlabel, link->network->ndisc_netlabel); if (r < 0) return r; Address *existing; if (address_get_harder(link, address, &existing) < 0) is_new = true; else if (address_can_update(existing, address)) is_new = false; else if (existing->source == NETWORK_CONFIG_SOURCE_DHCP6) { /* SLAAC address is preferred over DHCPv6 address. */ log_link_debug(link, "Conflicting DHCPv6 address %s exists, removing.", IN_ADDR_PREFIX_TO_STRING(existing->family, &existing->in_addr, existing->prefixlen)); r = address_remove(existing, link); if (r < 0) return r; is_new = true; } else { /* Conflicting static address is configured?? */ log_link_debug(link, "Conflicting address %s exists, ignoring request.", IN_ADDR_PREFIX_TO_STRING(existing->family, &existing->in_addr, existing->prefixlen)); return 0; } r = link_request_address(link, address, &link->ndisc_messages, ndisc_address_handler, NULL); if (r < 0) return r; if (r > 0 && is_new) link->ndisc_configured = false; return 0; } int ndisc_reconfigure_address(Address *address, Link *link) { int r; assert(address); assert(address->source == NETWORK_CONFIG_SOURCE_NDISC); assert(link); r = regenerate_address(address, link); if (r <= 0) return r; r = ndisc_request_address(address, link); if (r < 0) return r; if (!link->ndisc_configured) link_set_state(link, LINK_STATE_CONFIGURING); link_check_ready(link); return 0; } static int ndisc_redirect_route_new(sd_ndisc_redirect *rd, Route **ret) { _cleanup_(route_unrefp) Route *route = NULL; struct in6_addr gateway, destination; int r; assert(rd); assert(ret); r = sd_ndisc_redirect_get_target_address(rd, &gateway); if (r < 0) return r; r = sd_ndisc_redirect_get_destination_address(rd, &destination); if (r < 0) return r; r = route_new(&route); if (r < 0) return r; route->family = AF_INET6; if (!in6_addr_equal(&gateway, &destination)) { route->nexthop.gw.in6 = gateway; route->nexthop.family = AF_INET6; } route->dst.in6 = destination; route->dst_prefixlen = 128; route->protocol = RTPROT_REDIRECT; r = sd_ndisc_redirect_get_sender_address(rd, &route->provider.in6); if (r < 0) return r; *ret = TAKE_PTR(route); return 0; } static int ndisc_remove_redirect_route(Link *link, sd_ndisc_redirect *rd) { _cleanup_(route_unrefp) Route *route = NULL; int r; assert(link); assert(rd); r = ndisc_redirect_route_new(rd, &route); if (r < 0) return r; return ndisc_remove_route(route, link); } static void ndisc_redirect_hash_func(const sd_ndisc_redirect *x, struct siphash *state) { struct in6_addr dest = {}; assert(x); assert(state); (void) sd_ndisc_redirect_get_destination_address((sd_ndisc_redirect*) x, &dest); siphash24_compress_typesafe(dest, state); } static int ndisc_redirect_compare_func(const sd_ndisc_redirect *x, const sd_ndisc_redirect *y) { struct in6_addr dest_x = {}, dest_y = {}; assert(x); assert(y); (void) sd_ndisc_redirect_get_destination_address((sd_ndisc_redirect*) x, &dest_x); (void) sd_ndisc_redirect_get_destination_address((sd_ndisc_redirect*) y, &dest_y); return memcmp(&dest_x, &dest_y, sizeof(dest_x)); } DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( ndisc_redirect_hash_ops, sd_ndisc_redirect, ndisc_redirect_hash_func, ndisc_redirect_compare_func, sd_ndisc_redirect_unref); static int ndisc_redirect_equal(sd_ndisc_redirect *x, sd_ndisc_redirect *y) { struct in6_addr a, b; int r; assert(x); assert(y); r = sd_ndisc_redirect_get_destination_address(x, &a); if (r < 0) return r; r = sd_ndisc_redirect_get_destination_address(y, &b); if (r < 0) return r; if (!in6_addr_equal(&a, &b)) return false; r = sd_ndisc_redirect_get_target_address(x, &a); if (r < 0) return r; r = sd_ndisc_redirect_get_target_address(y, &b); if (r < 0) return r; return in6_addr_equal(&a, &b); } static int ndisc_redirect_drop_conflict(Link *link, sd_ndisc_redirect *rd) { _cleanup_(sd_ndisc_redirect_unrefp) sd_ndisc_redirect *existing = NULL; int r; assert(link); assert(rd); existing = set_remove(link->ndisc_redirects, rd); if (!existing) return 0; r = ndisc_redirect_equal(rd, existing); if (r != 0) return r; return ndisc_remove_redirect_route(link, existing); } static int ndisc_redirect_verify_sender(Link *link, sd_ndisc_redirect *rd) { int r; assert(link); assert(rd); /* RFC 4861 section 8.1 * The IP source address of the Redirect is the same as the current first-hop router for the specified * ICMP Destination Address. */ struct in6_addr sender; r = sd_ndisc_redirect_get_sender_address(rd, &sender); if (r < 0) return r; /* We will reuse the sender's router lifetime as the lifetime of the redirect route. Hence, if we * have not remembered an RA from the sender, refuse the Redirect message. */ sd_ndisc_router *router = hashmap_get(link->ndisc_routers_by_sender, &sender); if (!router) return false; sd_ndisc_redirect *existing = set_get(link->ndisc_redirects, rd); if (existing) { struct in6_addr target, dest; /* If we have received Redirect message for the host, the sender must be the previous target. */ r = sd_ndisc_redirect_get_target_address(existing, &target); if (r < 0) return r; if (in6_addr_equal(&sender, &target)) return true; /* If the existing redirect route is on-link, that is, the destination and target address are * equivalent, then also accept Redirect message from the current default router. This is not * mentioned by the RFC, but without this, we cannot update on-link redirect route. */ r = sd_ndisc_redirect_get_destination_address(existing, &dest); if (r < 0) return r; if (!in6_addr_equal(&dest, &target)) return false; } /* Check if the sender is one of the known router with highest priority. */ uint8_t preference; r = sd_ndisc_router_get_preference(router, &preference); if (r < 0) return r; if (preference == SD_NDISC_PREFERENCE_HIGH) return true; sd_ndisc_router *rt; HASHMAP_FOREACH(rt, link->ndisc_routers_by_sender) { if (rt == router) continue; uint8_t pref; if (sd_ndisc_router_get_preference(rt, &pref) < 0) continue; if (pref == SD_NDISC_PREFERENCE_HIGH || (pref == SD_NDISC_PREFERENCE_MEDIUM && preference == SD_NDISC_PREFERENCE_LOW)) return false; } return true; } static int ndisc_redirect_handler(Link *link, sd_ndisc_redirect *rd) { int r; assert(link); assert(link->network); assert(rd); if (!link->network->ndisc_use_redirect) return 0; usec_t now_usec; r = sd_event_now(link->manager->event, CLOCK_BOOTTIME, &now_usec); if (r < 0) return r; r = ndisc_drop_outdated(link, /* router = */ NULL, now_usec); if (r < 0) return r; r = ndisc_redirect_verify_sender(link, rd); if (r <= 0) return r; /* First, drop conflicting redirect route, if exists. */ r = ndisc_redirect_drop_conflict(link, rd); if (r < 0) return r; /* Then, remember the received message. */ r = set_ensure_put(&link->ndisc_redirects, &ndisc_redirect_hash_ops, rd); if (r < 0) return r; sd_ndisc_redirect_ref(rd); /* Finally, request the corresponding route. */ _cleanup_(route_unrefp) Route *route = NULL; r = ndisc_redirect_route_new(rd, &route); if (r < 0) return r; sd_ndisc_router *rt = hashmap_get(link->ndisc_routers_by_sender, &route->provider.in6); if (!rt) return -EADDRNOTAVAIL; r = sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &route->lifetime_usec); if (r < 0) return r; return ndisc_request_route(route, link); } static int ndisc_drop_redirect(Link *link, const struct in6_addr *router) { int r, ret = 0; assert(link); sd_ndisc_redirect *rd; SET_FOREACH(rd, link->ndisc_redirects) { if (router) { struct in6_addr a; if (!(sd_ndisc_redirect_get_sender_address(rd, &a) >= 0 && in6_addr_equal(&a, router)) && !(sd_ndisc_redirect_get_target_address(rd, &a) >= 0 && in6_addr_equal(&a, router))) continue; } r = ndisc_remove_redirect_route(link, rd); if (r < 0) RET_GATHER(ret, log_link_warning_errno(link, r, "Failed to remove redirect route, ignoring: %m")); sd_ndisc_redirect_unref(set_remove(link->ndisc_redirects, rd)); } return ret; } static int ndisc_update_redirect_sender(Link *link, const struct in6_addr *original_address, const struct in6_addr *current_address) { int r; assert(link); assert(original_address); assert(current_address); sd_ndisc_redirect *rd; SET_FOREACH(rd, link->ndisc_redirects) { struct in6_addr sender; r = sd_ndisc_redirect_get_sender_address(rd, &sender); if (r < 0) return r; if (!in6_addr_equal(&sender, original_address)) continue; r = sd_ndisc_redirect_set_sender_address(rd, current_address); if (r < 0) return r; } return 0; } static int ndisc_router_drop_default(Link *link, sd_ndisc_router *rt) { _cleanup_(route_unrefp) Route *route = NULL; struct in6_addr gateway; int r; assert(link); assert(link->network); assert(rt); r = sd_ndisc_router_get_sender_address(rt, &gateway); if (r < 0) return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); r = route_new(&route); if (r < 0) return log_oom(); route->family = AF_INET6; route->nexthop.family = AF_INET6; route->nexthop.gw.in6 = gateway; r = ndisc_remove_route(route, link); if (r < 0) return log_link_warning_errno(link, r, "Failed to remove the default gateway configured by RA: %m"); Route *route_gw; HASHMAP_FOREACH(route_gw, link->network->routes_by_section) { _cleanup_(route_unrefp) Route *tmp = NULL; if (!route_gw->gateway_from_dhcp_or_ra) continue; if (route_gw->nexthop.family != AF_INET6) continue; r = route_dup(route_gw, NULL, &tmp); if (r < 0) return r; tmp->nexthop.gw.in6 = gateway; r = ndisc_remove_route(tmp, link); if (r < 0) return log_link_warning_errno(link, r, "Could not remove semi-static gateway: %m"); } return 0; } static int ndisc_router_process_default(Link *link, sd_ndisc_router *rt) { usec_t lifetime_usec; struct in6_addr gateway; uint8_t preference; int r; assert(link); assert(link->network); assert(rt); /* If the router lifetime is zero, the router should not be used as the default gateway. */ r = sd_ndisc_router_get_lifetime(rt, NULL); if (r < 0) return r; if (r == 0) return ndisc_router_drop_default(link, rt); if (!link->network->ndisc_use_gateway && hashmap_isempty(link->network->routes_by_section)) return 0; r = sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get gateway lifetime from RA: %m"); r = sd_ndisc_router_get_sender_address(rt, &gateway); if (r < 0) return log_link_warning_errno(link, r, "Failed to get gateway address from RA: %m"); r = sd_ndisc_router_get_preference(rt, &preference); if (r < 0) return log_link_warning_errno(link, r, "Failed to get router preference from RA: %m"); if (link->network->ndisc_use_gateway) { _cleanup_(route_unrefp) Route *route = NULL; r = route_new(&route); if (r < 0) return log_oom(); route->family = AF_INET6; route->pref = preference; route->nexthop.family = AF_INET6; route->nexthop.gw.in6 = gateway; route->lifetime_usec = lifetime_usec; r = ndisc_request_router_route(route, link, rt); if (r < 0) return log_link_warning_errno(link, r, "Could not request default route: %m"); } Route *route_gw; HASHMAP_FOREACH(route_gw, link->network->routes_by_section) { _cleanup_(route_unrefp) Route *route = NULL; if (!route_gw->gateway_from_dhcp_or_ra) continue; if (route_gw->nexthop.family != AF_INET6) continue; r = route_dup(route_gw, NULL, &route); if (r < 0) return r; route->nexthop.gw.in6 = gateway; if (!route->pref_set) route->pref = preference; route->lifetime_usec = lifetime_usec; r = ndisc_request_router_route(route, link, rt); if (r < 0) return log_link_warning_errno(link, r, "Could not request gateway: %m"); } return 0; } DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( ndisc_router_hash_ops, struct in6_addr, in6_addr_hash_func, in6_addr_compare_func, sd_ndisc_router, sd_ndisc_router_unref); static int ndisc_update_router_address(Link *link, const struct in6_addr *original_address, const struct in6_addr *current_address) { _cleanup_(sd_ndisc_router_unrefp) sd_ndisc_router *rt = NULL; int r; assert(link); assert(original_address); assert(current_address); rt = hashmap_remove(link->ndisc_routers_by_sender, original_address); if (!rt) return 0; /* If we already received an RA from the new address, then forget the RA from the old address. */ if (hashmap_contains(link->ndisc_routers_by_sender, current_address)) return 0; /* Otherwise, update the sender address of the previously received RA. */ r = sd_ndisc_router_set_sender_address(rt, current_address); if (r < 0) return r; r = hashmap_put(link->ndisc_routers_by_sender, &rt->packet->sender_address, rt); if (r < 0) return r; TAKE_PTR(rt); return 0; } static int ndisc_drop_router_one(Link *link, sd_ndisc_router *rt, usec_t timestamp_usec) { usec_t lifetime_usec; int r; assert(link); assert(rt); assert(rt->packet); r = sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); if (r < 0) return r; if (lifetime_usec > timestamp_usec) return 0; r = ndisc_drop_redirect(link, &rt->packet->sender_address); sd_ndisc_router_unref(hashmap_remove(link->ndisc_routers_by_sender, &rt->packet->sender_address)); return r; } static int ndisc_drop_routers(Link *link, const struct in6_addr *router, usec_t timestamp_usec) { sd_ndisc_router *rt; int ret = 0; assert(link); if (router) { rt = hashmap_get(link->ndisc_routers_by_sender, router); if (!rt) return 0; return ndisc_drop_router_one(link, rt, timestamp_usec); } HASHMAP_FOREACH_KEY(rt, router, link->ndisc_routers_by_sender) RET_GATHER(ret, ndisc_drop_router_one(link, rt, timestamp_usec)); return ret; } static int ndisc_remember_router(Link *link, sd_ndisc_router *rt) { int r; assert(link); assert(rt); assert(rt->packet); sd_ndisc_router_unref(hashmap_remove(link->ndisc_routers_by_sender, &rt->packet->sender_address)); /* Remember RAs with non-zero lifetime. */ r = sd_ndisc_router_get_lifetime(rt, NULL); if (r <= 0) return r; r = hashmap_ensure_put(&link->ndisc_routers_by_sender, &ndisc_router_hash_ops, &rt->packet->sender_address, rt); if (r < 0) return r; sd_ndisc_router_ref(rt); return 0; } static int ndisc_router_process_reachable_time(Link *link, sd_ndisc_router *rt) { usec_t reachable_time, msec; int r; assert(link); assert(link->network); assert(rt); if (!link->network->ndisc_use_reachable_time) return 0; r = sd_ndisc_router_get_reachable_time(rt, &reachable_time); if (r < 0) return log_link_warning_errno(link, r, "Failed to get reachable time from RA: %m"); /* 0 is the unspecified value and must not be set (see RFC4861, 6.3.4) */ if (!timestamp_is_set(reachable_time)) return 0; msec = DIV_ROUND_UP(reachable_time, USEC_PER_MSEC); if (msec <= 0 || msec > UINT32_MAX) { log_link_debug(link, "Failed to get reachable time from RA - out of range (%"PRIu64"), ignoring", msec); return 0; } /* Set the reachable time for Neighbor Solicitations. */ r = sysctl_write_ip_neighbor_property_uint32(AF_INET6, link->ifname, "base_reachable_time_ms", (uint32_t) msec); if (r < 0) log_link_warning_errno(link, r, "Failed to apply neighbor reachable time (%"PRIu64"), ignoring: %m", msec); return 0; } static int ndisc_router_process_retransmission_time(Link *link, sd_ndisc_router *rt) { usec_t retrans_time, msec; int r; assert(link); assert(link->network); assert(rt); if (!link->network->ndisc_use_retransmission_time) return 0; r = sd_ndisc_router_get_retransmission_time(rt, &retrans_time); if (r < 0) return log_link_warning_errno(link, r, "Failed to get retransmission time from RA: %m"); /* 0 is the unspecified value and must not be set (see RFC4861, 6.3.4) */ if (!timestamp_is_set(retrans_time)) return 0; msec = DIV_ROUND_UP(retrans_time, USEC_PER_MSEC); if (msec <= 0 || msec > UINT32_MAX) { log_link_debug(link, "Failed to get retransmission time from RA - out of range (%"PRIu64"), ignoring", msec); return 0; } /* Set the retransmission time for Neighbor Solicitations. */ r = sysctl_write_ip_neighbor_property_uint32(AF_INET6, link->ifname, "retrans_time_ms", (uint32_t) msec); if (r < 0) log_link_warning_errno(link, r, "Failed to apply neighbor retransmission time (%"PRIu64"), ignoring: %m", msec); return 0; } static int ndisc_router_process_hop_limit(Link *link, sd_ndisc_router *rt) { uint8_t hop_limit; int r; assert(link); assert(link->network); assert(rt); if (!link->network->ndisc_use_hop_limit) return 0; r = sd_ndisc_router_get_hop_limit(rt, &hop_limit); if (r < 0) return log_link_warning_errno(link, r, "Failed to get hop limit from RA: %m"); /* 0 is the unspecified value and must not be set (see RFC4861, 6.3.4): * * A Router Advertisement field (e.g., Cur Hop Limit, Reachable Time, and Retrans Timer) may contain * a value denoting that it is unspecified. In such cases, the parameter should be ignored and the * host should continue using whatever value it is already using. In particular, a host MUST NOT * interpret the unspecified value as meaning change back to the default value that was in use before * the first Router Advertisement was received. * * If the received Cur Hop Limit value is non-zero, the host SHOULD set * its CurHopLimit variable to the received value.*/ if (hop_limit <= 0) return 0; r = sysctl_write_ip_property_uint32(AF_INET6, link->ifname, "hop_limit", (uint32_t) hop_limit); if (r < 0) log_link_warning_errno(link, r, "Failed to apply hop_limit (%u), ignoring: %m", hop_limit); return 0; } static int ndisc_router_process_mtu(Link *link, sd_ndisc_router *rt) { uint32_t mtu; int r; assert(link); assert(link->network); assert(rt); if (!link->network->ndisc_use_mtu) return 0; r = sd_ndisc_router_get_mtu(rt, &mtu); if (r == -ENODATA) return 0; if (r < 0) return log_link_warning_errno(link, r, "Failed to get MTU from RA: %m"); link->ndisc_mtu = mtu; r = link_set_ipv6_mtu(link, LOG_DEBUG); if (r < 0) log_link_warning_errno(link, r, "Failed to apply IPv6 MTU (%"PRIu32"), ignoring: %m", mtu); return 0; } static int ndisc_address_set_lifetime(Address *address, Link *link, sd_ndisc_router *rt) { Address *existing; usec_t t; int r; assert(address); assert(link); assert(rt); /* This is mostly based on RFC 4862 section 5.5.3 (e). However, the definition of 'RemainingLifetime' * is ambiguous, and there is no clear explanation when the address is not assigned yet. If we assume * that 'RemainingLifetime' is zero in that case, then IPv6 Core Conformance test [v6LC.3.2.5 Part C] * fails. So, in such case, we skip the conditions about 'RemainingLifetime'. */ r = sd_ndisc_router_prefix_get_valid_lifetime_timestamp(rt, CLOCK_BOOTTIME, &address->lifetime_valid_usec); if (r < 0) return r; r = sd_ndisc_router_prefix_get_preferred_lifetime_timestamp(rt, CLOCK_BOOTTIME, &address->lifetime_preferred_usec); if (r < 0) return r; /* RFC 4862 section 5.5.3 (e) * 1. If the received Valid Lifetime is greater than 2 hours or greater than RemainingLifetime, * set the valid lifetime of the corresponding address to the advertised Valid Lifetime. */ r = sd_ndisc_router_prefix_get_valid_lifetime(rt, &t); if (r < 0) return r; if (t > 2 * USEC_PER_HOUR) return 0; if (address_get(link, address, &existing) < 0 || existing->source != NETWORK_CONFIG_SOURCE_NDISC) return 0; if (address->lifetime_valid_usec > existing->lifetime_valid_usec) return 0; /* 2. If RemainingLifetime is less than or equal to 2 hours, ignore the Prefix Information option * with regards to the valid lifetime, unless the Router Advertisement from which this option was * obtained has been authenticated (e.g., via Secure Neighbor Discovery [RFC3971]). If the Router * Advertisement was authenticated, the valid lifetime of the corresponding address should be set * to the Valid Lifetime in the received option. * * Currently, authentication is not supported. So check the lifetime of the existing address. */ r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, &t); if (r < 0) return r; if (existing->lifetime_valid_usec <= usec_add(t, 2 * USEC_PER_HOUR)) { address->lifetime_valid_usec = existing->lifetime_valid_usec; return 0; } /* 3. Otherwise, reset the valid lifetime of the corresponding address to 2 hours. */ address->lifetime_valid_usec = usec_add(t, 2 * USEC_PER_HOUR); return 0; } static int ndisc_router_process_autonomous_prefix(Link *link, sd_ndisc_router *rt) { usec_t lifetime_valid_usec, lifetime_preferred_usec; struct in6_addr prefix, router; uint8_t prefixlen; int r; assert(link); assert(link->network); assert(rt); if (!link->network->ndisc_use_autonomous_prefix) return 0; r = sd_ndisc_router_get_sender_address(rt, &router); if (r < 0) return log_link_warning_errno(link, r, "Failed to get router address: %m"); r = sd_ndisc_router_prefix_get_address(rt, &prefix); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix address: %m"); r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix length: %m"); /* ndisc_generate_addresses() below requires the prefix length <= 64. */ if (prefixlen > 64) { log_link_debug(link, "Prefix is longer than 64, ignoring autonomous prefix %s.", IN6_ADDR_PREFIX_TO_STRING(&prefix, prefixlen)); return 0; } r = sd_ndisc_router_prefix_get_valid_lifetime(rt, &lifetime_valid_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix valid lifetime: %m"); r = sd_ndisc_router_prefix_get_preferred_lifetime(rt, &lifetime_preferred_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix preferred lifetime: %m"); /* RFC 4862 section 5.5.3 (c) * If the preferred lifetime is greater than the valid lifetime, silently ignore the Prefix * Information option. */ if (lifetime_preferred_usec > lifetime_valid_usec) return 0; _cleanup_hashmap_free_ Hashmap *tokens_by_address = NULL; r = ndisc_generate_addresses(link, &prefix, prefixlen, &tokens_by_address); if (r < 0) return log_link_warning_errno(link, r, "Failed to generate SLAAC addresses: %m"); IPv6Token *token; struct in6_addr *a; HASHMAP_FOREACH_KEY(token, a, tokens_by_address) { _cleanup_(address_unrefp) Address *address = NULL; r = address_new(&address); if (r < 0) return log_oom(); address->provider.in6 = router; address->family = AF_INET6; address->in_addr.in6 = *a; address->prefixlen = prefixlen; address->flags = IFA_F_NOPREFIXROUTE|IFA_F_MANAGETEMPADDR; address->token = ipv6_token_ref(token); r = ndisc_address_set_lifetime(address, link, rt); if (r < 0) return log_link_warning_errno(link, r, "Failed to set lifetime of SLAAC address: %m"); assert(address->lifetime_preferred_usec <= address->lifetime_valid_usec); r = ndisc_request_address(address, link); if (r < 0) return log_link_warning_errno(link, r, "Could not request SLAAC address: %m"); } return 0; } static int ndisc_router_process_onlink_prefix(Link *link, sd_ndisc_router *rt) { _cleanup_(route_unrefp) Route *route = NULL; uint8_t prefixlen, preference; usec_t lifetime_usec; struct in6_addr prefix; int r; assert(link); assert(link->network); assert(rt); if (!link->network->ndisc_use_onlink_prefix) return 0; r = sd_ndisc_router_prefix_get_valid_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix lifetime: %m"); r = sd_ndisc_router_prefix_get_address(rt, &prefix); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix address: %m"); r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix length: %m"); /* Prefix Information option does not have preference, hence we use the 'main' preference here */ r = sd_ndisc_router_get_preference(rt, &preference); if (r < 0) return log_link_warning_errno(link, r, "Failed to get router preference from RA: %m"); r = route_new(&route); if (r < 0) return log_oom(); route->family = AF_INET6; route->dst.in6 = prefix; route->dst_prefixlen = prefixlen; route->pref = preference; route->lifetime_usec = lifetime_usec; /* RFC 4861 section 6.3.4: * - If the prefix is not already present in the Prefix List, and the Prefix Information option's * Valid Lifetime field is non-zero, create a new entry for the prefix and initialize its * invalidation timer to the Valid Lifetime value in the Prefix Information option. * * - If the prefix is already present in the host's Prefix List as the result of a previously * received advertisement, reset its invalidation timer to the Valid Lifetime value in the Prefix * Information option. If the new Lifetime value is zero, time-out the prefix immediately. */ if (lifetime_usec == 0) { r = ndisc_remove_route(route, link); if (r < 0) return log_link_warning_errno(link, r, "Failed to remove prefix route: %m"); } else { r = ndisc_request_router_route(route, link, rt); if (r < 0) return log_link_warning_errno(link, r, "Failed to request prefix route: %m"); } return 0; } static int ndisc_router_process_prefix(Link *link, sd_ndisc_router *rt) { uint8_t flags, prefixlen; struct in6_addr a; int r; assert(link); assert(link->network); assert(rt); r = sd_ndisc_router_prefix_get_address(rt, &a); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix address: %m"); /* RFC 4861 Section 4.6.2: * A router SHOULD NOT send a prefix option for the link-local prefix and a host SHOULD ignore such * a prefix option. */ if (in6_addr_is_link_local(&a)) { log_link_debug(link, "Received link-local prefix, ignoring prefix."); return 0; } r = sd_ndisc_router_prefix_get_prefixlen(rt, &prefixlen); if (r < 0) return log_link_warning_errno(link, r, "Failed to get prefix length: %m"); if (in6_prefix_is_filtered(&a, prefixlen, link->network->ndisc_allow_listed_prefix, link->network->ndisc_deny_listed_prefix)) { if (DEBUG_LOGGING) log_link_debug(link, "Prefix '%s' is %s, ignoring", !set_isempty(link->network->ndisc_allow_listed_prefix) ? "not in allow list" : "in deny list", IN6_ADDR_PREFIX_TO_STRING(&a, prefixlen)); return 0; } r = sd_ndisc_router_prefix_get_flags(rt, &flags); if (r < 0) return log_link_warning_errno(link, r, "Failed to get RA prefix flags: %m"); if (FLAGS_SET(flags, ND_OPT_PI_FLAG_ONLINK)) { r = ndisc_router_process_onlink_prefix(link, rt); if (r < 0) return r; } if (FLAGS_SET(flags, ND_OPT_PI_FLAG_AUTO)) { r = ndisc_router_process_autonomous_prefix(link, rt); if (r < 0) return r; } return 0; } static int ndisc_router_process_route(Link *link, sd_ndisc_router *rt) { _cleanup_(route_unrefp) Route *route = NULL; uint8_t preference, prefixlen; struct in6_addr gateway, dst; usec_t lifetime_usec; int r; assert(link); if (!link->network->ndisc_use_route_prefix) return 0; r = sd_ndisc_router_route_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get route lifetime from RA: %m"); r = sd_ndisc_router_route_get_address(rt, &dst); if (r < 0) return log_link_warning_errno(link, r, "Failed to get route destination address: %m"); r = sd_ndisc_router_route_get_prefixlen(rt, &prefixlen); if (r < 0) return log_link_warning_errno(link, r, "Failed to get route prefix length: %m"); if (in6_prefix_is_filtered(&dst, prefixlen, link->network->ndisc_allow_listed_route_prefix, link->network->ndisc_deny_listed_route_prefix)) { if (DEBUG_LOGGING) log_link_debug(link, "Route prefix %s is %s, ignoring", !set_isempty(link->network->ndisc_allow_listed_route_prefix) ? "not in allow list" : "in deny list", IN6_ADDR_PREFIX_TO_STRING(&dst, prefixlen)); return 0; } r = sd_ndisc_router_get_sender_address(rt, &gateway); if (r < 0) return log_link_warning_errno(link, r, "Failed to get gateway address from RA: %m"); if (link_get_ipv6_address(link, &gateway, 0, NULL) >= 0) { if (DEBUG_LOGGING) log_link_debug(link, "Advertised route gateway %s is local to the link, ignoring route", IN6_ADDR_TO_STRING(&gateway)); return 0; } r = sd_ndisc_router_route_get_preference(rt, &preference); if (r == -EOPNOTSUPP) { log_link_debug_errno(link, r, "Received route prefix with unsupported preference, ignoring: %m"); return 0; } if (r < 0) return log_link_warning_errno(link, r, "Failed to get router preference from RA: %m"); r = route_new(&route); if (r < 0) return log_oom(); route->family = AF_INET6; route->pref = preference; route->nexthop.gw.in6 = gateway; route->nexthop.family = AF_INET6; route->dst.in6 = dst; route->dst_prefixlen = prefixlen; route->lifetime_usec = lifetime_usec; if (lifetime_usec != 0) { r = ndisc_request_router_route(route, link, rt); if (r < 0) return log_link_warning_errno(link, r, "Could not request additional route: %m"); } else { r = ndisc_remove_route(route, link); if (r < 0) return log_link_warning_errno(link, r, "Could not remove additional route with zero lifetime: %m"); } return 0; } static void ndisc_rdnss_hash_func(const NDiscRDNSS *x, struct siphash *state) { siphash24_compress_typesafe(x->address, state); } static int ndisc_rdnss_compare_func(const NDiscRDNSS *a, const NDiscRDNSS *b) { return memcmp(&a->address, &b->address, sizeof(a->address)); } DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( ndisc_rdnss_hash_ops, NDiscRDNSS, ndisc_rdnss_hash_func, ndisc_rdnss_compare_func, free); static int ndisc_router_process_rdnss(Link *link, sd_ndisc_router *rt) { usec_t lifetime_usec; const struct in6_addr *a; struct in6_addr router; bool updated = false, logged_about_too_many = false; int n, r; assert(link); assert(link->network); assert(rt); if (!link_get_use_dns(link, NETWORK_CONFIG_SOURCE_NDISC)) return 0; r = sd_ndisc_router_get_sender_address(rt, &router); if (r < 0) return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); r = sd_ndisc_router_rdnss_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get RDNSS lifetime: %m"); n = sd_ndisc_router_rdnss_get_addresses(rt, &a); if (n < 0) return log_link_warning_errno(link, n, "Failed to get RDNSS addresses: %m"); for (int j = 0; j < n; j++) { _cleanup_free_ NDiscRDNSS *x = NULL; NDiscRDNSS *rdnss, d = { .address = a[j], }; if (lifetime_usec == 0) { /* The entry is outdated. */ free(set_remove(link->ndisc_rdnss, &d)); updated = true; continue; } rdnss = set_get(link->ndisc_rdnss, &d); if (rdnss) { rdnss->router = router; rdnss->lifetime_usec = lifetime_usec; continue; } if (set_size(link->ndisc_rdnss) >= NDISC_RDNSS_MAX) { if (!logged_about_too_many) log_link_warning(link, "Too many RDNSS records per link. Only first %u records will be used.", NDISC_RDNSS_MAX); logged_about_too_many = true; continue; } x = new(NDiscRDNSS, 1); if (!x) return log_oom(); *x = (NDiscRDNSS) { .address = a[j], .router = router, .lifetime_usec = lifetime_usec, }; r = set_ensure_consume(&link->ndisc_rdnss, &ndisc_rdnss_hash_ops, TAKE_PTR(x)); if (r < 0) return log_oom(); assert(r > 0); updated = true; } if (updated) link_dirty(link); return 0; } static void ndisc_dnssl_hash_func(const NDiscDNSSL *x, struct siphash *state) { siphash24_compress_string(NDISC_DNSSL_DOMAIN(x), state); } static int ndisc_dnssl_compare_func(const NDiscDNSSL *a, const NDiscDNSSL *b) { return strcmp(NDISC_DNSSL_DOMAIN(a), NDISC_DNSSL_DOMAIN(b)); } DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( ndisc_dnssl_hash_ops, NDiscDNSSL, ndisc_dnssl_hash_func, ndisc_dnssl_compare_func, free); static int ndisc_router_process_dnssl(Link *link, sd_ndisc_router *rt) { char **l; usec_t lifetime_usec; struct in6_addr router; bool updated = false, logged_about_too_many = false; int r; assert(link); assert(link->network); assert(rt); if (link_get_use_domains(link, NETWORK_CONFIG_SOURCE_NDISC) <= 0) return 0; r = sd_ndisc_router_get_sender_address(rt, &router); if (r < 0) return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); r = sd_ndisc_router_dnssl_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get DNSSL lifetime: %m"); r = sd_ndisc_router_dnssl_get_domains(rt, &l); if (r < 0) return log_link_warning_errno(link, r, "Failed to get DNSSL addresses: %m"); STRV_FOREACH(j, l) { _cleanup_free_ NDiscDNSSL *s = NULL; NDiscDNSSL *dnssl; s = malloc0(ALIGN(sizeof(NDiscDNSSL)) + strlen(*j) + 1); if (!s) return log_oom(); strcpy(NDISC_DNSSL_DOMAIN(s), *j); if (lifetime_usec == 0) { /* The entry is outdated. */ free(set_remove(link->ndisc_dnssl, s)); updated = true; continue; } dnssl = set_get(link->ndisc_dnssl, s); if (dnssl) { dnssl->router = router; dnssl->lifetime_usec = lifetime_usec; continue; } if (set_size(link->ndisc_dnssl) >= NDISC_DNSSL_MAX) { if (!logged_about_too_many) log_link_warning(link, "Too many DNSSL records per link. Only first %u records will be used.", NDISC_DNSSL_MAX); logged_about_too_many = true; continue; } s->router = router; s->lifetime_usec = lifetime_usec; r = set_ensure_consume(&link->ndisc_dnssl, &ndisc_dnssl_hash_ops, TAKE_PTR(s)); if (r < 0) return log_oom(); assert(r > 0); updated = true; } if (updated) link_dirty(link); return 0; } static NDiscCaptivePortal* ndisc_captive_portal_free(NDiscCaptivePortal *x) { if (!x) return NULL; free(x->captive_portal); return mfree(x); } DEFINE_TRIVIAL_CLEANUP_FUNC(NDiscCaptivePortal*, ndisc_captive_portal_free); static void ndisc_captive_portal_hash_func(const NDiscCaptivePortal *x, struct siphash *state) { assert(x); siphash24_compress_string(x->captive_portal, state); } static int ndisc_captive_portal_compare_func(const NDiscCaptivePortal *a, const NDiscCaptivePortal *b) { assert(a); assert(b); return strcmp_ptr(a->captive_portal, b->captive_portal); } DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( ndisc_captive_portal_hash_ops, NDiscCaptivePortal, ndisc_captive_portal_hash_func, ndisc_captive_portal_compare_func, ndisc_captive_portal_free); static int ndisc_router_process_captive_portal(Link *link, sd_ndisc_router *rt) { _cleanup_(ndisc_captive_portal_freep) NDiscCaptivePortal *new_entry = NULL; _cleanup_free_ char *captive_portal = NULL; const char *uri; usec_t lifetime_usec; NDiscCaptivePortal *exist; struct in6_addr router; int r; assert(link); assert(link->network); assert(rt); if (!link->network->ndisc_use_captive_portal) return 0; r = sd_ndisc_router_get_sender_address(rt, &router); if (r < 0) return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); /* RFC 4861 section 4.2. states that the lifetime in the message header should be used only for the * default gateway, but the captive portal option does not have a lifetime field, hence, we use the * main lifetime for the portal. */ r = sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get lifetime of RA message: %m"); r = sd_ndisc_router_get_captive_portal(rt, &uri); if (r < 0) return log_link_warning_errno(link, r, "Failed to get captive portal from RA: %m"); captive_portal = strdup(uri); if (!captive_portal) return log_oom(); if (lifetime_usec == 0) { /* Drop the portal with zero lifetime. */ ndisc_captive_portal_free(set_remove(link->ndisc_captive_portals, &(const NDiscCaptivePortal) { .captive_portal = captive_portal, })); return 0; } exist = set_get(link->ndisc_captive_portals, &(const NDiscCaptivePortal) { .captive_portal = captive_portal, }); if (exist) { /* update existing entry */ exist->router = router; exist->lifetime_usec = lifetime_usec; return 1; } if (set_size(link->ndisc_captive_portals) >= NDISC_CAPTIVE_PORTAL_MAX) { NDiscCaptivePortal *c, *target = NULL; /* Find the portal who has the minimal lifetime and drop it to store new one. */ SET_FOREACH(c, link->ndisc_captive_portals) if (!target || c->lifetime_usec < target->lifetime_usec) target = c; assert(target); assert(set_remove(link->ndisc_captive_portals, target) == target); ndisc_captive_portal_free(target); } new_entry = new(NDiscCaptivePortal, 1); if (!new_entry) return log_oom(); *new_entry = (NDiscCaptivePortal) { .router = router, .lifetime_usec = lifetime_usec, .captive_portal = TAKE_PTR(captive_portal), }; r = set_ensure_put(&link->ndisc_captive_portals, &ndisc_captive_portal_hash_ops, new_entry); if (r < 0) return log_oom(); assert(r > 0); TAKE_PTR(new_entry); link_dirty(link); return 1; } static void ndisc_pref64_hash_func(const NDiscPREF64 *x, struct siphash *state) { assert(x); siphash24_compress_typesafe(x->prefix_len, state); siphash24_compress_typesafe(x->prefix, state); } static int ndisc_pref64_compare_func(const NDiscPREF64 *a, const NDiscPREF64 *b) { int r; assert(a); assert(b); r = CMP(a->prefix_len, b->prefix_len); if (r != 0) return r; return memcmp(&a->prefix, &b->prefix, sizeof(a->prefix)); } DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( ndisc_pref64_hash_ops, NDiscPREF64, ndisc_pref64_hash_func, ndisc_pref64_compare_func, mfree); static int ndisc_router_process_pref64(Link *link, sd_ndisc_router *rt) { _cleanup_free_ NDiscPREF64 *new_entry = NULL; usec_t lifetime_usec; struct in6_addr a, router; uint8_t prefix_len; NDiscPREF64 *exist; int r; assert(link); assert(link->network); assert(rt); if (!link->network->ndisc_use_pref64) return 0; r = sd_ndisc_router_get_sender_address(rt, &router); if (r < 0) return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); r = sd_ndisc_router_prefix64_get_prefix(rt, &a); if (r < 0) return log_link_warning_errno(link, r, "Failed to get pref64 prefix: %m"); r = sd_ndisc_router_prefix64_get_prefixlen(rt, &prefix_len); if (r < 0) return log_link_warning_errno(link, r, "Failed to get pref64 prefix length: %m"); r = sd_ndisc_router_prefix64_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); if (r < 0) return log_link_warning_errno(link, r, "Failed to get pref64 prefix lifetime: %m"); if (lifetime_usec == 0) { free(set_remove(link->ndisc_pref64, &(NDiscPREF64) { .prefix = a, .prefix_len = prefix_len })); return 0; } exist = set_get(link->ndisc_pref64, &(NDiscPREF64) { .prefix = a, .prefix_len = prefix_len }); if (exist) { /* update existing entry */ exist->router = router; exist->lifetime_usec = lifetime_usec; return 0; } if (set_size(link->ndisc_pref64) >= NDISC_PREF64_MAX) { log_link_debug(link, "Too many PREF64 records received. Only first %u records will be used.", NDISC_PREF64_MAX); return 0; } new_entry = new(NDiscPREF64, 1); if (!new_entry) return log_oom(); *new_entry = (NDiscPREF64) { .router = router, .lifetime_usec = lifetime_usec, .prefix = a, .prefix_len = prefix_len, }; r = set_ensure_put(&link->ndisc_pref64, &ndisc_pref64_hash_ops, new_entry); if (r < 0) return log_oom(); assert(r > 0); TAKE_PTR(new_entry); return 0; } static int ndisc_router_process_options(Link *link, sd_ndisc_router *rt) { size_t n_captive_portal = 0; int r; assert(link); assert(link->network); assert(rt); for (r = sd_ndisc_router_option_rewind(rt); ; r = sd_ndisc_router_option_next(rt)) { uint8_t type; if (r < 0) return log_link_warning_errno(link, r, "Failed to iterate through options: %m"); if (r == 0) /* EOF */ return 0; r = sd_ndisc_router_option_get_type(rt, &type); if (r < 0) return log_link_warning_errno(link, r, "Failed to get RA option type: %m"); switch (type) { case SD_NDISC_OPTION_PREFIX_INFORMATION: r = ndisc_router_process_prefix(link, rt); break; case SD_NDISC_OPTION_ROUTE_INFORMATION: r = ndisc_router_process_route(link, rt); break; case SD_NDISC_OPTION_RDNSS: r = ndisc_router_process_rdnss(link, rt); break; case SD_NDISC_OPTION_DNSSL: r = ndisc_router_process_dnssl(link, rt); break; case SD_NDISC_OPTION_CAPTIVE_PORTAL: if (n_captive_portal > 0) { if (n_captive_portal == 1) log_link_notice(link, "Received RA with multiple captive portals, only using the first one."); n_captive_portal++; continue; } r = ndisc_router_process_captive_portal(link, rt); if (r > 0) n_captive_portal++; break; case SD_NDISC_OPTION_PREF64: r = ndisc_router_process_pref64(link, rt); break; } if (r < 0 && r != -EBADMSG) return r; } } static int ndisc_drop_outdated(Link *link, const struct in6_addr *router, usec_t timestamp_usec) { bool updated = false; NDiscDNSSL *dnssl; NDiscRDNSS *rdnss; NDiscCaptivePortal *cp; NDiscPREF64 *p64; Address *address; Route *route; int r, ret = 0; assert(link); assert(link->manager); /* If an address or friends is already assigned, but not valid anymore, then refuse to update it, * and let's immediately remove it. * See RFC4862, section 5.5.3.e. But the following logic is deviated from RFC4862 by honoring all * valid lifetimes to improve the reaction of SLAAC to renumbering events. * See draft-ietf-6man-slaac-renum-02, section 4.2. */ r = ndisc_drop_routers(link, router, timestamp_usec); if (r < 0) RET_GATHER(ret, log_link_warning_errno(link, r, "Failed to drop outdated default router, ignoring: %m")); SET_FOREACH(route, link->manager->routes) { if (route->source != NETWORK_CONFIG_SOURCE_NDISC) continue; if (route->nexthop.ifindex != link->ifindex) continue; if (route->protocol == RTPROT_REDIRECT) continue; /* redirect route will be dropped by ndisc_drop_redirect(). */ if (route->lifetime_usec > timestamp_usec) continue; /* the route is still valid */ if (router && !in6_addr_equal(&route->provider.in6, router)) continue; r = route_remove_and_cancel(route, link->manager); if (r < 0) RET_GATHER(ret, log_link_warning_errno(link, r, "Failed to remove outdated SLAAC route, ignoring: %m")); } SET_FOREACH(address, link->addresses) { if (address->source != NETWORK_CONFIG_SOURCE_NDISC) continue; if (address->lifetime_valid_usec > timestamp_usec) continue; /* the address is still valid */ if (router && !in6_addr_equal(&address->provider.in6, router)) continue; r = address_remove_and_cancel(address, link); if (r < 0) RET_GATHER(ret, log_link_warning_errno(link, r, "Failed to remove outdated SLAAC address, ignoring: %m")); } SET_FOREACH(rdnss, link->ndisc_rdnss) { if (rdnss->lifetime_usec > timestamp_usec) continue; /* the DNS server is still valid */ if (router && !in6_addr_equal(&rdnss->router, router)) continue; free(set_remove(link->ndisc_rdnss, rdnss)); updated = true; } SET_FOREACH(dnssl, link->ndisc_dnssl) { if (dnssl->lifetime_usec > timestamp_usec) continue; /* the DNS domain is still valid */ if (router && !in6_addr_equal(&dnssl->router, router)) continue; free(set_remove(link->ndisc_dnssl, dnssl)); updated = true; } SET_FOREACH(cp, link->ndisc_captive_portals) { if (cp->lifetime_usec > timestamp_usec) continue; /* the captive portal is still valid */ if (router && !in6_addr_equal(&cp->router, router)) continue; ndisc_captive_portal_free(set_remove(link->ndisc_captive_portals, cp)); updated = true; } SET_FOREACH(p64, link->ndisc_pref64) { if (p64->lifetime_usec > timestamp_usec) continue; /* the pref64 prefix is still valid */ if (router && !in6_addr_equal(&p64->router, router)) continue; free(set_remove(link->ndisc_pref64, p64)); /* The pref64 prefix is not exported through the state file, hence it is not necessary to set * the 'updated' flag. */ } if (updated) link_dirty(link); return ret; } static int ndisc_setup_expire(Link *link); static int ndisc_expire_handler(sd_event_source *s, uint64_t usec, void *userdata) { Link *link = ASSERT_PTR(userdata); usec_t now_usec; assert(link->manager); assert_se(sd_event_now(link->manager->event, CLOCK_BOOTTIME, &now_usec) >= 0); (void) ndisc_drop_outdated(link, /* router = */ NULL, now_usec); (void) ndisc_setup_expire(link); return 0; } static int ndisc_setup_expire(Link *link) { usec_t lifetime_usec = USEC_INFINITY; NDiscCaptivePortal *cp; NDiscDNSSL *dnssl; NDiscRDNSS *rdnss; NDiscPREF64 *p64; Address *address; Route *route; int r; assert(link); assert(link->manager); sd_ndisc_router *rt; HASHMAP_FOREACH(rt, link->ndisc_routers_by_sender) { usec_t t; if (sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &t) < 0) continue; lifetime_usec = MIN(lifetime_usec, t); } SET_FOREACH(route, link->manager->routes) { if (route->source != NETWORK_CONFIG_SOURCE_NDISC) continue; if (route->nexthop.ifindex != link->ifindex) continue; if (!route_exists(route)) continue; lifetime_usec = MIN(lifetime_usec, route->lifetime_usec); } SET_FOREACH(address, link->addresses) { if (address->source != NETWORK_CONFIG_SOURCE_NDISC) continue; if (!address_exists(address)) continue; lifetime_usec = MIN(lifetime_usec, address->lifetime_valid_usec); } SET_FOREACH(rdnss, link->ndisc_rdnss) lifetime_usec = MIN(lifetime_usec, rdnss->lifetime_usec); SET_FOREACH(dnssl, link->ndisc_dnssl) lifetime_usec = MIN(lifetime_usec, dnssl->lifetime_usec); SET_FOREACH(cp, link->ndisc_captive_portals) lifetime_usec = MIN(lifetime_usec, cp->lifetime_usec); SET_FOREACH(p64, link->ndisc_pref64) lifetime_usec = MIN(lifetime_usec, p64->lifetime_usec); if (lifetime_usec == USEC_INFINITY) return 0; r = event_reset_time(link->manager->event, &link->ndisc_expire, CLOCK_BOOTTIME, lifetime_usec, 0, ndisc_expire_handler, link, 0, "ndisc-expiration", true); if (r < 0) return log_link_warning_errno(link, r, "Failed to update expiration timer for ndisc: %m"); return 0; } static int ndisc_start_dhcp6_client(Link *link, sd_ndisc_router *rt) { int r; assert(link); assert(link->network); /* Do not start DHCPv6 client if the router lifetime is zero, as the message sent as a signal of * that the router is e.g. shutting down, revoked, etc,. */ r = sd_ndisc_router_get_lifetime(rt, NULL); if (r <= 0) return r; switch (link->network->ndisc_start_dhcp6_client) { case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO: return 0; case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES: { uint64_t flags; r = sd_ndisc_router_get_flags(rt, &flags); if (r < 0) return log_link_warning_errno(link, r, "Failed to get RA flags: %m"); if ((flags & (ND_RA_FLAG_MANAGED | ND_RA_FLAG_OTHER)) == 0) return 0; /* (re)start DHCPv6 client in stateful or stateless mode according to RA flags. * Note, if both "managed" and "other configuration" bits are set, then ignore * "other configuration" bit. See RFC 4861. */ r = dhcp6_start_on_ra(link, !(flags & ND_RA_FLAG_MANAGED)); break; } case IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS: /* When IPv6AcceptRA.DHCPv6Client=always, start dhcp6 client in solicit mode * even if the router flags have neither M nor O flags. */ r = dhcp6_start_on_ra(link, /* information_request = */ false); break; default: assert_not_reached(); } if (r < 0) return log_link_warning_errno(link, r, "Could not acquire DHCPv6 lease on NDisc request: %m"); log_link_debug(link, "Acquiring DHCPv6 lease on NDisc request"); return 0; } static int ndisc_router_handler(Link *link, sd_ndisc_router *rt) { struct in6_addr router; usec_t timestamp_usec; int r; assert(link); assert(link->network); assert(link->manager); assert(rt); r = sd_ndisc_router_get_sender_address(rt, &router); if (r == -ENODATA) { log_link_debug(link, "Received RA without router address, ignoring."); return 0; } if (r < 0) return log_link_warning_errno(link, r, "Failed to get router address from RA: %m"); if (in6_prefix_is_filtered(&router, 128, link->network->ndisc_allow_listed_router, link->network->ndisc_deny_listed_router)) { if (DEBUG_LOGGING) { if (!set_isempty(link->network->ndisc_allow_listed_router)) log_link_debug(link, "Router %s is not in allow list, ignoring.", IN6_ADDR_TO_STRING(&router)); else log_link_debug(link, "Router %s is in deny list, ignoring.", IN6_ADDR_TO_STRING(&router)); } return 0; } r = sd_ndisc_router_get_timestamp(rt, CLOCK_BOOTTIME, ×tamp_usec); if (r == -ENODATA) { log_link_debug(link, "Received RA without timestamp, ignoring."); return 0; } if (r < 0) return r; r = ndisc_drop_outdated(link, /* router = */ NULL, timestamp_usec); if (r < 0) return r; r = ndisc_remember_router(link, rt); if (r < 0) return r; r = ndisc_start_dhcp6_client(link, rt); if (r < 0) return r; r = ndisc_router_process_default(link, rt); if (r < 0) return r; r = ndisc_router_process_reachable_time(link, rt); if (r < 0) return r; r = ndisc_router_process_retransmission_time(link, rt); if (r < 0) return r; r = ndisc_router_process_hop_limit(link, rt); if (r < 0) return r; r = ndisc_router_process_mtu(link, rt); if (r < 0) return r; r = ndisc_router_process_options(link, rt); if (r < 0) return r; r = ndisc_setup_expire(link); if (r < 0) return r; if (sd_ndisc_router_get_lifetime(rt, NULL) <= 0) (void) ndisc_drop_redirect(link, &router); if (link->ndisc_messages == 0) link->ndisc_configured = true; else log_link_debug(link, "Setting SLAAC addresses and router."); if (!link->ndisc_configured) link_set_state(link, LINK_STATE_CONFIGURING); link_check_ready(link); return 0; } static int ndisc_neighbor_handle_non_router_message(Link *link, sd_ndisc_neighbor *na) { struct in6_addr address; int r; assert(link); assert(na); /* Received Neighbor Advertisement message without Router flag. The node might have been a router, * and now it is not. Let's drop all configurations based on RAs sent from the node. */ r = sd_ndisc_neighbor_get_target_address(na, &address); if (r == -ENODATA) return 0; if (r < 0) return r; (void) ndisc_drop_outdated(link, /* router = */ &address, /* timestamp_usec = */ USEC_INFINITY); (void) ndisc_drop_redirect(link, &address); return 0; } static int ndisc_neighbor_handle_router_message(Link *link, sd_ndisc_neighbor *na) { struct in6_addr current_address, original_address; int r; assert(link); assert(link->manager); assert(na); /* Received Neighbor Advertisement message with Router flag. If the router address is changed, update * the provider field of configurations. */ r = sd_ndisc_neighbor_get_sender_address(na, ¤t_address); if (r == -ENODATA) return 0; if (r < 0) return r; r = sd_ndisc_neighbor_get_target_address(na, &original_address); if (r == -ENODATA) return 0; if (r < 0) return r; if (in6_addr_equal(¤t_address, &original_address)) return 0; /* the router address is not changed */ r = ndisc_update_router_address(link, &original_address, ¤t_address); if (r < 0) return r; r = ndisc_update_redirect_sender(link, &original_address, ¤t_address); if (r < 0) return r; Route *route; SET_FOREACH(route, link->manager->routes) { if (route->source != NETWORK_CONFIG_SOURCE_NDISC) continue; if (route->nexthop.ifindex != link->ifindex) continue; if (!in6_addr_equal(&route->provider.in6, &original_address)) continue; route->provider.in6 = current_address; } Address *address; SET_FOREACH(address, link->addresses) { if (address->source != NETWORK_CONFIG_SOURCE_NDISC) continue; if (!in6_addr_equal(&address->provider.in6, &original_address)) continue; address->provider.in6 = current_address; } NDiscRDNSS *rdnss; SET_FOREACH(rdnss, link->ndisc_rdnss) { if (!in6_addr_equal(&rdnss->router, &original_address)) continue; rdnss->router = current_address; } NDiscDNSSL *dnssl; SET_FOREACH(dnssl, link->ndisc_dnssl) { if (!in6_addr_equal(&dnssl->router, &original_address)) continue; dnssl->router = current_address; } NDiscCaptivePortal *cp; SET_FOREACH(cp, link->ndisc_captive_portals) { if (!in6_addr_equal(&cp->router, &original_address)) continue; cp->router = current_address; } NDiscPREF64 *p64; SET_FOREACH(p64, link->ndisc_pref64) { if (!in6_addr_equal(&p64->router, &original_address)) continue; p64->router = current_address; } return 0; } static int ndisc_neighbor_handler(Link *link, sd_ndisc_neighbor *na) { int r; assert(link); assert(na); r = sd_ndisc_neighbor_is_router(na); if (r < 0) return r; if (r == 0) r = ndisc_neighbor_handle_non_router_message(link, na); else r = ndisc_neighbor_handle_router_message(link, na); if (r < 0) return r; return 0; } static void ndisc_handler(sd_ndisc *nd, sd_ndisc_event_t event, void *message, void *userdata) { Link *link = ASSERT_PTR(userdata); int r; if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) return; switch (event) { case SD_NDISC_EVENT_ROUTER: r = ndisc_router_handler(link, ASSERT_PTR(message)); if (r < 0 && r != -EBADMSG) { link_enter_failed(link); return; } break; case SD_NDISC_EVENT_NEIGHBOR: r = ndisc_neighbor_handler(link, ASSERT_PTR(message)); if (r < 0 && r != -EBADMSG) { link_enter_failed(link); return; } break; case SD_NDISC_EVENT_REDIRECT: r = ndisc_redirect_handler(link, ASSERT_PTR(message)); if (r < 0 && r != -EBADMSG) { log_link_warning_errno(link, r, "Failed to process Redirect message: %m"); link_enter_failed(link); return; } break; case SD_NDISC_EVENT_TIMEOUT: log_link_debug(link, "NDisc handler get timeout event"); if (link->ndisc_messages == 0) { link->ndisc_configured = true; link_check_ready(link); } break; default: log_link_debug(link, "Received unsupported NDisc event, ignoring."); } } static int ndisc_configure(Link *link) { int r; assert(link); if (!link_ndisc_enabled(link)) return 0; if (link->ndisc) return -EBUSY; /* Already configured. */ r = sd_ndisc_new(&link->ndisc); if (r < 0) return r; r = sd_ndisc_attach_event(link->ndisc, link->manager->event, 0); if (r < 0) return r; if (link->hw_addr.length == ETH_ALEN) { r = sd_ndisc_set_mac(link->ndisc, &link->hw_addr.ether); if (r < 0) return r; } r = sd_ndisc_set_ifindex(link->ndisc, link->ifindex); if (r < 0) return r; r = sd_ndisc_set_callback(link->ndisc, ndisc_handler, link); if (r < 0) return r; return 0; } int ndisc_start(Link *link) { int r; assert(link); if (!link->ndisc || !link->dhcp6_client) return 0; if (!link_has_carrier(link)) return 0; if (in6_addr_is_null(&link->ipv6ll_address)) return 0; r = sd_ndisc_set_link_local_address(link->ndisc, &link->ipv6ll_address); if (r < 0) return r; log_link_debug(link, "Discovering IPv6 routers"); r = sd_ndisc_start(link->ndisc); if (r < 0) return r; return 1; } static int ndisc_process_request(Request *req, Link *link, void *userdata) { int r; assert(link); if (!link_is_ready_to_configure(link, /* allow_unmanaged = */ false)) return 0; r = ndisc_configure(link); if (r < 0) return log_link_warning_errno(link, r, "Failed to configure IPv6 Router Discovery: %m"); r = ndisc_start(link); if (r < 0) return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m"); log_link_debug(link, "IPv6 Router Discovery is configured%s.", r > 0 ? " and started" : ""); return 1; } int link_request_ndisc(Link *link) { int r; assert(link); if (!link_ndisc_enabled(link)) return 0; if (link->ndisc) return 0; r = link_queue_request(link, REQUEST_TYPE_NDISC, ndisc_process_request, NULL); if (r < 0) return log_link_warning_errno(link, r, "Failed to request configuring of the IPv6 Router Discovery: %m"); log_link_debug(link, "Requested configuring of the IPv6 Router Discovery."); return 0; } int ndisc_stop(Link *link) { assert(link); link->ndisc_expire = sd_event_source_disable_unref(link->ndisc_expire); return sd_ndisc_stop(link->ndisc); } void ndisc_flush(Link *link) { assert(link); /* Remove all addresses, routes, RDNSS, DNSSL, and Captive Portal entries, without exception. */ (void) ndisc_drop_outdated(link, /* router = */ NULL, /* timestamp_usec = */ USEC_INFINITY); (void) ndisc_drop_redirect(link, /* router = */ NULL); link->ndisc_routers_by_sender = hashmap_free(link->ndisc_routers_by_sender); link->ndisc_rdnss = set_free(link->ndisc_rdnss); link->ndisc_dnssl = set_free(link->ndisc_dnssl); link->ndisc_captive_portals = set_free(link->ndisc_captive_portals); link->ndisc_pref64 = set_free(link->ndisc_pref64); link->ndisc_redirects = set_free(link->ndisc_redirects); link->ndisc_mtu = 0; } static const char* const ndisc_start_dhcp6_client_table[_IPV6_ACCEPT_RA_START_DHCP6_CLIENT_MAX] = { [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_NO] = "no", [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_ALWAYS] = "always", [IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES] = "yes", }; DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(ndisc_start_dhcp6_client, IPv6AcceptRAStartDHCP6Client, IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES); DEFINE_CONFIG_PARSE_ENUM(config_parse_ndisc_start_dhcp6_client, ndisc_start_dhcp6_client, IPv6AcceptRAStartDHCP6Client, "Failed to parse DHCPv6Client= setting");