Home | History | Annotate | Line # | Download | only in common
      1 /*	$NetBSD: lpf.c,v 1.4 2022/04/03 01:10:58 christos Exp $	*/
      2 
      3 /* lpf.c
      4 
      5    Linux packet filter code, contributed by Brian Murrel at Interlinx
      6    Support Services in Vancouver, B.C. */
      7 
      8 /*
      9  * Copyright (C) 2004-2022 Internet Systems Consortium, Inc. ("ISC")
     10  * Copyright (c) 1996-2003 by Internet Software Consortium
     11  *
     12  * This Source Code Form is subject to the terms of the Mozilla Public
     13  * License, v. 2.0. If a copy of the MPL was not distributed with this
     14  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
     15  *
     16  * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
     17  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     18  * MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR
     19  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     20  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     21  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
     22  * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     23  *
     24  *   Internet Systems Consortium, Inc.
     25  *   PO Box 360
     26  *   Newmarket, NH 03857 USA
     27  *   <info (at) isc.org>
     28  *   https://www.isc.org/
     29  */
     30 
     31 #include <sys/cdefs.h>
     32 __RCSID("$NetBSD: lpf.c,v 1.4 2022/04/03 01:10:58 christos Exp $");
     33 
     34 #include "dhcpd.h"
     35 #if defined (USE_LPF_SEND) || defined (USE_LPF_RECEIVE)
     36 #include <sys/uio.h>
     37 #include <errno.h>
     38 
     39 #include <asm/types.h>
     40 #include <linux/filter.h>
     41 #include <linux/if_ether.h>
     42 #include <linux/if_packet.h>
     43 #include <netinet/in_systm.h>
     44 #include "includes/netinet/ip.h"
     45 #include "includes/netinet/udp.h"
     46 #include "includes/netinet/if_ether.h"
     47 #endif
     48 
     49 #if defined (USE_LPF_RECEIVE) || defined (USE_LPF_HWADDR)
     50 #include <sys/ioctl.h>
     51 #include <sys/socket.h>
     52 #include <net/if.h>
     53 #endif
     54 
     55 #if defined (USE_LPF_SEND) || defined (USE_LPF_RECEIVE)
     56 /* Reinitializes the specified interface after an address change.   This
     57    is not required for packet-filter APIs. */
     58 
     59 #ifdef USE_LPF_SEND
     60 void if_reinitialize_send (info)
     61 	struct interface_info *info;
     62 {
     63 }
     64 #endif
     65 
     66 #ifdef USE_LPF_RECEIVE
     67 void if_reinitialize_receive (info)
     68 	struct interface_info *info;
     69 {
     70 }
     71 #endif
     72 
     73 /* Called by get_interface_list for each interface that's discovered.
     74    Opens a packet filter for each interface and adds it to the select
     75    mask. */
     76 
     77 int if_register_lpf (info)
     78 	struct interface_info *info;
     79 {
     80 	int sock;
     81 	union {
     82 		struct sockaddr_ll ll;
     83 		struct sockaddr common;
     84 		} sa;
     85 	struct ifreq ifr;
     86 
     87 	/* Make an LPF socket. */
     88 	if ((sock = socket(PF_PACKET, SOCK_RAW,
     89 			   htons((short)ETH_P_ALL))) < 0) {
     90 		if (errno == ENOPROTOOPT || errno == EPROTONOSUPPORT ||
     91 		    errno == ESOCKTNOSUPPORT || errno == EPFNOSUPPORT ||
     92 		    errno == EAFNOSUPPORT || errno == EINVAL) {
     93 			log_error ("socket: %m - make sure");
     94 			log_error ("CONFIG_PACKET (Packet socket) %s",
     95 				   "and CONFIG_FILTER");
     96 			log_error ("(Socket Filtering) are enabled %s",
     97 				   "in your kernel");
     98 			log_fatal ("configuration!");
     99 		}
    100 		log_fatal ("Open a socket for LPF: %m");
    101 	}
    102 
    103 	memset (&ifr, 0, sizeof ifr);
    104 	strncpy (ifr.ifr_name, (const char *)info -> ifp, sizeof ifr.ifr_name);
    105 	ifr.ifr_name[IFNAMSIZ-1] = '\0';
    106 	if (ioctl (sock, SIOCGIFINDEX, &ifr))
    107 		log_fatal ("Failed to get interface index: %m");
    108 
    109 	/* Bind to the interface name */
    110 	memset (&sa, 0, sizeof sa);
    111 	sa.ll.sll_family = AF_PACKET;
    112 	sa.ll.sll_ifindex = ifr.ifr_ifindex;
    113 	if (bind (sock, &sa.common, sizeof sa)) {
    114 		if (errno == ENOPROTOOPT || errno == EPROTONOSUPPORT ||
    115 		    errno == ESOCKTNOSUPPORT || errno == EPFNOSUPPORT ||
    116 		    errno == EAFNOSUPPORT || errno == EINVAL) {
    117 			log_error ("socket: %m - make sure");
    118 			log_error ("CONFIG_PACKET (Packet socket) %s",
    119 				   "and CONFIG_FILTER");
    120 			log_error ("(Socket Filtering) are enabled %s",
    121 				   "in your kernel");
    122 			log_fatal ("configuration!");
    123 		}
    124 		log_fatal ("Bind socket to interface: %m");
    125 
    126 	}
    127 
    128 	get_hw_addr(info->name, &info->hw_address);
    129 
    130 	return sock;
    131 }
    132 #endif /* USE_LPF_SEND || USE_LPF_RECEIVE */
    133 
    134 #ifdef USE_LPF_SEND
    135 void if_register_send (info)
    136 	struct interface_info *info;
    137 {
    138 	/* If we're using the lpf API for sending and receiving,
    139 	   we don't need to register this interface twice. */
    140 #ifndef USE_LPF_RECEIVE
    141 	info -> wfdesc = if_register_lpf (info);
    142 #else
    143 	info -> wfdesc = info -> rfdesc;
    144 #endif
    145 	if (!quiet_interface_discovery)
    146 		log_info ("Sending on   LPF/%s/%s%s%s",
    147 		      info -> name,
    148 		      print_hw_addr (info -> hw_address.hbuf [0],
    149 				     info -> hw_address.hlen - 1,
    150 				     &info -> hw_address.hbuf [1]),
    151 		      (info -> shared_network ? "/" : ""),
    152 		      (info -> shared_network ?
    153 		       info -> shared_network -> name : ""));
    154 }
    155 
    156 void if_deregister_send (info)
    157 	struct interface_info *info;
    158 {
    159 	/* don't need to close twice if we are using lpf for sending and
    160 	   receiving */
    161 #ifndef USE_LPF_RECEIVE
    162 	/* for LPF this is simple, packet filters are removed when sockets
    163 	   are closed */
    164 	close (info -> wfdesc);
    165 #endif
    166 	info -> wfdesc = -1;
    167 	if (!quiet_interface_discovery)
    168 		log_info ("Disabling output on LPF/%s/%s%s%s",
    169 		      info -> name,
    170 		      print_hw_addr (info -> hw_address.hbuf [0],
    171 				     info -> hw_address.hlen - 1,
    172 				     &info -> hw_address.hbuf [1]),
    173 		      (info -> shared_network ? "/" : ""),
    174 		      (info -> shared_network ?
    175 		       info -> shared_network -> name : ""));
    176 }
    177 #endif /* USE_LPF_SEND */
    178 
    179 #ifdef USE_LPF_RECEIVE
    180 /* Defined in bpf.c.   We can't extern these in dhcpd.h without pulling
    181    in bpf includes... */
    182 extern struct sock_filter dhcp_bpf_filter [];
    183 extern int dhcp_bpf_filter_len;
    184 
    185 #if defined(RELAY_PORT)
    186 extern struct sock_filter dhcp_bpf_relay_filter [];
    187 extern int dhcp_bpf_relay_filter_len;
    188 #endif
    189 
    190 #if defined (HAVE_TR_SUPPORT)
    191 extern struct sock_filter dhcp_bpf_tr_filter [];
    192 extern int dhcp_bpf_tr_filter_len;
    193 static void lpf_tr_filter_setup (struct interface_info *);
    194 #endif
    195 
    196 static void lpf_gen_filter_setup (struct interface_info *);
    197 
    198 void if_register_receive (info)
    199 	struct interface_info *info;
    200 {
    201 	/* Open a LPF device and hang it on this interface... */
    202 	info -> rfdesc = if_register_lpf (info);
    203 
    204 #ifdef PACKET_AUXDATA
    205 	{
    206 	int val = 1;
    207 
    208 	if (setsockopt(info->rfdesc, SOL_PACKET, PACKET_AUXDATA,
    209 		       &val, sizeof(val)) < 0) {
    210 		if (errno != ENOPROTOOPT) {
    211 			log_fatal ("Failed to set auxiliary packet data: %m");
    212 		}
    213 	}
    214 	}
    215 #endif
    216 
    217 
    218 #if defined (HAVE_TR_SUPPORT)
    219 	if (info -> hw_address.hbuf [0] == HTYPE_IEEE802)
    220 		lpf_tr_filter_setup (info);
    221 	else
    222 #endif
    223 		lpf_gen_filter_setup (info);
    224 
    225 	if (!quiet_interface_discovery)
    226 		log_info ("Listening on LPF/%s/%s%s%s",
    227 			  info -> name,
    228 			  print_hw_addr (info -> hw_address.hbuf [0],
    229 					 info -> hw_address.hlen - 1,
    230 					 &info -> hw_address.hbuf [1]),
    231 			  (info -> shared_network ? "/" : ""),
    232 			  (info -> shared_network ?
    233 			   info -> shared_network -> name : ""));
    234 }
    235 
    236 void if_deregister_receive (info)
    237 	struct interface_info *info;
    238 {
    239 	/* for LPF this is simple, packet filters are removed when sockets
    240 	   are closed */
    241 	close (info -> rfdesc);
    242 	info -> rfdesc = -1;
    243 	if (!quiet_interface_discovery)
    244 		log_info ("Disabling input on LPF/%s/%s%s%s",
    245 			  info -> name,
    246 			  print_hw_addr (info -> hw_address.hbuf [0],
    247 					 info -> hw_address.hlen - 1,
    248 					 &info -> hw_address.hbuf [1]),
    249 			  (info -> shared_network ? "/" : ""),
    250 			  (info -> shared_network ?
    251 			   info -> shared_network -> name : ""));
    252 }
    253 
    254 static void lpf_gen_filter_setup (info)
    255 	struct interface_info *info;
    256 {
    257 	struct sock_fprog p;
    258 
    259 	memset(&p, 0, sizeof(p));
    260 
    261 	/* Set up the bpf filter program structure.    This is defined in
    262 	   bpf.c */
    263 	p.len = dhcp_bpf_filter_len;
    264 	p.filter = dhcp_bpf_filter;
    265 
    266         /* Patch the server port into the LPF  program...
    267 	   XXX changes to filter program may require changes
    268 	   to the insn number(s) used below! XXX */
    269 #if defined(RELAY_PORT)
    270 	if (relay_port) {
    271 		/*
    272 		 * If user defined relay UDP port, we need to filter
    273 		 * also on the user UDP port.
    274 		 */
    275 		p.len = dhcp_bpf_relay_filter_len;
    276 		p.filter = dhcp_bpf_relay_filter;
    277 
    278 		dhcp_bpf_relay_filter [10].k = ntohs (relay_port);
    279 	}
    280 #endif
    281 	dhcp_bpf_filter [8].k = ntohs (*libdhcp_callbacks.local_port);
    282 
    283 	if (setsockopt (info -> rfdesc, SOL_SOCKET, SO_ATTACH_FILTER, &p,
    284 			sizeof p) < 0) {
    285 		if (errno == ENOPROTOOPT || errno == EPROTONOSUPPORT ||
    286 		    errno == ESOCKTNOSUPPORT || errno == EPFNOSUPPORT ||
    287 		    errno == EAFNOSUPPORT) {
    288 			log_error ("socket: %m - make sure");
    289 			log_error ("CONFIG_PACKET (Packet socket) %s",
    290 				   "and CONFIG_FILTER");
    291 			log_error ("(Socket Filtering) are enabled %s",
    292 				   "in your kernel");
    293 			log_fatal ("configuration!");
    294 		}
    295 		log_fatal ("Can't install packet filter program: %m");
    296 	}
    297 }
    298 
    299 #if defined (HAVE_TR_SUPPORT)
    300 static void lpf_tr_filter_setup (info)
    301 	struct interface_info *info;
    302 {
    303 	struct sock_fprog p;
    304 
    305 	memset(&p, 0, sizeof(p));
    306 
    307 	/* Set up the bpf filter program structure.    This is defined in
    308 	   bpf.c */
    309 	p.len = dhcp_bpf_tr_filter_len;
    310 	p.filter = dhcp_bpf_tr_filter;
    311 
    312         /* Patch the server port into the LPF  program...
    313 	   XXX changes to filter program may require changes
    314 	   XXX to the insn number(s) used below!
    315 	   XXX Token ring filter is null - when/if we have a filter
    316 	   XXX that's not, we'll need this code.
    317 	   XXX dhcp_bpf_filter [?].k = ntohs (*libdhcp_callbacks.local_port); */
    318 
    319 	if (setsockopt (info -> rfdesc, SOL_SOCKET, SO_ATTACH_FILTER, &p,
    320 			sizeof p) < 0) {
    321 		if (errno == ENOPROTOOPT || errno == EPROTONOSUPPORT ||
    322 		    errno == ESOCKTNOSUPPORT || errno == EPFNOSUPPORT ||
    323 		    errno == EAFNOSUPPORT) {
    324 			log_error ("socket: %m - make sure");
    325 			log_error ("CONFIG_PACKET (Packet socket) %s",
    326 				   "and CONFIG_FILTER");
    327 			log_error ("(Socket Filtering) are enabled %s",
    328 				   "in your kernel");
    329 			log_fatal ("configuration!");
    330 		}
    331 		log_fatal ("Can't install packet filter program: %m");
    332 	}
    333 }
    334 #endif /* HAVE_TR_SUPPORT */
    335 #endif /* USE_LPF_RECEIVE */
    336 
    337 #ifdef USE_LPF_SEND
    338 ssize_t send_packet (interface, packet, raw, len, from, to, hto)
    339 	struct interface_info *interface;
    340 	struct packet *packet;
    341 	struct dhcp_packet *raw;
    342 	size_t len;
    343 	struct in_addr from;
    344 	struct sockaddr_in *to;
    345 	struct hardware *hto;
    346 {
    347 	unsigned hbufp = 0, ibufp = 0;
    348 	double hh [16];
    349 	double ih [1536 / sizeof (double)];
    350 	unsigned char *buf = (unsigned char *)ih;
    351 	int result;
    352 	int fudge;
    353 
    354 	if (!strcmp (interface -> name, "fallback"))
    355 		return send_fallback (interface, packet, raw,
    356 				      len, from, to, hto);
    357 
    358 	if (hto == NULL && interface->anycast_mac_addr.hlen)
    359 		hto = &interface->anycast_mac_addr;
    360 
    361 	/* Assemble the headers... */
    362 	assemble_hw_header (interface, (unsigned char *)hh, &hbufp, hto);
    363 	fudge = hbufp % 4;	/* IP header must be word-aligned. */
    364 	memcpy (buf + fudge, (unsigned char *)hh, hbufp);
    365 	ibufp = hbufp + fudge;
    366 	assemble_udp_ip_header (interface, buf, &ibufp, from.s_addr,
    367 				to -> sin_addr.s_addr, to -> sin_port,
    368 				(unsigned char *)raw, len);
    369 	memcpy (buf + ibufp, raw, len);
    370 	result = write(interface->wfdesc, buf + fudge, ibufp + len - fudge);
    371 	if (result < 0)
    372 		log_error ("send_packet: %m");
    373 	return result;
    374 }
    375 #endif /* USE_LPF_SEND */
    376 
    377 #ifdef USE_LPF_RECEIVE
    378 ssize_t receive_packet (interface, buf, len, from, hfrom)
    379 	struct interface_info *interface;
    380 	unsigned char *buf;
    381 	size_t len;
    382 	struct sockaddr_in *from;
    383 	struct hardware *hfrom;
    384 {
    385 	int length = 0;
    386 	int offset = 0;
    387 	int csum_ready = 1;
    388 	unsigned char ibuf [1536];
    389 	unsigned bufix = 0;
    390 	unsigned paylen;
    391 	struct iovec iov = {
    392 		.iov_base = ibuf,
    393 		.iov_len = sizeof ibuf,
    394 	};
    395 #ifdef PACKET_AUXDATA
    396 	/*
    397 	 * We only need cmsgbuf if we are getting the aux data and we
    398 	 * only get the auxdata if it is actually defined
    399 	 */
    400 	unsigned char cmsgbuf[CMSG_LEN(sizeof(struct tpacket_auxdata))];
    401 	struct msghdr msg = {
    402 		.msg_iov = &iov,
    403 		.msg_iovlen = 1,
    404 		.msg_control = cmsgbuf,
    405 		.msg_controllen = sizeof(cmsgbuf),
    406 	};
    407 #else
    408 	struct msghdr msg = {
    409 		.msg_iov = &iov,
    410 		.msg_iovlen = 1,
    411 		.msg_control = NULL,
    412 		.msg_controllen = 0,
    413 	};
    414 #endif /* PACKET_AUXDATA */
    415 
    416 	length = recvmsg (interface->rfdesc, &msg, 0);
    417 	if (length <= 0)
    418 		return length;
    419 
    420 #ifdef PACKET_AUXDATA
    421 	{
    422 	/*  Use auxiliary packet data to:
    423 	 *
    424 	 *  a. Weed out extraneous VLAN-tagged packets - If the NIC driver is
    425 	 *  handling VLAN encapsulation (i.e. stripping/adding VLAN tags),
    426 	 *  then an inbound VLAN packet will be seen twice: Once by
    427 	 *  the parent interface (e.g. eth0) with a VLAN tag != 0; and once
    428 	 *  by the vlan interface (e.g. eth0.n) with a VLAN tag of 0 (i.e none).
    429 	 *  We want to discard the packet sent to the parent and thus respond
    430 	 *  only over the vlan interface.  (Drivers for Intel PRO/1000 series
    431 	 *  NICs perform VLAN encapsulation, while drivers for PCnet series
    432 	 *  do not, for example. The linux kernel makes stripped vlan info
    433 	 *  visible to user space via CMSG/auxdata, this appears to not be
    434 	 *  true for BSD OSs.).  NOTE: this is only supported on linux flavors
    435 	 *  which define the tpacket_auxdata.tp_vlan_tci.
    436 	 *
    437 	 *  b. Determine if checksum is valid for use. It may not be if
    438 	 *  checksum offloading is enabled on the interface.  */
    439 	struct cmsghdr *cmsg;
    440 
    441 	for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
    442 		if (cmsg->cmsg_level == SOL_PACKET &&
    443 		    cmsg->cmsg_type == PACKET_AUXDATA) {
    444 			struct tpacket_auxdata *aux = (void *)CMSG_DATA(cmsg);
    445 #ifdef VLAN_TCI_PRESENT
    446 			/* Discard packets with stripped vlan id */
    447 			/* VLAN ID is only bottom 12-bits of TCI */
    448 			if (aux->tp_vlan_tci & 0x0fff)
    449 				return 0;
    450 #endif
    451 
    452 			csum_ready = ((aux->tp_status & TP_STATUS_CSUMNOTREADY)
    453 				      ? 0 : 1);
    454 		}
    455 	}
    456 
    457 	}
    458 #endif /* PACKET_AUXDATA */
    459 
    460 	bufix = 0;
    461 	/* Decode the physical header... */
    462 	offset = decode_hw_header (interface, ibuf, bufix, hfrom);
    463 
    464 	/* If a physical layer checksum failed (dunno of any
    465 	   physical layer that supports this, but WTH), skip this
    466 	   packet. */
    467 	if (offset < 0) {
    468 		return 0;
    469 	}
    470 
    471 	bufix += offset;
    472 	length -= offset;
    473 
    474 	/* Decode the IP and UDP headers... */
    475 	offset = decode_udp_ip_header (interface, ibuf, bufix, from,
    476 				       (unsigned)length, &paylen, csum_ready);
    477 
    478 	/* If the IP or UDP checksum was bad, skip the packet... */
    479 	if (offset < 0)
    480 		return 0;
    481 
    482 	bufix += offset;
    483 	length -= offset;
    484 
    485 	if (length < paylen)
    486 		log_fatal("Internal inconsistency at %s:%d.", MDL);
    487 
    488 	/* Copy out the data in the packet... */
    489 	memcpy(buf, &ibuf[bufix], paylen);
    490 	return paylen;
    491 }
    492 
    493 int can_unicast_without_arp (ip)
    494 	struct interface_info *ip;
    495 {
    496 	return 1;
    497 }
    498 
    499 int can_receive_unicast_unconfigured (ip)
    500 	struct interface_info *ip;
    501 {
    502 	return 1;
    503 }
    504 
    505 int supports_multiple_interfaces (ip)
    506 	struct interface_info *ip;
    507 {
    508 	return 1;
    509 }
    510 
    511 void maybe_setup_fallback ()
    512 {
    513 	isc_result_t status;
    514 	struct interface_info *fbi = (struct interface_info *)0;
    515 	if (setup_fallback (&fbi, MDL)) {
    516 		if_register_fallback (fbi);
    517 		status = omapi_register_io_object ((omapi_object_t *)fbi,
    518 						   if_readsocket, 0,
    519 						   fallback_discard, 0, 0);
    520 		if (status != ISC_R_SUCCESS)
    521 			log_fatal ("Can't register I/O handle for \"%s\": %s",
    522 				   fbi -> name, isc_result_totext (status));
    523 		interface_dereference (&fbi, MDL);
    524 	}
    525 }
    526 #endif
    527 
    528 #if defined (USE_LPF_RECEIVE) || defined (USE_LPF_HWADDR)
    529 void
    530 get_hw_addr(const char *name, struct hardware *hw) {
    531 	int sock;
    532 	struct ifreq tmp;
    533 	struct sockaddr *sa;
    534 
    535 	if (strlen(name) >= sizeof(tmp.ifr_name)) {
    536 		log_fatal("Device name too long: \"%s\"", name);
    537 	}
    538 
    539 	sock = socket(AF_INET, SOCK_DGRAM, 0);
    540 	if (sock < 0) {
    541 		log_fatal("Can't create socket for \"%s\": %m", name);
    542 	}
    543 
    544 	memset(&tmp, 0, sizeof(tmp));
    545 	strcpy(tmp.ifr_name, name);
    546 	if (ioctl(sock, SIOCGIFHWADDR, &tmp) < 0) {
    547 		log_fatal("Error getting hardware address for \"%s\": %m",
    548 			  name);
    549 	}
    550 
    551 	sa = &tmp.ifr_hwaddr;
    552 	switch (sa->sa_family) {
    553 		case ARPHRD_ETHER:
    554 			hw->hlen = 7;
    555 			hw->hbuf[0] = HTYPE_ETHER;
    556 			memcpy(&hw->hbuf[1], sa->sa_data, 6);
    557 			break;
    558 		case ARPHRD_IEEE802:
    559 #ifdef ARPHRD_IEEE802_TR
    560 		case ARPHRD_IEEE802_TR:
    561 #endif /* ARPHRD_IEEE802_TR */
    562 			hw->hlen = 7;
    563 			hw->hbuf[0] = HTYPE_IEEE802;
    564 			memcpy(&hw->hbuf[1], sa->sa_data, 6);
    565 			break;
    566 		case ARPHRD_FDDI:
    567 			hw->hlen = 7;
    568 			hw->hbuf[0] = HTYPE_FDDI;
    569 			memcpy(&hw->hbuf[1], sa->sa_data, 6);
    570 			break;
    571 		default:
    572 			log_fatal("Unsupported device type %ld for \"%s\"",
    573 				  (long int)sa->sa_family, name);
    574 	}
    575 
    576 	close(sock);
    577 }
    578 #endif
    579