/* SPDX-License-Identifier: LGPL-2.1-or-later */ /*** Copyright © 2014 Intel Corporation. All rights reserved. ***/ #include #include #include "sd-ndisc.h" #include "alloc-util.h" #include "ether-addr-util.h" #include "event-util.h" #include "fd-util.h" #include "icmp6-util.h" #include "in-addr-util.h" #include "memory-util.h" #include "ndisc-internal.h" #include "ndisc-neighbor-internal.h" #include "ndisc-redirect-internal.h" #include "ndisc-router-internal.h" #include "network-common.h" #include "random-util.h" #include "socket-util.h" #include "string-table.h" #include "string-util.h" #define NDISC_TIMEOUT_NO_RA_USEC (NDISC_ROUTER_SOLICITATION_INTERVAL * NDISC_MAX_ROUTER_SOLICITATIONS) static const char * const ndisc_event_table[_SD_NDISC_EVENT_MAX] = { [SD_NDISC_EVENT_TIMEOUT] = "timeout", [SD_NDISC_EVENT_ROUTER] = "router", [SD_NDISC_EVENT_NEIGHBOR] = "neighbor", [SD_NDISC_EVENT_REDIRECT] = "redirect", }; DEFINE_STRING_TABLE_LOOKUP(ndisc_event, sd_ndisc_event_t); static void ndisc_callback(sd_ndisc *ndisc, sd_ndisc_event_t event, void *message) { assert(ndisc); assert(event >= 0 && event < _SD_NDISC_EVENT_MAX); if (!ndisc->callback) return (void) log_ndisc(ndisc, "Received '%s' event.", ndisc_event_to_string(event)); log_ndisc(ndisc, "Invoking callback for '%s' event.", ndisc_event_to_string(event)); ndisc->callback(ndisc, event, message, ndisc->userdata); } int sd_ndisc_is_running(sd_ndisc *nd) { if (!nd) return false; return sd_event_source_get_enabled(nd->recv_event_source, NULL) > 0; } int sd_ndisc_set_callback( sd_ndisc *nd, sd_ndisc_callback_t callback, void *userdata) { assert_return(nd, -EINVAL); nd->callback = callback; nd->userdata = userdata; return 0; } int sd_ndisc_set_ifindex(sd_ndisc *nd, int ifindex) { assert_return(nd, -EINVAL); assert_return(ifindex > 0, -EINVAL); assert_return(!sd_ndisc_is_running(nd), -EBUSY); nd->ifindex = ifindex; return 0; } int sd_ndisc_set_ifname(sd_ndisc *nd, const char *ifname) { assert_return(nd, -EINVAL); assert_return(ifname, -EINVAL); if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE)) return -EINVAL; return free_and_strdup(&nd->ifname, ifname); } int sd_ndisc_get_ifname(sd_ndisc *nd, const char **ret) { int r; assert_return(nd, -EINVAL); r = get_ifname(nd->ifindex, &nd->ifname); if (r < 0) return r; if (ret) *ret = nd->ifname; return 0; } int sd_ndisc_set_link_local_address(sd_ndisc *nd, const struct in6_addr *addr) { assert_return(nd, -EINVAL); assert_return(!addr || in6_addr_is_link_local(addr), -EINVAL); if (addr) nd->link_local_addr = *addr; else zero(nd->link_local_addr); return 0; } int sd_ndisc_set_mac(sd_ndisc *nd, const struct ether_addr *mac_addr) { assert_return(nd, -EINVAL); if (mac_addr) nd->mac_addr = *mac_addr; else zero(nd->mac_addr); return 0; } int sd_ndisc_attach_event(sd_ndisc *nd, sd_event *event, int64_t priority) { int r; assert_return(nd, -EINVAL); assert_return(!sd_ndisc_is_running(nd), -EBUSY); assert_return(!nd->event, -EBUSY); if (event) nd->event = sd_event_ref(event); else { r = sd_event_default(&nd->event); if (r < 0) return 0; } nd->event_priority = priority; return 0; } int sd_ndisc_detach_event(sd_ndisc *nd) { assert_return(nd, -EINVAL); assert_return(!sd_ndisc_is_running(nd), -EBUSY); nd->event = sd_event_unref(nd->event); return 0; } sd_event *sd_ndisc_get_event(sd_ndisc *nd) { assert_return(nd, NULL); return nd->event; } static void ndisc_reset(sd_ndisc *nd) { assert(nd); (void) event_source_disable(nd->timeout_event_source); (void) event_source_disable(nd->timeout_no_ra); nd->retransmit_time = 0; nd->recv_event_source = sd_event_source_disable_unref(nd->recv_event_source); nd->fd = safe_close(nd->fd); } static sd_ndisc *ndisc_free(sd_ndisc *nd) { assert(nd); ndisc_reset(nd); sd_event_source_unref(nd->timeout_event_source); sd_event_source_unref(nd->timeout_no_ra); sd_ndisc_detach_event(nd); free(nd->ifname); return mfree(nd); } DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_ndisc, sd_ndisc, ndisc_free); int sd_ndisc_new(sd_ndisc **ret) { _cleanup_(sd_ndisc_unrefp) sd_ndisc *nd = NULL; assert_return(ret, -EINVAL); nd = new(sd_ndisc, 1); if (!nd) return -ENOMEM; *nd = (sd_ndisc) { .n_ref = 1, .fd = -EBADF, }; *ret = TAKE_PTR(nd); return 0; } static int ndisc_handle_router(sd_ndisc *nd, ICMP6Packet *packet) { _cleanup_(sd_ndisc_router_unrefp) sd_ndisc_router *rt = NULL; int r; assert(nd); assert(packet); rt = ndisc_router_new(packet); if (!rt) return -ENOMEM; r = ndisc_router_parse(nd, rt); if (r < 0) return r; (void) event_source_disable(nd->timeout_event_source); (void) event_source_disable(nd->timeout_no_ra); if (DEBUG_LOGGING) { _cleanup_free_ char *s = NULL; struct in6_addr a; uint64_t flags; uint8_t pref; usec_t lifetime; r = sd_ndisc_router_get_sender_address(rt, &a); if (r < 0) return r; r = sd_ndisc_router_get_flags(rt, &flags); if (r < 0) return r; r = ndisc_router_flags_to_string(flags, &s); if (r < 0) return r; r = sd_ndisc_router_get_preference(rt, &pref); if (r < 0) return r; r = sd_ndisc_router_get_lifetime(rt, &lifetime); if (r < 0) return r; log_ndisc(nd, "Received Router Advertisement from %s: flags=0x%0*"PRIx64"(%s), preference=%s, lifetime=%s", IN6_ADDR_TO_STRING(&a), flags & UINT64_C(0x00ffffffffffff00) ? 14 : 2, flags, /* suppress too many zeros if no extension */ s ?: "none", ndisc_router_preference_to_string(pref), FORMAT_TIMESPAN(lifetime, USEC_PER_SEC)); } ndisc_callback(nd, SD_NDISC_EVENT_ROUTER, rt); return 0; } static int ndisc_handle_neighbor(sd_ndisc *nd, ICMP6Packet *packet) { _cleanup_(sd_ndisc_neighbor_unrefp) sd_ndisc_neighbor *na = NULL; int r; assert(nd); assert(packet); na = ndisc_neighbor_new(packet); if (!na) return -ENOMEM; r = ndisc_neighbor_parse(nd, na); if (r < 0) return r; if (DEBUG_LOGGING) { struct in6_addr a; r = sd_ndisc_neighbor_get_sender_address(na, &a); if (r < 0) return r; log_ndisc(nd, "Received Neighbor Advertisement from %s: Router=%s, Solicited=%s, Override=%s", IN6_ADDR_TO_STRING(&a), yes_no(sd_ndisc_neighbor_is_router(na) > 0), yes_no(sd_ndisc_neighbor_is_solicited(na) > 0), yes_no(sd_ndisc_neighbor_is_override(na) > 0)); } ndisc_callback(nd, SD_NDISC_EVENT_NEIGHBOR, na); return 0; } static int ndisc_handle_redirect(sd_ndisc *nd, ICMP6Packet *packet) { _cleanup_(sd_ndisc_redirect_unrefp) sd_ndisc_redirect *rd = NULL; int r; assert(nd); assert(packet); rd = ndisc_redirect_new(packet); if (!rd) return -ENOMEM; r = ndisc_redirect_parse(nd, rd); if (r < 0) return r; if (DEBUG_LOGGING) { struct in6_addr sender, target, dest; r = sd_ndisc_redirect_get_sender_address(rd, &sender); if (r < 0) return r; r = sd_ndisc_redirect_get_target_address(rd, &target); if (r < 0) return r; r = sd_ndisc_redirect_get_destination_address(rd, &dest); if (r < 0) return r; log_ndisc(nd, "Received Redirect message from %s: Target=%s, Destination=%s", IN6_ADDR_TO_STRING(&sender), IN6_ADDR_TO_STRING(&target), IN6_ADDR_TO_STRING(&dest)); } ndisc_callback(nd, SD_NDISC_EVENT_REDIRECT, rd); return 0; } static int ndisc_recv(sd_event_source *s, int fd, uint32_t revents, void *userdata) { _cleanup_(icmp6_packet_unrefp) ICMP6Packet *packet = NULL; sd_ndisc *nd = ASSERT_PTR(userdata); int r; assert(s); assert(nd->event); r = icmp6_packet_receive(fd, &packet); if (r < 0) { log_ndisc_errno(nd, r, "Failed to receive ICMPv6 packet, ignoring: %m"); return 0; } /* The function icmp6_receive() accepts the null source address, but RFC 4861 Section 6.1.2 states * that hosts MUST discard messages with the null source address. */ if (in6_addr_is_null(&packet->sender_address)) { log_ndisc(nd, "Received an ICMPv6 packet from null address, ignoring."); return 0; } if (in6_addr_equal(&packet->sender_address, &nd->link_local_addr)) { log_ndisc(nd, "Received an ICMPv6 packet sent by the same interface, ignoring."); return 0; } r = icmp6_packet_get_type(packet); if (r < 0) { log_ndisc_errno(nd, r, "Received an invalid ICMPv6 packet, ignoring: %m"); return 0; } switch (r) { case ND_ROUTER_ADVERT: (void) ndisc_handle_router(nd, packet); break; case ND_NEIGHBOR_ADVERT: (void) ndisc_handle_neighbor(nd, packet); break; case ND_REDIRECT: (void) ndisc_handle_redirect(nd, packet); break; default: log_ndisc(nd, "Received an ICMPv6 packet with unexpected type %i, ignoring.", r); } return 0; } static int ndisc_send_router_solicitation(sd_ndisc *nd) { static const struct nd_router_solicit header = { .nd_rs_type = ND_ROUTER_SOLICIT, }; _cleanup_set_free_ Set *options = NULL; int r; assert(nd); if (!ether_addr_is_null(&nd->mac_addr)) { r = ndisc_option_set_link_layer_address(&options, SD_NDISC_OPTION_SOURCE_LL_ADDRESS, &nd->mac_addr); if (r < 0) return r; } return ndisc_send(nd->fd, &IN6_ADDR_ALL_ROUTERS_MULTICAST, &header.nd_rs_hdr, options, USEC_INFINITY); } static usec_t ndisc_timeout_compute_random(usec_t val) { /* compute a time that is random within ±10% of the given value */ return val - val / 10 + (random_u64() % (2 * USEC_PER_SEC)) * val / 10 / USEC_PER_SEC; } static int ndisc_timeout(sd_event_source *s, uint64_t usec, void *userdata) { sd_ndisc *nd = ASSERT_PTR(userdata); usec_t time_now; int r; assert(s); assert(nd->event); assert_se(sd_event_now(nd->event, CLOCK_BOOTTIME, &time_now) >= 0); if (!nd->retransmit_time) nd->retransmit_time = ndisc_timeout_compute_random(NDISC_ROUTER_SOLICITATION_INTERVAL); else { if (nd->retransmit_time > NDISC_MAX_ROUTER_SOLICITATION_INTERVAL / 2) nd->retransmit_time = ndisc_timeout_compute_random(NDISC_MAX_ROUTER_SOLICITATION_INTERVAL); else nd->retransmit_time += ndisc_timeout_compute_random(nd->retransmit_time); } r = event_reset_time(nd->event, &nd->timeout_event_source, CLOCK_BOOTTIME, time_now + nd->retransmit_time, 10 * USEC_PER_MSEC, ndisc_timeout, nd, nd->event_priority, "ndisc-timeout-no-ra", true); if (r < 0) goto fail; r = ndisc_send_router_solicitation(nd); if (r < 0) log_ndisc_errno(nd, r, "Failed to send Router Solicitation, next solicitation in %s, ignoring: %m", FORMAT_TIMESPAN(nd->retransmit_time, USEC_PER_SEC)); else log_ndisc(nd, "Sent Router Solicitation, next solicitation in %s", FORMAT_TIMESPAN(nd->retransmit_time, USEC_PER_SEC)); return 0; fail: (void) sd_ndisc_stop(nd); return 0; } static int ndisc_timeout_no_ra(sd_event_source *s, uint64_t usec, void *userdata) { sd_ndisc *nd = ASSERT_PTR(userdata); assert(s); log_ndisc(nd, "No RA received before link confirmation timeout"); (void) event_source_disable(nd->timeout_no_ra); ndisc_callback(nd, SD_NDISC_EVENT_TIMEOUT, NULL); return 0; } int sd_ndisc_stop(sd_ndisc *nd) { if (!nd) return 0; if (!sd_ndisc_is_running(nd)) return 0; log_ndisc(nd, "Stopping IPv6 Router Solicitation client"); ndisc_reset(nd); return 1; } static int ndisc_setup_recv_event(sd_ndisc *nd) { int r; assert(nd); assert(nd->event); assert(nd->ifindex > 0); _cleanup_close_ int fd = -EBADF; fd = icmp6_bind(nd->ifindex, /* is_router = */ false); if (fd < 0) return fd; _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; r = sd_event_add_io(nd->event, &s, fd, EPOLLIN, ndisc_recv, nd); if (r < 0) return r; r = sd_event_source_set_priority(s, nd->event_priority); if (r < 0) return r; (void) sd_event_source_set_description(s, "ndisc-receive-router-message"); nd->fd = TAKE_FD(fd); nd->recv_event_source = TAKE_PTR(s); return 1; } static int ndisc_setup_timer(sd_ndisc *nd) { int r; assert(nd); assert(nd->event); r = event_reset_time_relative(nd->event, &nd->timeout_event_source, CLOCK_BOOTTIME, USEC_PER_SEC / 2, 1 * USEC_PER_SEC, /* See RFC 8415 sec. 18.2.1 */ ndisc_timeout, nd, nd->event_priority, "ndisc-timeout", true); if (r < 0) return r; r = event_reset_time_relative(nd->event, &nd->timeout_no_ra, CLOCK_BOOTTIME, NDISC_TIMEOUT_NO_RA_USEC, 10 * USEC_PER_MSEC, ndisc_timeout_no_ra, nd, nd->event_priority, "ndisc-timeout-no-ra", true); if (r < 0) return r; return 0; } int sd_ndisc_start(sd_ndisc *nd) { int r; assert_return(nd, -EINVAL); assert_return(nd->event, -EINVAL); assert_return(nd->ifindex > 0, -EINVAL); if (sd_ndisc_is_running(nd)) return 0; r = ndisc_setup_recv_event(nd); if (r < 0) goto fail; r = ndisc_setup_timer(nd); if (r < 0) goto fail; log_ndisc(nd, "Started IPv6 Router Solicitation client"); return 1; fail: ndisc_reset(nd); return r; }