| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| /*** |
| Copyright © 2014 Intel Corporation. All rights reserved. |
| ***/ |
| |
| #include <netinet/icmp6.h> |
| #include <netinet/in.h> |
| |
| #include "sd-ndisc.h" |
| |
| #include "alloc-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-router.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", |
| }; |
| |
| DEFINE_STRING_TABLE_LOOKUP(ndisc_event, sd_ndisc_event_t); |
| |
| static void ndisc_callback(sd_ndisc *ndisc, sd_ndisc_event_t event, sd_ndisc_router *rt) { |
| 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, rt, ndisc->userdata); |
| } |
| |
| 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(nd->fd < 0, -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_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(nd->fd < 0, -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(nd->fd < 0, -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_datagram(sd_ndisc *nd, sd_ndisc_router *rt) { |
| int r; |
| |
| assert(nd); |
| assert(rt); |
| |
| r = ndisc_router_parse(nd, rt); |
| if (r < 0) |
| return r; |
| |
| log_ndisc(nd, "Received Router Advertisement: flags %s preference %s lifetime %" PRIu16 " sec", |
| rt->flags & ND_RA_FLAG_MANAGED ? "MANAGED" : rt->flags & ND_RA_FLAG_OTHER ? "OTHER" : "none", |
| rt->preference == SD_NDISC_PREFERENCE_HIGH ? "high" : rt->preference == SD_NDISC_PREFERENCE_LOW ? "low" : "medium", |
| rt->lifetime); |
| |
| ndisc_callback(nd, SD_NDISC_EVENT_ROUTER, rt); |
| return 0; |
| } |
| |
| static int ndisc_recv(sd_event_source *s, int fd, uint32_t revents, void *userdata) { |
| _cleanup_(sd_ndisc_router_unrefp) sd_ndisc_router *rt = NULL; |
| sd_ndisc *nd = ASSERT_PTR(userdata); |
| ssize_t buflen; |
| int r; |
| |
| assert(s); |
| assert(nd->event); |
| |
| buflen = next_datagram_size_fd(fd); |
| if (buflen < 0) { |
| if (ERRNO_IS_TRANSIENT(buflen) || ERRNO_IS_DISCONNECT(buflen)) |
| return 0; |
| |
| log_ndisc_errno(nd, buflen, "Failed to determine datagram size to read, ignoring: %m"); |
| return 0; |
| } |
| |
| rt = ndisc_router_new(buflen); |
| if (!rt) |
| return -ENOMEM; |
| |
| r = icmp6_receive(fd, NDISC_ROUTER_RAW(rt), rt->raw_size, &rt->address, &rt->timestamp); |
| if (r < 0) { |
| if (ERRNO_IS_TRANSIENT(r) || ERRNO_IS_DISCONNECT(r)) |
| return 0; |
| |
| switch (r) { |
| case -EADDRNOTAVAIL: |
| log_ndisc(nd, "Received RA from non-link-local address %s. Ignoring.", |
| IN6_ADDR_TO_STRING(&rt->address)); |
| break; |
| |
| case -EMULTIHOP: |
| log_ndisc(nd, "Received RA with invalid hop limit. Ignoring."); |
| break; |
| |
| case -EPFNOSUPPORT: |
| log_ndisc(nd, "Received invalid source address from ICMPv6 socket. Ignoring."); |
| break; |
| |
| default: |
| log_ndisc_errno(nd, r, "Unexpected error while reading from ICMPv6, ignoring: %m"); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| (void) event_source_disable(nd->timeout_event_source); |
| (void) ndisc_handle_datagram(nd, rt); |
| return 0; |
| } |
| |
| 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 = icmp6_send_router_solicitation(nd->fd, &nd->mac_addr); |
| 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 (nd->fd < 0) |
| return 0; |
| |
| log_ndisc(nd, "Stopping IPv6 Router Solicitation client"); |
| |
| ndisc_reset(nd); |
| return 1; |
| } |
| |
| int sd_ndisc_start(sd_ndisc *nd) { |
| int r; |
| usec_t time_now; |
| |
| assert_return(nd, -EINVAL); |
| assert_return(nd->event, -EINVAL); |
| assert_return(nd->ifindex > 0, -EINVAL); |
| |
| if (nd->fd >= 0) |
| return 0; |
| |
| assert(!nd->recv_event_source); |
| |
| r = sd_event_now(nd->event, CLOCK_BOOTTIME, &time_now); |
| if (r < 0) |
| goto fail; |
| |
| nd->fd = icmp6_bind_router_solicitation(nd->ifindex); |
| if (nd->fd < 0) |
| return nd->fd; |
| |
| r = sd_event_add_io(nd->event, &nd->recv_event_source, nd->fd, EPOLLIN, ndisc_recv, nd); |
| if (r < 0) |
| goto fail; |
| |
| r = sd_event_source_set_priority(nd->recv_event_source, nd->event_priority); |
| if (r < 0) |
| goto fail; |
| |
| (void) sd_event_source_set_description(nd->recv_event_source, "ndisc-receive-message"); |
| |
| r = event_reset_time(nd->event, &nd->timeout_event_source, |
| CLOCK_BOOTTIME, |
| time_now + 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) |
| goto fail; |
| |
| r = event_reset_time(nd->event, &nd->timeout_no_ra, |
| CLOCK_BOOTTIME, |
| time_now + 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) |
| goto fail; |
| |
| log_ndisc(nd, "Started IPv6 Router Solicitation client"); |
| return 1; |
| |
| fail: |
| ndisc_reset(nd); |
| return r; |
| } |