1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2022 Alexander V. Chernikov <melifaro@FreeBSD.org> 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 * SUCH DAMAGE. 26 */ 27 28 #include "opt_netlink.h" 29 30 #include <sys/cdefs.h> 31 __FBSDID("$FreeBSD$"); 32 #include "opt_inet.h" 33 #include "opt_inet6.h" 34 #include <sys/types.h> 35 #include <sys/eventhandler.h> 36 #include <sys/kernel.h> 37 #include <sys/malloc.h> 38 #include <sys/socket.h> 39 #include <sys/syslog.h> 40 41 #include <net/if.h> 42 #include <net/if_var.h> 43 #include <net/if_private.h> 44 #include <net/if_llatbl.h> 45 #include <netlink/netlink.h> 46 #include <netlink/netlink_ctl.h> 47 #include <netlink/netlink_route.h> 48 #include <netlink/route/route_var.h> 49 50 #include <netinet6/in6_var.h> /* nd6.h requires this */ 51 #include <netinet6/nd6.h> /* nd6 state machine */ 52 #include <netinet6/scope6_var.h> /* scope deembedding */ 53 54 #define DEBUG_MOD_NAME nl_neigh 55 #define DEBUG_MAX_LEVEL LOG_DEBUG3 56 #include <netlink/netlink_debug.h> 57 _DECLARE_DEBUG(LOG_INFO); 58 59 static int lle_families[] = { AF_INET, AF_INET6 }; 60 61 static eventhandler_tag lle_event_p; 62 63 struct netlink_walkargs { 64 struct nl_writer *nw; 65 struct nlmsghdr hdr; 66 struct nlpcb *so; 67 if_t ifp; 68 int family; 69 int error; 70 int count; 71 int dumped; 72 }; 73 74 static int 75 lle_state_to_nl_state(int family, struct llentry *lle) 76 { 77 int state = lle->ln_state; 78 79 switch (family) { 80 case AF_INET: 81 if (lle->la_flags & (LLE_STATIC | LLE_IFADDR)) 82 state = 1; 83 switch (state) { 84 case 0: /* ARP_LLINFO_INCOMPLETE */ 85 return (NUD_INCOMPLETE); 86 case 1: /* ARP_LLINFO_REACHABLE */ 87 return (NUD_REACHABLE); 88 case 2: /* ARP_LLINFO_VERIFY */ 89 return (NUD_PROBE); 90 } 91 break; 92 case AF_INET6: 93 switch (state) { 94 case ND6_LLINFO_INCOMPLETE: 95 return (NUD_INCOMPLETE); 96 case ND6_LLINFO_REACHABLE: 97 return (NUD_REACHABLE); 98 case ND6_LLINFO_STALE: 99 return (NUD_STALE); 100 case ND6_LLINFO_DELAY: 101 return (NUD_DELAY); 102 case ND6_LLINFO_PROBE: 103 return (NUD_PROBE); 104 } 105 break; 106 } 107 108 return (NUD_NONE); 109 } 110 111 static uint32_t 112 lle_flags_to_nl_flags(const struct llentry *lle) 113 { 114 uint32_t nl_flags = 0; 115 116 if (lle->la_flags & LLE_IFADDR) 117 nl_flags |= NTF_SELF; 118 if (lle->la_flags & LLE_PUB) 119 nl_flags |= NTF_PROXY; 120 if (lle->la_flags & LLE_STATIC) 121 nl_flags |= NTF_STICKY; 122 if (lle->ln_router != 0) 123 nl_flags |= NTF_ROUTER; 124 125 return (nl_flags); 126 } 127 128 static uint32_t 129 get_lle_next_ts(const struct llentry *lle) 130 { 131 if (lle->la_expire == 0) 132 return (0); 133 return (lle->la_expire + lle->lle_remtime / hz + time_second - time_uptime); 134 } 135 136 static int 137 dump_lle_locked(struct llentry *lle, void *arg) 138 { 139 struct netlink_walkargs *wa = (struct netlink_walkargs *)arg; 140 struct nlmsghdr *hdr = &wa->hdr; 141 struct nl_writer *nw = wa->nw; 142 struct ndmsg *ndm; 143 #if defined(INET) || defined(INET6) 144 union { 145 struct in_addr in; 146 struct in6_addr in6; 147 } addr; 148 #endif 149 150 IF_DEBUG_LEVEL(LOG_DEBUG2) { 151 char llebuf[NHOP_PRINT_BUFSIZE]; 152 llentry_print_buf_lltable(lle, llebuf, sizeof(llebuf)); 153 NL_LOG(LOG_DEBUG2, "dumping %s", llebuf); 154 } 155 156 if (!nlmsg_reply(nw, hdr, sizeof(struct ndmsg))) 157 goto enomem; 158 159 ndm = nlmsg_reserve_object(nw, struct ndmsg); 160 ndm->ndm_family = wa->family; 161 ndm->ndm_ifindex = if_getindex(wa->ifp); 162 ndm->ndm_state = lle_state_to_nl_state(wa->family, lle); 163 ndm->ndm_flags = lle_flags_to_nl_flags(lle); 164 165 switch (wa->family) { 166 #ifdef INET 167 case AF_INET: 168 addr.in = lle->r_l3addr.addr4; 169 nlattr_add(nw, NDA_DST, 4, &addr); 170 break; 171 #endif 172 #ifdef INET6 173 case AF_INET6: 174 addr.in6 = lle->r_l3addr.addr6; 175 in6_clearscope(&addr.in6); 176 nlattr_add(nw, NDA_DST, 16, &addr); 177 break; 178 #endif 179 } 180 181 if (lle->r_flags & RLLE_VALID) { 182 /* Has L2 */ 183 int addrlen = if_getaddrlen(wa->ifp); 184 nlattr_add(nw, NDA_LLADDR, addrlen, lle->ll_addr); 185 } 186 187 nlattr_add_u32(nw, NDA_PROBES, lle->la_asked); 188 189 struct nda_cacheinfo *cache; 190 cache = nlmsg_reserve_attr(nw, NDA_CACHEINFO, struct nda_cacheinfo); 191 if (cache == NULL) 192 goto enomem; 193 /* TODO: provide confirmed/updated */ 194 cache->ndm_refcnt = lle->lle_refcnt; 195 196 int off = nlattr_add_nested(nw, NDA_FREEBSD); 197 if (off != 0) { 198 nlattr_add_u32(nw, NDAF_NEXT_STATE_TS, get_lle_next_ts(lle)); 199 200 nlattr_set_len(nw, off); 201 } 202 203 if (nlmsg_end(nw)) 204 return (0); 205 enomem: 206 NL_LOG(LOG_DEBUG, "unable to dump lle state (ENOMEM)"); 207 nlmsg_abort(nw); 208 return (ENOMEM); 209 } 210 211 static int 212 dump_lle(struct lltable *llt, struct llentry *lle, void *arg) 213 { 214 int error; 215 216 LLE_RLOCK(lle); 217 error = dump_lle_locked(lle, arg); 218 LLE_RUNLOCK(lle); 219 return (error); 220 } 221 222 static bool 223 dump_llt(struct lltable *llt, struct netlink_walkargs *wa) 224 { 225 lltable_foreach_lle(llt, dump_lle, wa); 226 227 return (true); 228 } 229 230 static int 231 dump_llts_iface(struct netlink_walkargs *wa, if_t ifp, int family) 232 { 233 int error = 0; 234 235 wa->ifp = ifp; 236 for (int i = 0; i < sizeof(lle_families) / sizeof(int); i++) { 237 int fam = lle_families[i]; 238 struct lltable *llt = lltable_get(ifp, fam); 239 if (llt != NULL && (family == 0 || family == fam)) { 240 wa->count++; 241 wa->family = fam; 242 if (!dump_llt(llt, wa)) { 243 error = ENOMEM; 244 break; 245 } 246 wa->dumped++; 247 } 248 } 249 return (error); 250 } 251 252 static int 253 dump_llts(struct netlink_walkargs *wa, if_t ifp, int family) 254 { 255 NL_LOG(LOG_DEBUG2, "Start dump ifp=%s family=%d", ifp ? if_name(ifp) : "NULL", family); 256 257 wa->hdr.nlmsg_flags |= NLM_F_MULTI; 258 259 if (ifp != NULL) { 260 dump_llts_iface(wa, ifp, family); 261 } else { 262 struct if_iter it; 263 264 for (ifp = if_iter_start(&it); ifp != NULL; ifp = if_iter_next(&it)) { 265 dump_llts_iface(wa, ifp, family); 266 } 267 if_iter_finish(&it); 268 } 269 270 NL_LOG(LOG_DEBUG2, "End dump, iterated %d dumped %d", wa->count, wa->dumped); 271 272 if (!nlmsg_end_dump(wa->nw, wa->error, &wa->hdr)) { 273 NL_LOG(LOG_DEBUG, "Unable to add new message"); 274 return (ENOMEM); 275 } 276 277 return (0); 278 } 279 280 static int 281 get_lle(struct netlink_walkargs *wa, if_t ifp, int family, struct sockaddr *dst) 282 { 283 struct lltable *llt = lltable_get(ifp, family); 284 if (llt == NULL) 285 return (ESRCH); 286 287 struct llentry *lle = lla_lookup(llt, LLE_UNLOCKED, dst); 288 if (lle == NULL) 289 return (ESRCH); 290 291 wa->ifp = ifp; 292 wa->family = family; 293 294 return (dump_lle(llt, lle, wa)); 295 } 296 297 static void 298 set_scope6(struct sockaddr *sa, if_t ifp) 299 { 300 #ifdef INET6 301 if (sa != NULL && sa->sa_family == AF_INET6 && ifp != NULL) { 302 struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)sa; 303 304 if (IN6_IS_ADDR_LINKLOCAL(&sa6->sin6_addr)) 305 in6_set_unicast_scopeid(&sa6->sin6_addr, if_getindex(ifp)); 306 } 307 #endif 308 } 309 310 struct nl_parsed_neigh { 311 struct sockaddr *nda_dst; 312 struct ifnet *nda_ifp; 313 struct nlattr *nda_lladdr; 314 uint32_t ndaf_next_ts; 315 uint32_t ndm_flags; 316 uint16_t ndm_state; 317 uint8_t ndm_family; 318 }; 319 320 #define _IN(_field) offsetof(struct ndmsg, _field) 321 #define _OUT(_field) offsetof(struct nl_parsed_neigh, _field) 322 static const struct nlattr_parser nla_p_neigh_fbsd[] = { 323 { .type = NDAF_NEXT_STATE_TS, .off = _OUT(ndaf_next_ts), .cb = nlattr_get_uint32 }, 324 }; 325 NL_DECLARE_ATTR_PARSER(neigh_fbsd_parser, nla_p_neigh_fbsd); 326 327 static const struct nlfield_parser nlf_p_neigh[] = { 328 { .off_in = _IN(ndm_family), .off_out = _OUT(ndm_family), .cb = nlf_get_u8 }, 329 { .off_in = _IN(ndm_flags), .off_out = _OUT(ndm_flags), .cb = nlf_get_u8_u32 }, 330 { .off_in = _IN(ndm_state), .off_out = _OUT(ndm_state), .cb = nlf_get_u16 }, 331 { .off_in = _IN(ndm_ifindex), .off_out = _OUT(nda_ifp), .cb = nlf_get_ifpz }, 332 }; 333 334 static const struct nlattr_parser nla_p_neigh[] = { 335 { .type = NDA_DST, .off = _OUT(nda_dst), .cb = nlattr_get_ip }, 336 { .type = NDA_LLADDR, .off = _OUT(nda_lladdr), .cb = nlattr_get_nla }, 337 { .type = NDA_IFINDEX, .off = _OUT(nda_ifp), .cb = nlattr_get_ifp }, 338 { .type = NDA_FLAGS_EXT, .off = _OUT(ndm_flags), .cb = nlattr_get_uint32 }, 339 { .type = NDA_FREEBSD, .arg = &neigh_fbsd_parser, .cb = nlattr_get_nested }, 340 }; 341 #undef _IN 342 #undef _OUT 343 344 static bool 345 post_p_neigh(void *_attrs, struct nl_pstate *npt __unused) 346 { 347 struct nl_parsed_neigh *attrs = (struct nl_parsed_neigh *)_attrs; 348 349 set_scope6(attrs->nda_dst, attrs->nda_ifp); 350 return (true); 351 } 352 NL_DECLARE_PARSER_EXT(ndmsg_parser, struct ndmsg, NULL, nlf_p_neigh, nla_p_neigh, post_p_neigh); 353 354 355 /* 356 * type=RTM_NEWNEIGH, flags=NLM_F_REQUEST|NLM_F_ACK|NLM_F_EXCL|NLM_F_CREATE, seq=1661941473, pid=0}, 357 * {ndm_family=AF_INET6, ndm_ifindex=if_nametoindex("enp0s31f6"), ndm_state=NUD_PERMANENT, ndm_flags=0, ndm_type=RTN_UNSPEC}, 358 * [ 359 * {{nla_len=20, nla_type=NDA_DST}, inet_pton(AF_INET6, "2a01:4f8:13a:70c::3")}, 360 * {{nla_len=10, nla_type=NDA_LLADDR}, 20:4e:71:62:ae:f2}]}, iov_len=60} 361 */ 362 363 static int 364 rtnl_handle_newneigh(struct nlmsghdr *hdr, struct nlpcb *nlp, struct nl_pstate *npt) 365 { 366 int error; 367 368 struct nl_parsed_neigh attrs = {}; 369 error = nl_parse_nlmsg(hdr, &ndmsg_parser, npt, &attrs); 370 if (error != 0) 371 return (error); 372 373 if (attrs.nda_ifp == NULL || attrs.nda_dst == NULL || attrs.nda_lladdr == NULL) { 374 if (attrs.nda_ifp == NULL) 375 NLMSG_REPORT_ERR_MSG(npt, "NDA_IFINDEX / ndm_ifindex not set"); 376 if (attrs.nda_dst == NULL) 377 NLMSG_REPORT_ERR_MSG(npt, "NDA_DST not set"); 378 if (attrs.nda_lladdr == NULL) 379 NLMSG_REPORT_ERR_MSG(npt, "NDA_LLADDR not set"); 380 return (EINVAL); 381 } 382 383 if (attrs.nda_dst->sa_family != attrs.ndm_family) { 384 NLMSG_REPORT_ERR_MSG(npt, 385 "NDA_DST family (%d) is different from ndm_family (%d)", 386 attrs.nda_dst->sa_family, attrs.ndm_family); 387 return (EINVAL); 388 } 389 390 int addrlen = if_getaddrlen(attrs.nda_ifp); 391 if (attrs.nda_lladdr->nla_len != sizeof(struct nlattr) + addrlen) { 392 NLMSG_REPORT_ERR_MSG(npt, 393 "NDA_LLADDR address length (%d) is different from expected (%d)", 394 (int)attrs.nda_lladdr->nla_len - (int)sizeof(struct nlattr), addrlen); 395 return (EINVAL); 396 } 397 398 const uint16_t supported_flags = NTF_PROXY | NTF_STICKY; 399 if ((attrs.ndm_flags & supported_flags) != attrs.ndm_flags) { 400 NLMSG_REPORT_ERR_MSG(npt, "ndm_flags %X not supported", 401 attrs.ndm_flags &~ supported_flags); 402 return (ENOTSUP); 403 } 404 405 /* Replacement requires new entry creation anyway */ 406 if ((hdr->nlmsg_flags & (NLM_F_CREATE | NLM_F_REPLACE)) == 0) 407 return (ENOTSUP); 408 409 struct lltable *llt = lltable_get(attrs.nda_ifp, attrs.ndm_family); 410 if (llt == NULL) 411 return (EAFNOSUPPORT); 412 413 414 uint8_t linkhdr[LLE_MAX_LINKHDR]; 415 size_t linkhdrsize = sizeof(linkhdr); 416 int lladdr_off = 0; 417 if (lltable_calc_llheader(attrs.nda_ifp, attrs.ndm_family, 418 (char *)(attrs.nda_lladdr + 1), linkhdr, &linkhdrsize, &lladdr_off) != 0) { 419 NLMSG_REPORT_ERR_MSG(npt, "unable to calculate lle prepend data"); 420 return (EINVAL); 421 } 422 423 int lle_flags = (attrs.ndm_flags & NTF_PROXY) ? LLE_PUB : 0; 424 if (attrs.ndm_flags & NTF_STICKY) 425 lle_flags |= LLE_STATIC; 426 struct llentry *lle = lltable_alloc_entry(llt, lle_flags, attrs.nda_dst); 427 if (lle == NULL) 428 return (ENOMEM); 429 lltable_set_entry_addr(attrs.nda_ifp, lle, linkhdr, linkhdrsize, lladdr_off); 430 431 if (attrs.ndm_flags & NTF_STICKY) 432 lle->la_expire = 0; 433 else 434 lle->la_expire = attrs.ndaf_next_ts - time_second + time_uptime; 435 436 /* llentry created, try to insert or update */ 437 IF_AFDATA_WLOCK(attrs.nda_ifp); 438 LLE_WLOCK(lle); 439 struct llentry *lle_tmp = lla_lookup(llt, LLE_EXCLUSIVE, attrs.nda_dst); 440 if (lle_tmp != NULL) { 441 error = EEXIST; 442 if (hdr->nlmsg_flags & NLM_F_EXCL) { 443 LLE_WUNLOCK(lle_tmp); 444 lle_tmp = NULL; 445 } else if (hdr->nlmsg_flags & NLM_F_REPLACE) { 446 if ((lle_tmp->la_flags & LLE_IFADDR) == 0) { 447 lltable_unlink_entry(llt, lle_tmp); 448 lltable_link_entry(llt, lle); 449 error = 0; 450 } else 451 error = EPERM; 452 } 453 } else { 454 if (hdr->nlmsg_flags & NLM_F_CREATE) 455 lltable_link_entry(llt, lle); 456 else 457 error = ENOENT; 458 } 459 IF_AFDATA_WUNLOCK(attrs.nda_ifp); 460 461 if (error != 0) { 462 if (lle != NULL) 463 llentry_free(lle); 464 return (error); 465 } 466 467 if (lle_tmp != NULL) 468 llentry_free(lle_tmp); 469 470 /* XXX: We're inside epoch */ 471 EVENTHANDLER_INVOKE(lle_event, lle, LLENTRY_RESOLVED); 472 LLE_WUNLOCK(lle); 473 llt->llt_post_resolved(llt, lle); 474 475 return (0); 476 } 477 478 static int 479 rtnl_handle_delneigh(struct nlmsghdr *hdr, struct nlpcb *nlp, struct nl_pstate *npt) 480 { 481 int error; 482 483 struct nl_parsed_neigh attrs = {}; 484 error = nl_parse_nlmsg(hdr, &ndmsg_parser, npt, &attrs); 485 if (error != 0) 486 return (error); 487 488 if (attrs.nda_dst == NULL) { 489 NLMSG_REPORT_ERR_MSG(npt, "NDA_DST not set"); 490 return (EINVAL); 491 } 492 493 if (attrs.nda_ifp == NULL) { 494 NLMSG_REPORT_ERR_MSG(npt, "no ifindex provided"); 495 return (EINVAL); 496 } 497 498 struct lltable *llt = lltable_get(attrs.nda_ifp, attrs.ndm_family); 499 if (llt == NULL) 500 return (EAFNOSUPPORT); 501 502 return (lltable_delete_addr(llt, 0, attrs.nda_dst)); 503 } 504 505 static int 506 rtnl_handle_getneigh(struct nlmsghdr *hdr, struct nlpcb *nlp, struct nl_pstate *npt) 507 { 508 int error; 509 510 struct nl_parsed_neigh attrs = {}; 511 error = nl_parse_nlmsg(hdr, &ndmsg_parser, npt, &attrs); 512 if (error != 0) 513 return (error); 514 515 if (attrs.nda_dst != NULL && attrs.nda_ifp == NULL) { 516 NLMSG_REPORT_ERR_MSG(npt, "has NDA_DST but no ifindex provided"); 517 return (EINVAL); 518 } 519 520 struct netlink_walkargs wa = { 521 .so = nlp, 522 .nw = npt->nw, 523 .hdr.nlmsg_pid = hdr->nlmsg_pid, 524 .hdr.nlmsg_seq = hdr->nlmsg_seq, 525 .hdr.nlmsg_flags = hdr->nlmsg_flags, 526 .hdr.nlmsg_type = NL_RTM_NEWNEIGH, 527 }; 528 529 if (attrs.nda_dst == NULL) 530 error = dump_llts(&wa, attrs.nda_ifp, attrs.ndm_family); 531 else 532 error = get_lle(&wa, attrs.nda_ifp, attrs.ndm_family, attrs.nda_dst); 533 534 return (error); 535 } 536 537 static const struct rtnl_cmd_handler cmd_handlers[] = { 538 { 539 .cmd = NL_RTM_NEWNEIGH, 540 .name = "RTM_NEWNEIGH", 541 .cb = &rtnl_handle_newneigh, 542 .priv = PRIV_NET_ROUTE, 543 }, 544 { 545 .cmd = NL_RTM_DELNEIGH, 546 .name = "RTM_DELNEIGH", 547 .cb = &rtnl_handle_delneigh, 548 .priv = PRIV_NET_ROUTE, 549 }, 550 { 551 .cmd = NL_RTM_GETNEIGH, 552 .name = "RTM_GETNEIGH", 553 .cb = &rtnl_handle_getneigh, 554 } 555 }; 556 557 static void 558 rtnl_lle_event(void *arg __unused, struct llentry *lle, int evt) 559 { 560 if_t ifp; 561 int family; 562 563 LLE_WLOCK_ASSERT(lle); 564 565 ifp = lltable_get_ifp(lle->lle_tbl); 566 family = lltable_get_af(lle->lle_tbl); 567 568 if (family != AF_INET && family != AF_INET6) 569 return; 570 571 int nlmsgs_type = evt == LLENTRY_RESOLVED ? NL_RTM_NEWNEIGH : NL_RTM_DELNEIGH; 572 573 struct nl_writer nw = {}; 574 if (!nlmsg_get_group_writer(&nw, NLMSG_SMALL, NETLINK_ROUTE, RTNLGRP_NEIGH)) { 575 NL_LOG(LOG_DEBUG, "error allocating group writer"); 576 return; 577 } 578 579 struct netlink_walkargs wa = { 580 .hdr.nlmsg_type = nlmsgs_type, 581 .nw = &nw, 582 .ifp = ifp, 583 .family = family, 584 }; 585 586 dump_lle_locked(lle, &wa); 587 nlmsg_flush(&nw); 588 } 589 590 static const struct nlhdr_parser *all_parsers[] = { &ndmsg_parser, &neigh_fbsd_parser }; 591 592 void 593 rtnl_neighs_init(void) 594 { 595 NL_VERIFY_PARSERS(all_parsers); 596 rtnl_register_messages(cmd_handlers, NL_ARRAY_LEN(cmd_handlers)); 597 lle_event_p = EVENTHANDLER_REGISTER(lle_event, rtnl_lle_event, NULL, 598 EVENTHANDLER_PRI_ANY); 599 } 600 601 void 602 rtnl_neighs_destroy(void) 603 { 604 EVENTHANDLER_DEREGISTER(lle_event, lle_event_p); 605 } 606