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