620 lines
16 KiB
C
620 lines
16 KiB
C
/*
|
|
* Copyright (C) 1998 WIDE Project.
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of the project nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
/*
|
|
* Copyright (c) 1998 by the University of Southern California.
|
|
* All rights reserved.
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software and
|
|
* its documentation in source and binary forms for lawful
|
|
* purposes and without fee is hereby granted, provided
|
|
* that the above copyright notice appear in all copies and that both
|
|
* the copyright notice and this permission notice appear in supporting
|
|
* documentation, and that any documentation, advertising materials,
|
|
* and other materials related to such distribution and use acknowledge
|
|
* that the software was developed by the University of Southern
|
|
* California and/or Information Sciences Institute.
|
|
* The name of the University of Southern California may not
|
|
* be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THE UNIVERSITY OF SOUTHERN CALIFORNIA DOES NOT MAKE ANY REPRESENTATIONS
|
|
* ABOUT THE SUITABILITY OF THIS SOFTWARE FOR ANY PURPOSE. THIS SOFTWARE IS
|
|
* PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
* INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND
|
|
* NON-INFRINGEMENT.
|
|
*
|
|
* IN NO EVENT SHALL USC, OR ANY OTHER CONTRIBUTOR BE LIABLE FOR ANY
|
|
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES, WHETHER IN CONTRACT,
|
|
* TORT, OR OTHER FORM OF ACTION, ARISING OUT OF OR IN CONNECTION WITH,
|
|
* THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
* Other copyrights might apply to parts of this software and are so
|
|
* noted when applicable.
|
|
*/
|
|
/*
|
|
* Questions concerning this software should be directed to
|
|
* Mickael Hoerdt (hoerdt@clarinet.u-strasbg.fr) LSIIT Strasbourg.
|
|
*
|
|
*/
|
|
/*
|
|
* This program has been derived from pim6dd.
|
|
* The pim6dd program is covered by the license in the accompanying file
|
|
* named "LICENSE.pim6dd".
|
|
*/
|
|
/*
|
|
* This program has been derived from pimd.
|
|
* The pimd program is covered by the license in the accompanying file
|
|
* named "LICENSE.pimd".
|
|
*
|
|
*/
|
|
/*
|
|
* Part of this program has been derived from mrouted.
|
|
* The mrouted program is covered by the license in the accompanying file
|
|
* named "LICENSE.mrouted".
|
|
*
|
|
* The mrouted program is COPYRIGHT 1989 by The Board of Trustees of
|
|
* Leland Stanford Junior University.
|
|
*
|
|
*/
|
|
/*
|
|
* Part of this program has been derived from mrouted.
|
|
* The mrouted program is covered by the license in the accompanying file
|
|
* named "LICENSE.mrouted".
|
|
*
|
|
* The mrouted program is COPYRIGHT 1989 by The Board of Trustees of
|
|
* Leland Stanford Junior University.
|
|
*
|
|
* $FreeBSD$
|
|
*/
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/types.h>
|
|
#include <sys/time.h>
|
|
#include <sys/socket.h>
|
|
#include <net/route.h>
|
|
#include <netinet/in.h>
|
|
#include <netinet6/ip6_mroute.h>
|
|
#include <netinet/icmp6.h>
|
|
#include <syslog.h>
|
|
#include <stdlib.h>
|
|
#include "mld6.h"
|
|
#include "vif.h"
|
|
#include "debug.h"
|
|
#include "inet6.h"
|
|
#include "route.h"
|
|
#include "callout.h"
|
|
#include "timer.h"
|
|
|
|
#include "mld6_proto.h"
|
|
|
|
extern struct in6_addr in6addr_any;
|
|
|
|
typedef struct
|
|
{
|
|
mifi_t mifi;
|
|
struct listaddr *g;
|
|
int q_time;
|
|
} cbk_t;
|
|
|
|
|
|
/*
|
|
* Forward declarations.
|
|
*/
|
|
static void DelVif __P((void *arg));
|
|
static int SetTimer __P((int mifi, struct listaddr * g));
|
|
static int DeleteTimer __P((int id));
|
|
static void SendQuery __P((void *arg));
|
|
static int SetQueryTimer
|
|
__P((struct listaddr * g, int mifi, int to_expire,
|
|
int q_time));
|
|
|
|
/*
|
|
* Send group membership queries on that interface if I am querier.
|
|
*/
|
|
void
|
|
query_groups(v)
|
|
register struct uvif *v;
|
|
{
|
|
register struct listaddr *g;
|
|
|
|
v->uv_gq_timer = MLD6_QUERY_INTERVAL;
|
|
if (v->uv_flags & VIFF_QUERIER && (v->uv_flags & VIFF_NOLISTENER) == 0) {
|
|
send_mld6(MLD6_LISTENER_QUERY, 0, &v->uv_linklocal->pa_addr,
|
|
NULL, (struct in6_addr *)&in6addr_any, v->uv_ifindex,
|
|
MLD6_QUERY_RESPONSE_INTERVAL, 0, 1);
|
|
v->uv_out_mld_query++;
|
|
}
|
|
|
|
/*
|
|
* Decrement the old-hosts-present timer for each active group on that
|
|
* vif.
|
|
*/
|
|
for (g = v->uv_groups; g != NULL; g = g->al_next)
|
|
if (g->al_old > timer_interval)
|
|
g->al_old -= timer_interval;
|
|
else
|
|
g->al_old = 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* Process an incoming host membership query
|
|
*/
|
|
void
|
|
accept_listener_query(src, dst, group, tmo)
|
|
struct sockaddr_in6 *src;
|
|
struct in6_addr *dst,
|
|
*group;
|
|
int tmo;
|
|
{
|
|
register int mifi;
|
|
register struct uvif *v;
|
|
struct sockaddr_in6 group_sa = {sizeof(group_sa), AF_INET6};
|
|
|
|
/* Ignore my own listener query */
|
|
if (local_address(src) != NO_VIF)
|
|
return;
|
|
|
|
if ((mifi = find_vif_direct(src)) == NO_VIF) {
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_INFO, 0,
|
|
"accept_listener_query: can't find a mif");
|
|
return;
|
|
}
|
|
v = &uvifs[mifi];
|
|
v->uv_in_mld_query++;
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0,
|
|
"accepting multicast listener query on %s: "
|
|
"src %s, dst %s, grp %s",
|
|
v->uv_name,
|
|
inet6_fmt(&src->sin6_addr), inet6_fmt(dst),
|
|
inet6_fmt(group));
|
|
|
|
if (v->uv_querier == NULL || !inet6_equal(&v->uv_querier->al_addr, src)) {
|
|
/*
|
|
* This might be:
|
|
* - A query from a new querier, with a lower source address than
|
|
* the current querier (who might be me).
|
|
* - A query from a new router that just started up and doesn't know
|
|
* who the querier is.
|
|
*/
|
|
if (inet6_lessthan(src, (v->uv_querier ? &v->uv_querier->al_addr
|
|
: &v->uv_linklocal->pa_addr)))
|
|
{
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0, "new querier %s (was %s) "
|
|
"on mif %d",
|
|
inet6_fmt(&src->sin6_addr),
|
|
v->uv_querier ?
|
|
inet6_fmt(&v->uv_querier->al_addr.sin6_addr) :
|
|
"me", mifi);
|
|
if (!v->uv_querier) { /* this should be impossible... */
|
|
v->uv_querier = (struct listaddr *)malloc(sizeof(struct listaddr));
|
|
memset(v->uv_querier, 0, sizeof(struct listaddr));
|
|
}
|
|
v->uv_flags &= ~VIFF_QUERIER;
|
|
v->uv_querier->al_addr = *src;
|
|
time(&v->uv_querier->al_ctime);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Reset the timer since we've received a query.
|
|
*/
|
|
if (v->uv_querier && inet6_equal(src, &v->uv_querier->al_addr))
|
|
v->uv_querier->al_timer = MLD6_OTHER_QUERIER_PRESENT_INTERVAL;
|
|
|
|
/*
|
|
* If this is a Group-Specific query which we did not source, we must set
|
|
* our membership timer to [Last Member Query Count] * the [Max Response
|
|
* Time] in the packet.
|
|
*/
|
|
if (!IN6_IS_ADDR_UNSPECIFIED(group) &&
|
|
inet6_equal(src, &v->uv_linklocal->pa_addr))
|
|
{
|
|
register struct listaddr *g;
|
|
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0,
|
|
"%s for %s from %s on mif %d, timer %d",
|
|
"Group-specific membership query",
|
|
inet6_fmt(group),
|
|
inet6_fmt(&src->sin6_addr), mifi, tmo);
|
|
|
|
group_sa.sin6_addr = *group;
|
|
group_sa.sin6_scope_id = inet6_uvif2scopeid(&group_sa, v);
|
|
for (g = v->uv_groups; g != NULL; g = g->al_next)
|
|
{
|
|
if (inet6_equal(&group_sa, &g->al_addr)
|
|
&& g->al_query == 0)
|
|
{
|
|
/* setup a timeout to remove the group membership */
|
|
if (g->al_timerid)
|
|
g->al_timerid = DeleteTimer(g->al_timerid);
|
|
g->al_timer = MLD6_LAST_LISTENER_QUERY_COUNT *
|
|
tmo / MLD6_TIMER_SCALE;
|
|
/*
|
|
* use al_query to record our presence in last-member state
|
|
*/
|
|
g->al_query = -1;
|
|
g->al_timerid = SetTimer(mifi, g);
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0,
|
|
"timer for grp %s on mif %d "
|
|
"set to %d",
|
|
inet6_fmt(group),
|
|
mifi, g->al_timer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Process an incoming group membership report.
|
|
*/
|
|
void
|
|
accept_listener_report(src, dst, group)
|
|
struct sockaddr_in6 *src;
|
|
struct in6_addr *dst,
|
|
*group;
|
|
{
|
|
register mifi_t mifi;
|
|
register struct uvif *v;
|
|
register struct listaddr *g;
|
|
struct sockaddr_in6 group_sa = {sizeof(group_sa), AF_INET6};
|
|
|
|
if (IN6_IS_ADDR_MC_LINKLOCAL(group)) {
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0,
|
|
"accept_listener_report: group(%s) has the "
|
|
"link-local scope. discard", inet6_fmt(group));
|
|
return;
|
|
}
|
|
|
|
if ((mifi = find_vif_direct_local(src)) == NO_VIF)
|
|
{
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_INFO, 0,
|
|
"accept_listener_report: can't find a mif");
|
|
return;
|
|
}
|
|
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0,
|
|
"accepting multicast listener report: "
|
|
"src %s,dst %s, grp %s",
|
|
inet6_fmt(&src->sin6_addr),inet6_fmt(dst),
|
|
inet6_fmt(group));
|
|
|
|
v = &uvifs[mifi];
|
|
v->uv_in_mld_report++;
|
|
|
|
/*
|
|
* Look for the group in our group list; if found, reset its timer.
|
|
*/
|
|
|
|
group_sa.sin6_addr = *group;
|
|
group_sa.sin6_scope_id = inet6_uvif2scopeid(&group_sa, v);
|
|
|
|
for (g = v->uv_groups; g != NULL; g = g->al_next)
|
|
{
|
|
if (inet6_equal(&group_sa, &g->al_addr))
|
|
{
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG,0,
|
|
"The group already exist");
|
|
|
|
g->al_reporter = *src;
|
|
|
|
/* delete old timers, set a timer for expiration */
|
|
|
|
g->al_timer = MLD6_LISTENER_INTERVAL;
|
|
if (g->al_query)
|
|
g->al_query = DeleteTimer(g->al_query);
|
|
if (g->al_timerid)
|
|
g->al_timerid = DeleteTimer(g->al_timerid);
|
|
g->al_timerid = SetTimer(mifi, g);
|
|
add_leaf(mifi, NULL, &group_sa);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If not found, add it to the list and update kernel cache.
|
|
*/
|
|
if (g == NULL)
|
|
{
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG,0,
|
|
"The group don't exist , trying to add it");
|
|
|
|
g = (struct listaddr *) malloc(sizeof(struct listaddr));
|
|
if (g == NULL)
|
|
log(LOG_ERR, 0, "ran out of memory"); /* fatal */
|
|
|
|
g->al_addr = group_sa;
|
|
g->al_old = 0;
|
|
|
|
/** set a timer for expiration **/
|
|
g->al_query = 0;
|
|
g->al_timer = MLD6_LISTENER_INTERVAL;
|
|
g->al_reporter = *src;
|
|
g->al_timerid = SetTimer(mifi, g);
|
|
g->al_next = v->uv_groups;
|
|
v->uv_groups = g;
|
|
time(&g->al_ctime);
|
|
|
|
add_leaf(mifi, NULL, &group_sa);
|
|
}
|
|
}
|
|
|
|
|
|
/* TODO: send PIM prune message if the last member? */
|
|
void
|
|
accept_listener_done(src, dst, group)
|
|
struct sockaddr_in6 *src;
|
|
struct in6_addr *dst,
|
|
*group;
|
|
{
|
|
register mifi_t mifi;
|
|
register struct uvif *v;
|
|
register struct listaddr *g;
|
|
struct sockaddr_in6 group_sa = {sizeof(group_sa), AF_INET6};
|
|
|
|
/* Don't create routing entries for the LAN scoped addresses */
|
|
|
|
if (IN6_IS_ADDR_MC_NODELOCAL(group)) /* sanity? */
|
|
{
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0,
|
|
"accept_listener_done: address multicast node local(%s),"
|
|
" ignore it...", inet6_fmt(group));
|
|
return;
|
|
}
|
|
|
|
if (IN6_IS_ADDR_MC_LINKLOCAL(group))
|
|
{
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0,
|
|
"accept_listener_done: address multicast link local(%s), "
|
|
"ignore it ...", inet6_fmt(group));
|
|
return;
|
|
}
|
|
|
|
if ((mifi = find_vif_direct_local(src)) == NO_VIF)
|
|
{
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_INFO, 0,
|
|
"accept_listener_done: can't find a mif");
|
|
return;
|
|
}
|
|
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_INFO, 0,
|
|
"accepting listener done message: src %s, dst %s, grp %s",
|
|
inet6_fmt(&src->sin6_addr),
|
|
inet6_fmt(dst), inet6_fmt(group));
|
|
|
|
v = &uvifs[mifi];
|
|
v->uv_in_mld_done++;
|
|
|
|
if (!(v->uv_flags & (VIFF_QUERIER | VIFF_DR)))
|
|
return;
|
|
|
|
/*
|
|
* Look for the group in our group list in order to set up a
|
|
* short-timeout query.
|
|
*/
|
|
group_sa.sin6_addr = *group;
|
|
group_sa.sin6_scope_id = inet6_uvif2scopeid(&group_sa, v);
|
|
for (g = v->uv_groups; g != NULL; g = g->al_next)
|
|
{
|
|
if (inet6_equal(&group_sa, &g->al_addr))
|
|
{
|
|
IF_DEBUG(DEBUG_MLD)
|
|
log(LOG_DEBUG, 0,
|
|
"[accept_done_message] %d %d \n",
|
|
g->al_old, g->al_query);
|
|
|
|
/*
|
|
* Ignore the done message if there are old hosts present
|
|
*/
|
|
if (g->al_old)
|
|
return;
|
|
|
|
/*
|
|
* still waiting for a reply to a query, ignore the done
|
|
*/
|
|
if (g->al_query)
|
|
return;
|
|
|
|
/** delete old timer set a timer for expiration **/
|
|
if (g->al_timerid)
|
|
g->al_timerid = DeleteTimer(g->al_timerid);
|
|
|
|
/** send a group specific querry **/
|
|
g->al_timer = (MLD6_LAST_LISTENER_QUERY_INTERVAL / MLD6_TIMER_SCALE) *
|
|
(MLD6_LAST_LISTENER_QUERY_COUNT + 1);
|
|
if (v->uv_flags & VIFF_QUERIER &&
|
|
(v->uv_flags & VIFF_NOLISTENER) == 0) {
|
|
send_mld6(MLD6_LISTENER_QUERY, 0,
|
|
&v->uv_linklocal->pa_addr, NULL,
|
|
&g->al_addr.sin6_addr,
|
|
v->uv_ifindex,
|
|
MLD6_LAST_LISTENER_QUERY_INTERVAL, 0, 1);
|
|
v->uv_out_mld_query++;
|
|
}
|
|
g->al_query = SetQueryTimer(g, mifi,
|
|
MLD6_LAST_LISTENER_QUERY_INTERVAL / MLD6_TIMER_SCALE,
|
|
MLD6_LAST_LISTENER_QUERY_INTERVAL);
|
|
g->al_timerid = SetTimer(mifi, g);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Time out record of a group membership on a vif
|
|
*/
|
|
static void
|
|
DelVif(arg)
|
|
void *arg;
|
|
{
|
|
cbk_t *cbk = (cbk_t *) arg;
|
|
mifi_t mifi = cbk->mifi;
|
|
struct uvif *v = &uvifs[mifi];
|
|
struct listaddr *a,
|
|
**anp,
|
|
*g = cbk->g;
|
|
|
|
/*
|
|
* Group has expired delete all kernel cache entries with this group
|
|
*/
|
|
if (g->al_query)
|
|
DeleteTimer(g->al_query);
|
|
|
|
delete_leaf(mifi, NULL, &g->al_addr);
|
|
|
|
/* increment statistics */
|
|
v->uv_listener_timo++;
|
|
|
|
anp = &(v->uv_groups);
|
|
while ((a = *anp) != NULL)
|
|
{
|
|
if (a == g)
|
|
{
|
|
*anp = a->al_next;
|
|
free((char *) a);
|
|
}
|
|
else
|
|
{
|
|
anp = &a->al_next;
|
|
}
|
|
}
|
|
|
|
free(cbk);
|
|
}
|
|
|
|
|
|
/*
|
|
* Set a timer to delete the record of a group membership on a vif.
|
|
*/
|
|
static int
|
|
SetTimer(mifi, g)
|
|
mifi_t mifi;
|
|
struct listaddr *g;
|
|
{
|
|
cbk_t *cbk;
|
|
|
|
cbk = (cbk_t *) malloc(sizeof(cbk_t));
|
|
cbk->mifi = mifi;
|
|
cbk->g = g;
|
|
return timer_setTimer(g->al_timer, DelVif, cbk);
|
|
}
|
|
|
|
|
|
/*
|
|
* Delete a timer that was set above.
|
|
*/
|
|
static int
|
|
DeleteTimer(id)
|
|
int id;
|
|
{
|
|
timer_clearTimer(id);
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* Send a group-specific query.
|
|
*/
|
|
static void
|
|
SendQuery(arg)
|
|
void *arg;
|
|
{
|
|
cbk_t *cbk = (cbk_t *) arg;
|
|
register struct uvif *v = &uvifs[cbk->mifi];
|
|
|
|
if (v->uv_flags & VIFF_QUERIER && (v->uv_flags & VIFF_NOLISTENER) == 0) {
|
|
send_mld6(MLD6_LISTENER_QUERY, 0, &v->uv_linklocal->pa_addr,
|
|
NULL, &cbk->g->al_addr.sin6_addr, v->uv_ifindex,
|
|
cbk->q_time, 0, 1);
|
|
v->uv_out_mld_query++;
|
|
}
|
|
cbk->g->al_query = 0;
|
|
free(cbk);
|
|
}
|
|
|
|
|
|
/*
|
|
* Set a timer to send a group-specific query.
|
|
*/
|
|
static int
|
|
SetQueryTimer(g, mifi, to_expire, q_time)
|
|
struct listaddr *g;
|
|
mifi_t mifi;
|
|
int to_expire;
|
|
int q_time;
|
|
{
|
|
cbk_t *cbk;
|
|
|
|
cbk = (cbk_t *) malloc(sizeof(cbk_t));
|
|
cbk->g = g;
|
|
cbk->q_time = q_time;
|
|
cbk->mifi = mifi;
|
|
return timer_setTimer(to_expire, SendQuery, cbk);
|
|
}
|
|
|
|
/*
|
|
* Checks for MLD listener: returns TRUE if there is a receiver for the group
|
|
* on the given uvif, or returns FALSE otherwise.
|
|
*/
|
|
int
|
|
check_multicast_listener(v, group)
|
|
struct uvif *v;
|
|
struct sockaddr_in6 *group;
|
|
{
|
|
register struct listaddr *g;
|
|
|
|
/*
|
|
* Look for the group in our listener list;
|
|
*/
|
|
for (g = v->uv_groups; g != NULL; g = g->al_next)
|
|
{
|
|
if (inet6_equal(group, &g->al_addr))
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|