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