File:  [ELWIX - Embedded LightWeight unIX -] / embedaddon / pimd / igmp.c
Revision 1.1.1.1 (vendor branch): download - view: text, annotated - select for diffs - revision graph
Mon Jun 12 07:59:37 2017 UTC (7 years ago) by misho
Branches: pimd, MAIN
CVS tags: v2_3_2, HEAD
pimd 2.3.2

    1: /*
    2:  * Copyright (c) 1998-2001
    3:  * University of Southern California/Information Sciences Institute.
    4:  * All rights reserved.
    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:  * 3. Neither the name of the project nor the names of its contributors
   15:  *    may be used to endorse or promote products derived from this software
   16:  *    without specific prior written permission.
   17:  *
   18:  * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
   19:  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   20:  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   21:  * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
   22:  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   23:  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
   24:  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
   25:  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   26:  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
   27:  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
   28:  * SUCH DAMAGE.
   29:  */
   30: /*
   31:  *  $Id: igmp.c,v 1.1.1.1 2017/06/12 07:59:37 misho Exp $
   32:  */
   33: /*
   34:  * Part of this program has been derived from mrouted.
   35:  * The mrouted program is covered by the license in the accompanying file
   36:  * named "LICENSE.mrouted".
   37:  *
   38:  * The mrouted program is COPYRIGHT 1989 by The Board of Trustees of
   39:  * Leland Stanford Junior University.
   40:  *
   41:  */
   42: 
   43: #include "defs.h"
   44: 
   45: /*
   46:  * Exported variables.
   47:  */
   48: char   *igmp_recv_buf;		/* input packet buffer               */
   49: char   *igmp_send_buf;		/* output packet buffer              */
   50: int     igmp_socket;		/* socket for all network I/O        */
   51: uint32_t allhosts_group;		/* allhosts  addr in net order       */
   52: uint32_t allrouters_group;	/* All-Routers addr in net order     */
   53: uint32_t allreports_group;	/* All IGMP routers in net order     */
   54: 
   55: #ifdef RAW_OUTPUT_IS_RAW
   56: extern int curttl;
   57: #endif /* RAW_OUTPUT_IS_RAW */
   58: 
   59: /*
   60:  * Local functions definitions.
   61:  */
   62: static void igmp_read   (int i, fd_set *rfd);
   63: static void accept_igmp (ssize_t recvlen);
   64: 
   65: 
   66: /*
   67:  * Open and initialize the igmp socket, and fill in the non-changing
   68:  * IP header fields in the output packet buffer.
   69:  */
   70: void init_igmp(void)
   71: {
   72:     struct ip *ip;
   73:     char *router_alert;
   74: 
   75:     igmp_recv_buf = calloc(1, RECV_BUF_SIZE);
   76:     igmp_send_buf = calloc(1, SEND_BUF_SIZE);
   77:     if (!igmp_recv_buf || !igmp_send_buf)
   78: 	logit(LOG_ERR, 0, "Ran out of memory in init_igmp()");
   79: 
   80:     if ((igmp_socket = socket(AF_INET, SOCK_RAW, IPPROTO_IGMP)) < 0)
   81: 	logit(LOG_ERR, errno, "Failed creating IGMP socket in init_igmp()");
   82: 
   83:     k_hdr_include(igmp_socket, TRUE);	/* include IP header when sending */
   84:     k_set_sndbuf(igmp_socket, SO_SEND_BUF_SIZE_MAX,
   85: 		 SO_SEND_BUF_SIZE_MIN); /* lots of output buffering        */
   86:     k_set_rcvbuf(igmp_socket, SO_RECV_BUF_SIZE_MAX,
   87: 		 SO_RECV_BUF_SIZE_MIN); /* lots of input buffering        */
   88:     k_set_ttl(igmp_socket, MINTTL);	/* restrict multicasts to one hop */
   89:     k_set_loop(igmp_socket, FALSE);	/* disable multicast loopback     */
   90: 
   91:     ip	       = (struct ip *)igmp_send_buf;
   92:     memset(ip, 0, IP_IGMP_HEADER_LEN);
   93:     ip->ip_v   = IPVERSION;
   94:     ip->ip_hl  = IP_IGMP_HEADER_LEN >> 2;
   95:     ip->ip_tos = 0xc0;			/* Internet Control   */
   96:     ip->ip_id  = 0;			/* let kernel fill in */
   97:     ip->ip_off = 0;
   98:     ip->ip_ttl = MAXTTL;		/* applies to unicasts only */
   99:     ip->ip_p   = IPPROTO_IGMP;
  100:     ip->ip_sum = 0;			/* let kernel fill in */
  101: 
  102:     /* Enable RFC2113 IP Router Alert.  Per spec this is required to
  103:      * force certain routers/switches to inspect this frame. */
  104:     router_alert    = igmp_send_buf + sizeof(struct ip);
  105:     router_alert[0] = IPOPT_RA;
  106:     router_alert[1] = 4;
  107:     router_alert[2] = 0;
  108:     router_alert[3] = 0;
  109: 
  110:     /* Everywhere in the daemon we use network-byte-order */
  111:     allhosts_group   = htonl(INADDR_ALLHOSTS_GROUP);
  112:     allrouters_group = htonl(INADDR_ALLRTRS_GROUP);
  113:     allreports_group = htonl(INADDR_ALLRPTS_GROUP);
  114: 
  115:     if (register_input_handler(igmp_socket, igmp_read) < 0)
  116: 	logit(LOG_ERR, 0, "Failed registering igmp_read() as an input handler in init_igmp()");
  117: }
  118: 
  119: 
  120: /* Read an IGMP message */
  121: static void igmp_read(int i __attribute__((unused)), fd_set *rfd __attribute__((unused)))
  122: {
  123:     ssize_t len;
  124:     socklen_t dummy = 0;
  125: 
  126:     while ((len = recvfrom(igmp_socket, igmp_recv_buf, RECV_BUF_SIZE, 0, NULL, &dummy)) < 0) {
  127: 	if (errno == EINTR)
  128: 	    continue;		/* Received signal, retry syscall. */
  129: 
  130: 	logit(LOG_ERR, errno, "Failed recvfrom() in igmp_read()");
  131: 	return;
  132:     }
  133: 
  134:     accept_igmp(len);
  135: }
  136: 
  137: /*
  138:  * Process a newly received IGMP packet that is sitting in the input
  139:  * packet buffer.
  140:  */
  141: static void accept_igmp(ssize_t recvlen)
  142: {
  143:     int ipdatalen, iphdrlen, igmpdatalen;
  144:     uint32_t src, dst, group;
  145:     struct ip *ip;
  146:     struct igmp *igmp;
  147:     int igmp_version = 3;
  148: 
  149:     if (recvlen < (ssize_t)sizeof(struct ip)) {
  150: 	logit(LOG_WARNING, 0, "Received IGMP packet too short (%u bytes) for IP header", recvlen);
  151: 	return;
  152:     }
  153: 
  154:     ip  = (struct ip *)igmp_recv_buf;
  155:     src = ip->ip_src.s_addr;
  156:     dst = ip->ip_dst.s_addr;
  157: 
  158:     /* packets sent up from kernel to daemon have ip->ip_p = 0 */
  159:     if (ip->ip_p == 0) {
  160: #if 0				/* XXX */
  161: 	if (src == 0 || dst == 0)
  162: 	    logit(LOG_WARNING, 0, "Kernel request not accurate, src %s dst %s",
  163: 		inet_fmt(src, s1, sizeof(s1)), inet_fmt(dst, s2, sizeof(s2)));
  164: 	else
  165: #endif
  166: 	    process_kernel_call();
  167: 	return;
  168:     }
  169: 
  170:     iphdrlen  = ip->ip_hl << 2;
  171: #if 0
  172: #ifdef HAVE_IP_HDRINCL_BSD_ORDER
  173: #ifdef __NetBSD__
  174:     ipdatalen = ip->ip_len; /* The NetBSD kernel subtracts hlen for us, unfortunately. */
  175: #else
  176:     ipdatalen = ip->ip_len - iphdrlen;
  177: #endif
  178: #else
  179:     ipdatalen = ntohs(ip->ip_len) - iphdrlen;
  180: #endif
  181: #else   /* !0 */
  182:     ipdatalen = recvlen - iphdrlen;
  183: #endif	/* O */
  184: 
  185:     if (iphdrlen + ipdatalen != recvlen) {
  186: 	logit(LOG_WARNING, 0, "Received packet from %s shorter (%u bytes) than hdr+data length (%u+%u)",
  187: 	    inet_fmt(src, s1, sizeof(s1)), recvlen, iphdrlen, ipdatalen);
  188: 	return;
  189:     }
  190: 
  191:     igmp	= (struct igmp *)(igmp_recv_buf + iphdrlen);
  192:     group       = igmp->igmp_group.s_addr;
  193:     igmpdatalen = ipdatalen - IGMP_MINLEN;
  194: 
  195:     if (igmpdatalen < 0) {
  196: 	logit(LOG_WARNING, 0, "Received IP data field too short (%u bytes) for IGMP, from %s",
  197: 	      ipdatalen, inet_fmt(src, s1, sizeof(s1)));
  198: 	return;
  199:     }
  200: 
  201:     IF_DEBUG(DEBUG_IGMP)
  202: 	logit(LOG_DEBUG, 0, "Received %s from %s to %s",
  203: 	      packet_kind(IPPROTO_IGMP, igmp->igmp_type, igmp->igmp_code),
  204: 	      inet_fmt(src, s1, sizeof(s1)), inet_fmt(dst, s2, sizeof(s2)));
  205: 
  206:     switch (igmp->igmp_type) {
  207: 	case IGMP_MEMBERSHIP_QUERY:
  208: 	    /* RFC 3376:7.1 */
  209: 	    if (ipdatalen == 8) {
  210: 		if (igmp->igmp_code == 0)
  211: 		    igmp_version = 1;
  212: 		else
  213: 		    igmp_version = 2;
  214: 	    } else if (ipdatalen >= 12) {
  215: 		igmp_version = 3;
  216: 	    } else {
  217: 		logit(LOG_DEBUG, 0, "Received invalid IGMP Membership query: Max Resp Code = %d, length = %d",
  218: 		      igmp->igmp_code, ipdatalen);
  219: 	    }
  220: 	    accept_membership_query(src, dst, group, igmp->igmp_code, igmp_version);
  221: 	    return;
  222: 
  223: 	case IGMP_V1_MEMBERSHIP_REPORT:
  224: 	case IGMP_V2_MEMBERSHIP_REPORT:
  225: 	    accept_group_report(src, dst, group, igmp->igmp_type);
  226: 	    return;
  227: 
  228: 	case IGMP_V2_LEAVE_GROUP:
  229: 	    accept_leave_message(src, dst, group);
  230: 	    return;
  231: 
  232: 	case IGMP_V3_MEMBERSHIP_REPORT:
  233: 	    if (igmpdatalen < IGMP_V3_GROUP_RECORD_MIN_SIZE) {
  234: 		logit(LOG_DEBUG, 0, "Too short IGMP v3 Membership report: igmpdatalen(%d) < MIN(%d)", igmpdatalen, IGMP_V3_GROUP_RECORD_MIN_SIZE);
  235: 		return;
  236: 	    }
  237: 	    accept_membership_report(src, dst, (struct igmpv3_report *)(igmp_recv_buf + iphdrlen), recvlen - iphdrlen);
  238: 	    return;
  239: 
  240: 	case IGMP_DVMRP:
  241: 	    /* XXX: TODO: most of the stuff below is not implemented. We are still
  242: 	     * only PIM router.
  243: 	     */
  244: 	    group = ntohl(group);
  245: 
  246: 	    switch (igmp->igmp_code) {
  247: 		case DVMRP_PROBE:
  248: 		    dvmrp_accept_probe(src, dst, (uint8_t *)(igmp+1), igmpdatalen, group);
  249: 		    return;
  250: 
  251: 		case DVMRP_REPORT:
  252: 		    dvmrp_accept_report(src, dst, (uint8_t *)(igmp+1), igmpdatalen, group);
  253: 		    return;
  254: 
  255: 		case DVMRP_ASK_NEIGHBORS:
  256: 		    accept_neighbor_request(src, dst);
  257: 		    return;
  258: 
  259: 		case DVMRP_ASK_NEIGHBORS2:
  260: 		    accept_neighbor_request2(src, dst);
  261: 		    return;
  262: 
  263: 		case DVMRP_NEIGHBORS:
  264: 		    dvmrp_accept_neighbors(src, dst, (uint8_t *)(igmp+1), igmpdatalen, group);
  265: 		    return;
  266: 
  267: 		case DVMRP_NEIGHBORS2:
  268: 		    dvmrp_accept_neighbors2(src, dst, (uint8_t *)(igmp+1), igmpdatalen, group);
  269: 		    return;
  270: 
  271: 		case DVMRP_PRUNE:
  272: 		    dvmrp_accept_prune(src, dst, (uint8_t *)(igmp+1), igmpdatalen);
  273: 		    return;
  274: 
  275: 		case DVMRP_GRAFT:
  276: 		    dvmrp_accept_graft(src, dst, (uint8_t *)(igmp+1), igmpdatalen);
  277: 		    return;
  278: 
  279: 		case DVMRP_GRAFT_ACK:
  280: 		    dvmrp_accept_g_ack(src, dst, (uint8_t *)(igmp+1), igmpdatalen);
  281: 		    return;
  282: 
  283: 		case DVMRP_INFO_REQUEST:
  284: 		    dvmrp_accept_info_request(src, dst, (uint8_t *)(igmp+1), igmpdatalen);
  285: 		    return;
  286: 
  287: 		case DVMRP_INFO_REPLY:
  288: 		    dvmrp_accept_info_reply(src, dst, (uint8_t *)(igmp+1), igmpdatalen);
  289: 		    return;
  290: 
  291: 		default:
  292: 		    logit(LOG_INFO, 0, "Ignoring unknown DVMRP message code %u from %s to %s",
  293: 			  igmp->igmp_code, inet_fmt(src, s1, sizeof(s1)), inet_fmt(dst, s2, sizeof(s2)));
  294: 		    return;
  295: 	    }
  296: 
  297: 	case IGMP_PIM:
  298: 	    return;    /* TODO: this is PIM v1 message. Handle it?. */
  299: 
  300: 	case IGMP_MTRACE_RESP:
  301: 	    return;    /* TODO: implement it */
  302: 
  303: 	case IGMP_MTRACE:
  304: 	    accept_mtrace(src, dst, group, (char *)(igmp+1), igmp->igmp_code, igmpdatalen);
  305: 	    return;
  306: 
  307: 	default:
  308: 	    logit(LOG_INFO, 0, "Ignoring unknown IGMP message type %x from %s to %s",
  309: 		  igmp->igmp_type, inet_fmt(src, s1, sizeof(s1)), inet_fmt(dst, s2, sizeof(s2)));
  310: 	    return;
  311:     }
  312: }
  313: 
  314: static void send_ip_frame(uint32_t src, uint32_t dst, int type, int code, char *buf, size_t len)
  315: {
  316:     int setloop = 0;
  317:     struct ip *ip;
  318:     struct sockaddr_in sin;
  319:     char source[20], dest[20];
  320: 
  321:     /* Prepare the IP header */
  322:     len		     += IP_IGMP_HEADER_LEN;
  323:     ip		      = (struct ip *)buf;
  324:     ip->ip_id	      = 0; /* let kernel fill in */
  325:     ip->ip_off	      = 0;
  326:     ip->ip_src.s_addr = src;
  327:     ip->ip_dst.s_addr = dst;
  328: #ifdef HAVE_IP_HDRINCL_BSD_ORDER
  329:     ip->ip_len	      = len;
  330: #else
  331:     ip->ip_len	      = htons(len);
  332: #endif
  333: 
  334:     if (IN_MULTICAST(ntohl(dst))) {
  335: 	k_set_if(igmp_socket, src);
  336: 	if (type != IGMP_DVMRP || dst == allhosts_group) {
  337: 	    setloop = 1;
  338: 	    k_set_loop(igmp_socket, TRUE);
  339: 	}
  340: #ifdef RAW_OUTPUT_IS_RAW
  341: 	ip->ip_ttl = curttl;
  342:     } else {
  343: 	ip->ip_ttl = MAXTTL;
  344: #endif
  345:     }
  346: 
  347:     memset(&sin, 0, sizeof(sin));
  348:     sin.sin_family = AF_INET;
  349:     sin.sin_addr.s_addr = dst;
  350: #ifdef HAVE_SA_LEN
  351:     sin.sin_len = sizeof(sin);
  352: #endif
  353: 
  354:     IF_DEBUG(DEBUG_IGMP)
  355: 	logit(LOG_DEBUG, 0, "Send %s from %s to %s",
  356: 	      packet_kind(IPPROTO_IGMP, type, code),
  357: 	      src == INADDR_ANY_N ? "INADDR_ANY" :
  358: 	      inet_fmt(src, s1, sizeof(s1)), inet_fmt(dst, s2, sizeof(s2)));
  359: 
  360:     while (sendto(igmp_socket, buf, len, 0, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
  361: 	if (errno == EINTR)
  362: 	    continue;		/* Received signal, retry syscall. */
  363: 	if (errno == ENETDOWN || errno == ENODEV)
  364: 	    check_vif_state();
  365: 	else if (errno == EPERM || errno == EHOSTUNREACH)
  366: 	    logit(LOG_WARNING, 0, "Not allowed to send IGMP message from %s to %s, possibly firewall"
  367: #ifdef __linux__
  368: 		  ", or SELinux policy violation,"
  369: #endif
  370: 		  " related problem."
  371: 		  ,
  372: 		  inet_fmt(src, source, sizeof(source)), inet_fmt(dst, dest, sizeof(dest)));
  373: 	else
  374: 	    logit(log_level(IPPROTO_IGMP, type, code), errno, "Sendto to %s on %s",
  375: 		  inet_fmt(dst, s1, sizeof(s1)), inet_fmt(src, s2, sizeof(s2)));
  376: 
  377: 	if (setloop)
  378: 	    k_set_loop(igmp_socket, FALSE);
  379: 
  380: 	return;
  381:     }
  382: 
  383:     if (setloop)
  384: 	k_set_loop(igmp_socket, FALSE);
  385: 
  386:     IF_DEBUG(DEBUG_PKT | debug_kind(IPPROTO_IGMP, type, code)) {
  387: 	logit(LOG_DEBUG, 0, "SENT %5d bytes %s from %-15s to %s", len,
  388: 	      packet_kind(IPPROTO_IGMP, type, code),
  389: 	      src == INADDR_ANY_N
  390: 		  ? "INADDR_ANY"
  391: 		  : inet_fmt(src, s1, sizeof(s1)),
  392: 	      inet_fmt(dst, s2, sizeof(s2)));
  393:     }
  394: }
  395: 
  396: /*
  397:  * RFC-3376 states that Max Resp Code (MRC) and Querier's Query Interval Code
  398:  * (QQIC) should be presented in floating point value if their value exceeds
  399:  * 128. The following formula is used by IGMPv3 clients to calculate the
  400:  * actual value of the floating point:
  401:  *
  402:  *       0 1 2 3 4 5 6 7
  403:  *      +-+-+-+-+-+-+-+-+
  404:  *      |1| exp | mant  |
  405:  *      +-+-+-+-+-+-+-+-+
  406:  *
  407:  *   QQI / MRT = (mant | 0x10) << (exp + 3)
  408:  *
  409:  * This requires us to find the largest set (fls) bit in the 15-bit number
  410:  * and set the exponent based on its index in the bits 15-8. ie.
  411:  *
  412:  *   exponent 0: igmp_fls(0000 0000 1000 0010)
  413:  *   exponent 5: igmp_fls(0001 0000 0000 0000)
  414:  *   exponent 7: igmp_fls(0111 0101 0000 0000)
  415:  *
  416:  * and set that as the exponent. The mantissa is set to the last 4 bits
  417:  * remaining after the (3 + exponent) shifts to the right.
  418:  *
  419:  * Note!
  420:  * The numbers 31744-32767 are the maximum we can present with floating
  421:  * point that has an exponent of 3 and a mantissa of 4. After this the
  422:  * implementation just wraps around back to zero.
  423:  */
  424: static inline uint8_t igmp_floating_point(unsigned int mantissa)
  425: {
  426:     unsigned int exponent;
  427: 
  428:     /* Wrap around numbers larger than 2^15, since those can not be
  429:      * presented with 7-bit floating point. */
  430:     mantissa &= 0x00007FFF;
  431: 
  432:     /* If top 8 bits are zero. */
  433:     if (!(mantissa & 0x00007F80))
  434:         return mantissa;
  435: 
  436:     /* Shift the mantissa and mark this code floating point. */
  437:     mantissa >>= 3;
  438:     /* At this point the actual exponent (bits 7-5) are still 0, but the
  439:      * exponent might be incremented below. */
  440:     exponent   = 0x00000080;
  441: 
  442:     /* If bits 7-4 are not zero. */
  443:     if (mantissa & 0x00000F00) {
  444:         mantissa >>= 4;
  445:         /* The index of largest set bit is at least 4. */
  446:         exponent  |= 0x00000040;
  447:     }
  448: 
  449:     /* If bits 7-6 OR bits 3-2 are not zero. */
  450:     if (mantissa & 0x000000C0) {
  451:         mantissa >>= 2;
  452:         /* The index of largest set bit is atleast 6 if we shifted the
  453:          * mantissa earlier or atleast 2 if we did not shift it. */
  454:         exponent  |= 0x00000020;
  455:     }
  456: 
  457:     /* If bit 7 OR bit 3 OR bit 1 is not zero. */
  458:     if (mantissa & 0x00000020) {
  459:         mantissa >>= 1;
  460:         /* The index of largest set bit is atleast 7 if we shifted the
  461:          * mantissa two times earlier or atleast 3 if we shifted the
  462:          * mantissa last time or atleast 1 if we did not shift it. */
  463:         exponent  |= 0x00000010;
  464:     }
  465: 
  466:     return exponent | (mantissa & 0x0000000F);
  467: }
  468: 
  469: void send_igmp(char *buf, uint32_t src, uint32_t dst, int type, int code, uint32_t group, int datalen)
  470: {
  471:     size_t len = IGMP_MINLEN + datalen;
  472:     struct igmpv3_query *igmp;
  473: 
  474:     igmp              = (struct igmpv3_query *)(buf + IP_IGMP_HEADER_LEN);
  475:     igmp->type        = type;
  476:     if (datalen >= 4)
  477:         igmp->code    = igmp_floating_point(code);
  478:     else
  479:         igmp->code    = code;
  480:     igmp->group       = group;
  481:     igmp->csum        = 0;
  482:     igmp->csum        = inet_cksum((uint16_t *)igmp, len);
  483: 
  484:     if (datalen >= 4) {
  485:         igmp->qrv = 2;
  486:         igmp->qqic = igmp_floating_point(igmp_query_interval);
  487:     }
  488: 
  489:     send_ip_frame(src, dst, type, code, buf, len);
  490: }
  491: 
  492: /**
  493:  * Local Variables:
  494:  *  version-control: t
  495:  *  indent-tabs-mode: t
  496:  *  c-file-style: "ellemtel"
  497:  *  c-basic-offset: 4
  498:  * End:
  499:  */

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>