diff options
Diffstat (limited to 'sys/netlink/route/neigh.c')
-rw-r--r-- | sys/netlink/route/neigh.c | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/sys/netlink/route/neigh.c b/sys/netlink/route/neigh.c new file mode 100644 index 000000000000..9eaaae263254 --- /dev/null +++ b/sys/netlink/route/neigh.c @@ -0,0 +1,601 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2022 Alexander V. Chernikov <melifaro@FreeBSD.org> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/cdefs.h> +#include "opt_inet.h" +#include "opt_inet6.h" +#include <sys/types.h> +#include <sys/eventhandler.h> +#include <sys/kernel.h> +#include <sys/malloc.h> +#include <sys/socket.h> +#include <sys/syslog.h> + +#include <net/if.h> +#include <net/if_var.h> +#include <net/if_private.h> +#include <net/if_llatbl.h> +#include <netlink/netlink.h> +#include <netlink/netlink_ctl.h> +#include <netlink/netlink_route.h> +#include <netlink/route/route_var.h> + +#include <netinet6/in6_var.h> /* nd6.h requires this */ +#include <netinet6/nd6.h> /* nd6 state machine */ +#include <netinet6/scope6_var.h> /* scope deembedding */ + +#define DEBUG_MOD_NAME nl_neigh +#define DEBUG_MAX_LEVEL LOG_DEBUG3 +#include <netlink/netlink_debug.h> +_DECLARE_DEBUG(LOG_INFO); + +static int lle_families[] = { AF_INET, AF_INET6 }; + +static eventhandler_tag lle_event_p; + +struct netlink_walkargs { + struct nl_writer *nw; + struct nlmsghdr hdr; + struct nlpcb *so; + if_t ifp; + int family; + int error; + int count; + int dumped; +}; + +static int +lle_state_to_nl_state(int family, struct llentry *lle) +{ + int state = lle->ln_state; + + switch (family) { + case AF_INET: + if (lle->la_flags & (LLE_STATIC | LLE_IFADDR)) + state = 1; + switch (state) { + case 0: /* ARP_LLINFO_INCOMPLETE */ + return (NUD_INCOMPLETE); + case 1: /* ARP_LLINFO_REACHABLE */ + return (NUD_REACHABLE); + case 2: /* ARP_LLINFO_VERIFY */ + return (NUD_PROBE); + } + break; + case AF_INET6: + switch (state) { + case ND6_LLINFO_INCOMPLETE: + return (NUD_INCOMPLETE); + case ND6_LLINFO_REACHABLE: + return (NUD_REACHABLE); + case ND6_LLINFO_STALE: + return (NUD_STALE); + case ND6_LLINFO_DELAY: + return (NUD_DELAY); + case ND6_LLINFO_PROBE: + return (NUD_PROBE); + } + break; + } + + return (NUD_NONE); +} + +static uint32_t +lle_flags_to_nl_flags(const struct llentry *lle) +{ + uint32_t nl_flags = 0; + + if (lle->la_flags & LLE_IFADDR) + nl_flags |= NTF_SELF; + if (lle->la_flags & LLE_PUB) + nl_flags |= NTF_PROXY; + if (lle->la_flags & LLE_STATIC) + nl_flags |= NTF_STICKY; + if (lle->ln_router != 0) + nl_flags |= NTF_ROUTER; + + return (nl_flags); +} + +static uint32_t +get_lle_next_ts(const struct llentry *lle) +{ + if (lle->la_expire == 0) + return (0); + return (lle->la_expire + lle->lle_remtime / hz + time_second - time_uptime); +} + +static int +dump_lle_locked(struct llentry *lle, void *arg) +{ + struct netlink_walkargs *wa = (struct netlink_walkargs *)arg; + struct nlmsghdr *hdr = &wa->hdr; + struct nl_writer *nw = wa->nw; + struct ndmsg *ndm; +#if defined(INET) || defined(INET6) + union { + struct in_addr in; + struct in6_addr in6; + } addr; +#endif + + IF_DEBUG_LEVEL(LOG_DEBUG2) { + char llebuf[NHOP_PRINT_BUFSIZE]; + llentry_print_buf_lltable(lle, llebuf, sizeof(llebuf)); + NL_LOG(LOG_DEBUG2, "dumping %s", llebuf); + } + + if (!nlmsg_reply(nw, hdr, sizeof(struct ndmsg))) + goto enomem; + + ndm = nlmsg_reserve_object(nw, struct ndmsg); + ndm->ndm_family = wa->family; + ndm->ndm_ifindex = if_getindex(wa->ifp); + ndm->ndm_state = lle_state_to_nl_state(wa->family, lle); + ndm->ndm_flags = lle_flags_to_nl_flags(lle); + + switch (wa->family) { +#ifdef INET + case AF_INET: + addr.in = lle->r_l3addr.addr4; + nlattr_add(nw, NDA_DST, 4, &addr); + break; +#endif +#ifdef INET6 + case AF_INET6: + addr.in6 = lle->r_l3addr.addr6; + in6_clearscope(&addr.in6); + nlattr_add(nw, NDA_DST, 16, &addr); + break; +#endif + } + + if (lle->r_flags & RLLE_VALID) { + /* Has L2 */ + int addrlen = if_getaddrlen(wa->ifp); + nlattr_add(nw, NDA_LLADDR, addrlen, lle->ll_addr); + } + + nlattr_add_u32(nw, NDA_PROBES, lle->la_asked); + + struct nda_cacheinfo *cache; + cache = nlmsg_reserve_attr(nw, NDA_CACHEINFO, struct nda_cacheinfo); + if (cache == NULL) + goto enomem; + /* TODO: provide confirmed/updated */ + cache->ndm_refcnt = lle->lle_refcnt; + + int off = nlattr_add_nested(nw, NDA_FREEBSD); + if (off != 0) { + nlattr_add_u32(nw, NDAF_NEXT_STATE_TS, get_lle_next_ts(lle)); + + nlattr_set_len(nw, off); + } + + if (nlmsg_end(nw)) + return (0); +enomem: + NL_LOG(LOG_DEBUG, "unable to dump lle state (ENOMEM)"); + nlmsg_abort(nw); + return (ENOMEM); +} + +static int +dump_lle(struct lltable *llt, struct llentry *lle, void *arg) +{ + int error; + + LLE_RLOCK(lle); + error = dump_lle_locked(lle, arg); + LLE_RUNLOCK(lle); + return (error); +} + +static bool +dump_llt(struct lltable *llt, struct netlink_walkargs *wa) +{ + lltable_foreach_lle(llt, dump_lle, wa); + + return (true); +} + +static int +dump_llts_iface(struct netlink_walkargs *wa, if_t ifp, int family) +{ + int error = 0; + + wa->ifp = ifp; + for (int i = 0; i < sizeof(lle_families) / sizeof(int); i++) { + int fam = lle_families[i]; + struct lltable *llt = lltable_get(ifp, fam); + if (llt != NULL && (family == 0 || family == fam)) { + wa->count++; + wa->family = fam; + if (!dump_llt(llt, wa)) { + error = ENOMEM; + break; + } + wa->dumped++; + } + } + return (error); +} + +static int +dump_llts(struct netlink_walkargs *wa, if_t ifp, int family) +{ + NL_LOG(LOG_DEBUG2, "Start dump ifp=%s family=%d", ifp ? if_name(ifp) : "NULL", family); + + wa->hdr.nlmsg_flags |= NLM_F_MULTI; + + if (ifp != NULL) { + dump_llts_iface(wa, ifp, family); + } else { + struct if_iter it; + + for (ifp = if_iter_start(&it); ifp != NULL; ifp = if_iter_next(&it)) { + dump_llts_iface(wa, ifp, family); + } + if_iter_finish(&it); + } + + NL_LOG(LOG_DEBUG2, "End dump, iterated %d dumped %d", wa->count, wa->dumped); + + if (!nlmsg_end_dump(wa->nw, wa->error, &wa->hdr)) { + NL_LOG(LOG_DEBUG, "Unable to add new message"); + return (ENOMEM); + } + + return (0); +} + +static int +get_lle(struct netlink_walkargs *wa, if_t ifp, int family, struct sockaddr *dst) +{ + struct lltable *llt = lltable_get(ifp, family); + if (llt == NULL) + return (ESRCH); + + struct llentry *lle = lla_lookup(llt, LLE_UNLOCKED, dst); + if (lle == NULL) + return (ESRCH); + + wa->ifp = ifp; + wa->family = family; + + return (dump_lle(llt, lle, wa)); +} + +static void +set_scope6(struct sockaddr *sa, if_t ifp) +{ +#ifdef INET6 + if (sa != NULL && sa->sa_family == AF_INET6 && ifp != NULL) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)sa; + + if (IN6_IS_ADDR_LINKLOCAL(&sa6->sin6_addr)) + in6_set_unicast_scopeid(&sa6->sin6_addr, if_getindex(ifp)); + } +#endif +} + +struct nl_parsed_neigh { + struct sockaddr *nda_dst; + struct ifnet *nda_ifp; + struct nlattr *nda_lladdr; + uint32_t ndaf_next_ts; + uint32_t ndm_flags; + uint16_t ndm_state; + uint8_t ndm_family; +}; + +#define _IN(_field) offsetof(struct ndmsg, _field) +#define _OUT(_field) offsetof(struct nl_parsed_neigh, _field) +static const struct nlattr_parser nla_p_neigh_fbsd[] = { + { .type = NDAF_NEXT_STATE_TS, .off = _OUT(ndaf_next_ts), .cb = nlattr_get_uint32 }, +}; +NL_DECLARE_ATTR_PARSER(neigh_fbsd_parser, nla_p_neigh_fbsd); + +static const struct nlfield_parser nlf_p_neigh[] = { + { .off_in = _IN(ndm_family), .off_out = _OUT(ndm_family), .cb = nlf_get_u8 }, + { .off_in = _IN(ndm_flags), .off_out = _OUT(ndm_flags), .cb = nlf_get_u8_u32 }, + { .off_in = _IN(ndm_state), .off_out = _OUT(ndm_state), .cb = nlf_get_u16 }, + { .off_in = _IN(ndm_ifindex), .off_out = _OUT(nda_ifp), .cb = nlf_get_ifpz }, +}; + +static const struct nlattr_parser nla_p_neigh[] = { + { .type = NDA_DST, .off = _OUT(nda_dst), .cb = nlattr_get_ip }, + { .type = NDA_LLADDR, .off = _OUT(nda_lladdr), .cb = nlattr_get_nla }, + { .type = NDA_IFINDEX, .off = _OUT(nda_ifp), .cb = nlattr_get_ifp }, + { .type = NDA_FLAGS_EXT, .off = _OUT(ndm_flags), .cb = nlattr_get_uint32 }, + { .type = NDA_FREEBSD, .arg = &neigh_fbsd_parser, .cb = nlattr_get_nested }, +}; +#undef _IN +#undef _OUT + +static bool +post_p_neigh(void *_attrs, struct nl_pstate *npt __unused) +{ + struct nl_parsed_neigh *attrs = (struct nl_parsed_neigh *)_attrs; + + set_scope6(attrs->nda_dst, attrs->nda_ifp); + return (true); +} +NL_DECLARE_PARSER_EXT(ndmsg_parser, struct ndmsg, NULL, nlf_p_neigh, nla_p_neigh, post_p_neigh); + + +/* + * type=RTM_NEWNEIGH, flags=NLM_F_REQUEST|NLM_F_ACK|NLM_F_EXCL|NLM_F_CREATE, seq=1661941473, pid=0}, + * {ndm_family=AF_INET6, ndm_ifindex=if_nametoindex("enp0s31f6"), ndm_state=NUD_PERMANENT, ndm_flags=0, ndm_type=RTN_UNSPEC}, + * [ + * {{nla_len=20, nla_type=NDA_DST}, inet_pton(AF_INET6, "2a01:4f8:13a:70c::3")}, + * {{nla_len=10, nla_type=NDA_LLADDR}, 20:4e:71:62:ae:f2}]}, iov_len=60} + */ + +static int +rtnl_handle_newneigh(struct nlmsghdr *hdr, struct nlpcb *nlp, struct nl_pstate *npt) +{ + int error; + + struct nl_parsed_neigh attrs = {}; + error = nl_parse_nlmsg(hdr, &ndmsg_parser, npt, &attrs); + if (error != 0) + return (error); + + if (attrs.nda_ifp == NULL || attrs.nda_dst == NULL || attrs.nda_lladdr == NULL) { + if (attrs.nda_ifp == NULL) + NLMSG_REPORT_ERR_MSG(npt, "NDA_IFINDEX / ndm_ifindex not set"); + if (attrs.nda_dst == NULL) + NLMSG_REPORT_ERR_MSG(npt, "NDA_DST not set"); + if (attrs.nda_lladdr == NULL) + NLMSG_REPORT_ERR_MSG(npt, "NDA_LLADDR not set"); + return (EINVAL); + } + + if (attrs.nda_dst->sa_family != attrs.ndm_family) { + NLMSG_REPORT_ERR_MSG(npt, + "NDA_DST family (%d) is different from ndm_family (%d)", + attrs.nda_dst->sa_family, attrs.ndm_family); + return (EINVAL); + } + + int addrlen = if_getaddrlen(attrs.nda_ifp); + if (attrs.nda_lladdr->nla_len != sizeof(struct nlattr) + addrlen) { + NLMSG_REPORT_ERR_MSG(npt, + "NDA_LLADDR address length (%d) is different from expected (%d)", + (int)attrs.nda_lladdr->nla_len - (int)sizeof(struct nlattr), addrlen); + return (EINVAL); + } + + const uint16_t supported_flags = NTF_PROXY | NTF_STICKY; + if ((attrs.ndm_flags & supported_flags) != attrs.ndm_flags) { + NLMSG_REPORT_ERR_MSG(npt, "ndm_flags %X not supported", + attrs.ndm_flags &~ supported_flags); + return (ENOTSUP); + } + + /* Replacement requires new entry creation anyway */ + if ((hdr->nlmsg_flags & (NLM_F_CREATE | NLM_F_REPLACE)) == 0) + return (ENOTSUP); + + struct lltable *llt = lltable_get(attrs.nda_ifp, attrs.ndm_family); + if (llt == NULL) + return (EAFNOSUPPORT); + + + uint8_t linkhdr[LLE_MAX_LINKHDR]; + size_t linkhdrsize = sizeof(linkhdr); + int lladdr_off = 0; + if (lltable_calc_llheader(attrs.nda_ifp, attrs.ndm_family, + (char *)(attrs.nda_lladdr + 1), linkhdr, &linkhdrsize, &lladdr_off) != 0) { + NLMSG_REPORT_ERR_MSG(npt, "unable to calculate lle prepend data"); + return (EINVAL); + } + + int lle_flags = (attrs.ndm_flags & NTF_PROXY) ? LLE_PUB : 0; + if (attrs.ndm_flags & NTF_STICKY) + lle_flags |= LLE_STATIC; + struct llentry *lle = lltable_alloc_entry(llt, lle_flags, attrs.nda_dst); + if (lle == NULL) + return (ENOMEM); + lltable_set_entry_addr(attrs.nda_ifp, lle, linkhdr, linkhdrsize, lladdr_off); + + if (attrs.ndm_flags & NTF_STICKY) + lle->la_expire = 0; + else + lle->la_expire = attrs.ndaf_next_ts - time_second + time_uptime; + + /* llentry created, try to insert or update */ + IF_AFDATA_WLOCK(attrs.nda_ifp); + LLE_WLOCK(lle); + struct llentry *lle_tmp = lla_lookup(llt, LLE_EXCLUSIVE, attrs.nda_dst); + if (lle_tmp != NULL) { + error = EEXIST; + if (hdr->nlmsg_flags & NLM_F_REPLACE) { + error = EPERM; + if ((lle_tmp->la_flags & LLE_IFADDR) == 0) { + error = 0; /* success */ + lltable_unlink_entry(llt, lle_tmp); + llentry_free(lle_tmp); + lle_tmp = NULL; + lltable_link_entry(llt, lle); + } + } + if (lle_tmp) + LLE_WUNLOCK(lle_tmp); + } else { + if (hdr->nlmsg_flags & NLM_F_CREATE) + lltable_link_entry(llt, lle); + else + error = ENOENT; + } + IF_AFDATA_WUNLOCK(attrs.nda_ifp); + + if (error != 0) { + /* throw away the newly allocated llentry */ + llentry_free(lle); + return (error); + } + + /* XXX: We're inside epoch */ + EVENTHANDLER_INVOKE(lle_event, lle, LLENTRY_RESOLVED); + LLE_WUNLOCK(lle); + llt->llt_post_resolved(llt, lle); + + return (0); +} + +static int +rtnl_handle_delneigh(struct nlmsghdr *hdr, struct nlpcb *nlp, struct nl_pstate *npt) +{ + int error; + + struct nl_parsed_neigh attrs = {}; + error = nl_parse_nlmsg(hdr, &ndmsg_parser, npt, &attrs); + if (error != 0) + return (error); + + if (attrs.nda_dst == NULL) { + NLMSG_REPORT_ERR_MSG(npt, "NDA_DST not set"); + return (EINVAL); + } + + if (attrs.nda_ifp == NULL) { + NLMSG_REPORT_ERR_MSG(npt, "no ifindex provided"); + return (EINVAL); + } + + struct lltable *llt = lltable_get(attrs.nda_ifp, attrs.ndm_family); + if (llt == NULL) + return (EAFNOSUPPORT); + + return (lltable_delete_addr(llt, 0, attrs.nda_dst)); +} + +static int +rtnl_handle_getneigh(struct nlmsghdr *hdr, struct nlpcb *nlp, struct nl_pstate *npt) +{ + int error; + + struct nl_parsed_neigh attrs = {}; + error = nl_parse_nlmsg(hdr, &ndmsg_parser, npt, &attrs); + if (error != 0) + return (error); + + if (attrs.nda_dst != NULL && attrs.nda_ifp == NULL) { + NLMSG_REPORT_ERR_MSG(npt, "has NDA_DST but no ifindex provided"); + return (EINVAL); + } + + struct netlink_walkargs wa = { + .so = nlp, + .nw = npt->nw, + .hdr.nlmsg_pid = hdr->nlmsg_pid, + .hdr.nlmsg_seq = hdr->nlmsg_seq, + .hdr.nlmsg_flags = hdr->nlmsg_flags, + .hdr.nlmsg_type = NL_RTM_NEWNEIGH, + }; + + if (attrs.nda_dst == NULL) + error = dump_llts(&wa, attrs.nda_ifp, attrs.ndm_family); + else + error = get_lle(&wa, attrs.nda_ifp, attrs.ndm_family, attrs.nda_dst); + + return (error); +} + +static const struct rtnl_cmd_handler cmd_handlers[] = { + { + .cmd = NL_RTM_NEWNEIGH, + .name = "RTM_NEWNEIGH", + .cb = &rtnl_handle_newneigh, + .priv = PRIV_NET_ROUTE, + }, + { + .cmd = NL_RTM_DELNEIGH, + .name = "RTM_DELNEIGH", + .cb = &rtnl_handle_delneigh, + .priv = PRIV_NET_ROUTE, + }, + { + .cmd = NL_RTM_GETNEIGH, + .name = "RTM_GETNEIGH", + .cb = &rtnl_handle_getneigh, + } +}; + +static void +rtnl_lle_event(void *arg __unused, struct llentry *lle, int evt) +{ + struct nl_writer nw; + if_t ifp; + int family; + + LLE_WLOCK_ASSERT(lle); + + ifp = lltable_get_ifp(lle->lle_tbl); + family = lltable_get_af(lle->lle_tbl); + + if (family != AF_INET && family != AF_INET6) + return; + + int nlmsgs_type = evt == LLENTRY_RESOLVED ? NL_RTM_NEWNEIGH : NL_RTM_DELNEIGH; + + if (!nl_writer_group(&nw, NLMSG_SMALL, NETLINK_ROUTE, RTNLGRP_NEIGH, 0, + false)) { + NL_LOG(LOG_DEBUG, "error allocating group writer"); + return; + } + + struct netlink_walkargs wa = { + .hdr.nlmsg_type = nlmsgs_type, + .nw = &nw, + .ifp = ifp, + .family = family, + }; + + dump_lle_locked(lle, &wa); + nlmsg_flush(&nw); +} + +static const struct nlhdr_parser *all_parsers[] = { &ndmsg_parser, &neigh_fbsd_parser }; + +void +rtnl_neighs_init(void) +{ + NL_VERIFY_PARSERS(all_parsers); + rtnl_register_messages(cmd_handlers, nitems(cmd_handlers)); + lle_event_p = EVENTHANDLER_REGISTER(lle_event, rtnl_lle_event, NULL, + EVENTHANDLER_PRI_ANY); +} + +void +rtnl_neighs_destroy(void) +{ + EVENTHANDLER_DEREGISTER(lle_event, lle_event_p); +} |