/* SPDX-License-Identifier: LGPL-2.1+ */ /*** This file is part of systemd. Copyright (C) 2014 Intel Corporation. All rights reserved. systemd is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. systemd is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with systemd; If not, see . ***/ #include #include #include "sd-ndisc.h" #include "alloc-util.h" #include "fd-util.h" #include "icmp6-util.h" #include "in-addr-util.h" #include "ndisc-internal.h" #include "ndisc-router.h" #include "random-util.h" #include "socket-util.h" #include "string-util.h" #include "util.h" #define NDISC_TIMEOUT_NO_RA_USEC (NDISC_ROUTER_SOLICITATION_INTERVAL * NDISC_MAX_ROUTER_SOLICITATIONS) static void ndisc_callback(sd_ndisc *ndisc, sd_ndisc_event event, sd_ndisc_router *rt) { assert(ndisc); log_ndisc("Invoking callback for '%c'.", event); if (!ndisc->callback) return; ndisc->callback(ndisc, event, rt, ndisc->userdata); } _public_ 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; } _public_ 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; } _public_ 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; } _public_ 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; } _public_ 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; } _public_ sd_event *sd_ndisc_get_event(sd_ndisc *nd) { assert_return(nd, NULL); return nd->event; } _public_ sd_ndisc *sd_ndisc_ref(sd_ndisc *nd) { if (!nd) return NULL; assert(nd->n_ref > 0); nd->n_ref++; return nd; } static int ndisc_reset(sd_ndisc *nd) { assert(nd); nd->timeout_event_source = sd_event_source_unref(nd->timeout_event_source); nd->timeout_no_ra = sd_event_source_unref(nd->timeout_no_ra); nd->retransmit_time = 0; nd->recv_event_source = sd_event_source_unref(nd->recv_event_source); nd->fd = safe_close(nd->fd); return 0; } _public_ sd_ndisc *sd_ndisc_unref(sd_ndisc *nd) { if (!nd) return NULL; assert(nd->n_ref > 0); nd->n_ref--; if (nd->n_ref > 0) return NULL; ndisc_reset(nd); sd_ndisc_detach_event(nd); return mfree(nd); } _public_ int sd_ndisc_new(sd_ndisc **ret) { _cleanup_(sd_ndisc_unrefp) sd_ndisc *nd = NULL; assert_return(ret, -EINVAL); nd = new0(sd_ndisc, 1); if (!nd) return -ENOMEM; nd->n_ref = 1; nd->fd = -1; *ret = TAKE_PTR(nd); return 0; } _public_ int sd_ndisc_get_mtu(sd_ndisc *nd, uint32_t *mtu) { assert_return(nd, -EINVAL); assert_return(mtu, -EINVAL); if (nd->mtu == 0) return -ENODATA; *mtu = nd->mtu; return 0; } _public_ int sd_ndisc_get_hop_limit(sd_ndisc *nd, uint8_t *ret) { assert_return(nd, -EINVAL); assert_return(ret, -EINVAL); if (nd->hop_limit == 0) return -ENODATA; *ret = nd->hop_limit; return 0; } static int ndisc_handle_datagram(sd_ndisc *nd, sd_ndisc_router *rt) { int r; assert(nd); assert(rt); r = ndisc_router_parse(rt); if (r == -EBADMSG) /* Bad packet */ return 0; if (r < 0) return 0; /* Update global variables we keep */ if (rt->mtu > 0) nd->mtu = rt->mtu; if (rt->hop_limit > 0) nd->hop_limit = rt->hop_limit; log_ndisc("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 = userdata; ssize_t buflen; int r; _cleanup_free_ char *addr = NULL; assert(s); assert(nd); assert(nd->event); buflen = next_datagram_size_fd(fd); if (buflen < 0) return log_ndisc_errno(buflen, "Failed to determine datagram size to read: %m"); 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) { switch (r) { case -EADDRNOTAVAIL: (void) in_addr_to_string(AF_INET6, (union in_addr_union*) &rt->address, &addr); log_ndisc("Received RA from non-link-local address %s. Ignoring", addr); break; case -EMULTIHOP: log_ndisc("Received RA with invalid hop limit. Ignoring."); break; case -EPFNOSUPPORT: log_ndisc("Received invalid source address from ICMPv6 socket."); break; } return 0; } nd->timeout_event_source = sd_event_source_unref(nd->timeout_event_source); return ndisc_handle_datagram(nd, rt); } 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 = userdata; usec_t time_now; int r; char time_string[FORMAT_TIMESPAN_MAX]; assert(s); assert(nd); assert(nd->event); assert_se(sd_event_now(nd->event, clock_boottime_or_monotonic(), &time_now) >= 0); nd->timeout_event_source = sd_event_source_unref(nd->timeout_event_source); 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 = sd_event_add_time(nd->event, &nd->timeout_event_source, clock_boottime_or_monotonic(), time_now + nd->retransmit_time, 10 * USEC_PER_MSEC, ndisc_timeout, nd); if (r < 0) goto fail; r = sd_event_source_set_priority(nd->timeout_event_source, nd->event_priority); if (r < 0) goto fail; (void) sd_event_source_set_description(nd->timeout_event_source, "ndisc-timeout-no-ra"); r = sd_event_source_set_enabled(nd->timeout_event_source, SD_EVENT_ONESHOT); if (r < 0) { log_ndisc_errno(r, "Error reenabling timer: %m"); goto fail; } r = icmp6_send_router_solicitation(nd->fd, &nd->mac_addr); if (r < 0) { log_ndisc_errno(r, "Error sending Router Solicitation: %m"); goto fail; } log_ndisc("Sent Router Solicitation, next solicitation in %s", format_timespan(time_string, FORMAT_TIMESPAN_MAX, nd->retransmit_time, USEC_PER_SEC)); return 0; fail: sd_ndisc_stop(nd); return 0; } static int ndisc_timeout_no_ra(sd_event_source *s, uint64_t usec, void *userdata) { sd_ndisc *nd = userdata; assert(s); assert(nd); log_ndisc("No RA received before link confirmation timeout"); nd->timeout_no_ra = sd_event_source_unref(nd->timeout_no_ra); ndisc_callback(nd, SD_NDISC_EVENT_TIMEOUT, NULL); return 0; } _public_ int sd_ndisc_stop(sd_ndisc *nd) { assert_return(nd, -EINVAL); if (nd->fd < 0) return 0; log_ndisc("Stopping IPv6 Router Solicitation client"); ndisc_reset(nd); return 1; } _public_ 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); assert(!nd->timeout_event_source); r = sd_event_now(nd->event, clock_boottime_or_monotonic(), &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 = sd_event_add_time(nd->event, &nd->timeout_event_source, clock_boottime_or_monotonic(), 0, 0, ndisc_timeout, nd); if (r < 0) goto fail; r = sd_event_source_set_priority(nd->timeout_event_source, nd->event_priority); if (r < 0) goto fail; (void) sd_event_source_set_description(nd->timeout_event_source, "ndisc-timeout"); r = sd_event_add_time(nd->event, &nd->timeout_no_ra, clock_boottime_or_monotonic(), time_now + NDISC_TIMEOUT_NO_RA_USEC, 10 * USEC_PER_MSEC, ndisc_timeout_no_ra, nd); if (r < 0) goto fail; r = sd_event_source_set_priority(nd->timeout_no_ra, nd->event_priority); if (r < 0) goto fail; (void) sd_event_source_set_description(nd->timeout_no_ra, "ndisc-timeout-no-ra"); log_ndisc("Started IPv6 Router Solicitation client"); return 1; fail: ndisc_reset(nd); return r; }