3225 lines
85 KiB
C
3225 lines
85 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* PIMv6 MLD querier
|
|
* Copyright (C) 2021-2022 David Lamparter for NetDEF, Inc.
|
|
*/
|
|
|
|
/*
|
|
* keep pim6_mld.h open when working on this code. Most data structures are
|
|
* commented in the header.
|
|
*
|
|
* IPv4 support is pre-planned but hasn't been tackled yet. It is intended
|
|
* that this code will replace the old IGMP querier at some point.
|
|
*/
|
|
|
|
#include <zebra.h>
|
|
#include <netinet/icmp6.h>
|
|
#include <netinet/ip6.h>
|
|
|
|
#include "lib/memory.h"
|
|
#include "lib/jhash.h"
|
|
#include "lib/prefix.h"
|
|
#include "lib/checksum.h"
|
|
#include "lib/frrevent.h"
|
|
#include "termtable.h"
|
|
|
|
#include "pimd/pim6_mld.h"
|
|
#include "pimd/pim6_mld_protocol.h"
|
|
#include "pimd/pim_memory.h"
|
|
#include "pimd/pim_instance.h"
|
|
#include "pimd/pim_iface.h"
|
|
#include "pimd/pim6_cmd.h"
|
|
#include "pimd/pim_cmd_common.h"
|
|
#include "pimd/pim_util.h"
|
|
#include "pimd/pim_tib.h"
|
|
#include "pimd/pimd.h"
|
|
|
|
#ifndef IPV6_MULTICAST_ALL
|
|
#define IPV6_MULTICAST_ALL 29
|
|
#endif
|
|
|
|
DEFINE_MTYPE_STATIC(PIMD, GM_IFACE, "MLD interface");
|
|
DEFINE_MTYPE_STATIC(PIMD, GM_PACKET, "MLD packet");
|
|
DEFINE_MTYPE_STATIC(PIMD, GM_SUBSCRIBER, "MLD subscriber");
|
|
DEFINE_MTYPE_STATIC(PIMD, GM_STATE, "MLD subscription state");
|
|
DEFINE_MTYPE_STATIC(PIMD, GM_SG, "MLD (S,G)");
|
|
DEFINE_MTYPE_STATIC(PIMD, GM_GRP_PENDING, "MLD group query state");
|
|
DEFINE_MTYPE_STATIC(PIMD, GM_GSQ_PENDING, "MLD group/source query aggregate");
|
|
|
|
static void gm_t_query(struct event *t);
|
|
static void gm_trigger_specific(struct gm_sg *sg);
|
|
static void gm_sg_timer_start(struct gm_if *gm_ifp, struct gm_sg *sg,
|
|
struct timeval expire_wait);
|
|
|
|
/* shorthand for log messages */
|
|
#define log_ifp(msg) \
|
|
"[MLD %s:%s] " msg, gm_ifp->ifp->vrf->name, gm_ifp->ifp->name
|
|
#define log_pkt_src(msg) \
|
|
"[MLD %s:%s %pI6] " msg, gm_ifp->ifp->vrf->name, gm_ifp->ifp->name, \
|
|
&pkt_src->sin6_addr
|
|
#define log_sg(sg, msg) \
|
|
"[MLD %s:%s %pSG] " msg, sg->iface->ifp->vrf->name, \
|
|
sg->iface->ifp->name, &sg->sgaddr
|
|
|
|
/* clang-format off */
|
|
static const pim_addr gm_all_hosts = {
|
|
.s6_addr = {
|
|
0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
|
},
|
|
};
|
|
static const pim_addr gm_all_routers = {
|
|
.s6_addr = {
|
|
0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16,
|
|
},
|
|
};
|
|
/* MLDv1 does not allow subscriber tracking due to report suppression
|
|
* hence, the source address is replaced with ffff:...:ffff
|
|
*/
|
|
static const pim_addr gm_dummy_untracked = {
|
|
.s6_addr = {
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
},
|
|
};
|
|
/* clang-format on */
|
|
|
|
#define IPV6_MULTICAST_SCOPE_LINK 2
|
|
|
|
static inline uint8_t in6_multicast_scope(const pim_addr *addr)
|
|
{
|
|
return addr->s6_addr[1] & 0xf;
|
|
}
|
|
|
|
bool in6_multicast_nofwd(const pim_addr *addr)
|
|
{
|
|
return in6_multicast_scope(addr) <= IPV6_MULTICAST_SCOPE_LINK;
|
|
}
|
|
|
|
/*
|
|
* (S,G) -> subscriber,(S,G)
|
|
*/
|
|
|
|
static int gm_packet_sg_cmp(const struct gm_packet_sg *a,
|
|
const struct gm_packet_sg *b)
|
|
{
|
|
const struct gm_packet_state *s_a, *s_b;
|
|
|
|
s_a = gm_packet_sg2state(a);
|
|
s_b = gm_packet_sg2state(b);
|
|
return IPV6_ADDR_CMP(&s_a->subscriber->addr, &s_b->subscriber->addr);
|
|
}
|
|
|
|
DECLARE_RBTREE_UNIQ(gm_packet_sg_subs, struct gm_packet_sg, subs_itm,
|
|
gm_packet_sg_cmp);
|
|
|
|
static struct gm_packet_sg *gm_packet_sg_find(struct gm_sg *sg,
|
|
enum gm_sub_sense sense,
|
|
struct gm_subscriber *sub)
|
|
{
|
|
struct {
|
|
struct gm_packet_state hdr;
|
|
struct gm_packet_sg item;
|
|
} ref = {
|
|
/* clang-format off */
|
|
.hdr = {
|
|
.subscriber = sub,
|
|
},
|
|
.item = {
|
|
.offset = 0,
|
|
},
|
|
/* clang-format on */
|
|
};
|
|
|
|
return gm_packet_sg_subs_find(&sg->subs[sense], &ref.item);
|
|
}
|
|
|
|
/*
|
|
* interface -> (*,G),pending
|
|
*/
|
|
|
|
static int gm_grp_pending_cmp(const struct gm_grp_pending *a,
|
|
const struct gm_grp_pending *b)
|
|
{
|
|
return IPV6_ADDR_CMP(&a->grp, &b->grp);
|
|
}
|
|
|
|
DECLARE_RBTREE_UNIQ(gm_grp_pends, struct gm_grp_pending, itm,
|
|
gm_grp_pending_cmp);
|
|
|
|
/*
|
|
* interface -> ([S1,S2,...],G),pending
|
|
*/
|
|
|
|
static int gm_gsq_pending_cmp(const struct gm_gsq_pending *a,
|
|
const struct gm_gsq_pending *b)
|
|
{
|
|
if (a->s_bit != b->s_bit)
|
|
return numcmp(a->s_bit, b->s_bit);
|
|
|
|
return IPV6_ADDR_CMP(&a->grp, &b->grp);
|
|
}
|
|
|
|
static uint32_t gm_gsq_pending_hash(const struct gm_gsq_pending *a)
|
|
{
|
|
uint32_t seed = a->s_bit ? 0x68f0eb5e : 0x156b7f19;
|
|
|
|
return jhash(&a->grp, sizeof(a->grp), seed);
|
|
}
|
|
|
|
DECLARE_HASH(gm_gsq_pends, struct gm_gsq_pending, itm, gm_gsq_pending_cmp,
|
|
gm_gsq_pending_hash);
|
|
|
|
/*
|
|
* interface -> (S,G)
|
|
*/
|
|
|
|
int gm_sg_cmp(const struct gm_sg *a, const struct gm_sg *b)
|
|
{
|
|
return pim_sgaddr_cmp(a->sgaddr, b->sgaddr);
|
|
}
|
|
|
|
static struct gm_sg *gm_sg_find(struct gm_if *gm_ifp, pim_addr grp,
|
|
pim_addr src)
|
|
{
|
|
struct gm_sg ref = {};
|
|
|
|
ref.sgaddr.grp = grp;
|
|
ref.sgaddr.src = src;
|
|
return gm_sgs_find(gm_ifp->sgs, &ref);
|
|
}
|
|
|
|
static struct gm_sg *gm_sg_make(struct gm_if *gm_ifp, pim_addr grp,
|
|
pim_addr src)
|
|
{
|
|
struct gm_sg *ret, *prev;
|
|
|
|
ret = XCALLOC(MTYPE_GM_SG, sizeof(*ret));
|
|
ret->sgaddr.grp = grp;
|
|
ret->sgaddr.src = src;
|
|
ret->iface = gm_ifp;
|
|
prev = gm_sgs_add(gm_ifp->sgs, ret);
|
|
|
|
if (prev) {
|
|
XFREE(MTYPE_GM_SG, ret);
|
|
ret = prev;
|
|
} else {
|
|
monotime(&ret->created);
|
|
gm_packet_sg_subs_init(ret->subs_positive);
|
|
gm_packet_sg_subs_init(ret->subs_negative);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* interface -> packets, sorted by expiry (because add_tail insert order)
|
|
*/
|
|
|
|
DECLARE_DLIST(gm_packet_expires, struct gm_packet_state, exp_itm);
|
|
|
|
/*
|
|
* subscriber -> packets
|
|
*/
|
|
|
|
DECLARE_DLIST(gm_packets, struct gm_packet_state, pkt_itm);
|
|
|
|
/*
|
|
* interface -> subscriber
|
|
*/
|
|
|
|
static int gm_subscriber_cmp(const struct gm_subscriber *a,
|
|
const struct gm_subscriber *b)
|
|
{
|
|
return IPV6_ADDR_CMP(&a->addr, &b->addr);
|
|
}
|
|
|
|
static uint32_t gm_subscriber_hash(const struct gm_subscriber *a)
|
|
{
|
|
return jhash(&a->addr, sizeof(a->addr), 0xd0e94ad4);
|
|
}
|
|
|
|
DECLARE_HASH(gm_subscribers, struct gm_subscriber, itm, gm_subscriber_cmp,
|
|
gm_subscriber_hash);
|
|
|
|
static struct gm_subscriber *gm_subscriber_findref(struct gm_if *gm_ifp,
|
|
pim_addr addr)
|
|
{
|
|
struct gm_subscriber ref = {}, *ret;
|
|
|
|
ref.addr = addr;
|
|
ret = gm_subscribers_find(gm_ifp->subscribers, &ref);
|
|
if (ret)
|
|
ret->refcount++;
|
|
return ret;
|
|
}
|
|
|
|
static struct gm_subscriber *gm_subscriber_get(struct gm_if *gm_ifp,
|
|
pim_addr addr)
|
|
{
|
|
struct gm_subscriber ref = {}, *ret;
|
|
|
|
ref.addr = addr;
|
|
ret = gm_subscribers_find(gm_ifp->subscribers, &ref);
|
|
|
|
if (!ret) {
|
|
ret = XCALLOC(MTYPE_GM_SUBSCRIBER, sizeof(*ret));
|
|
ret->iface = gm_ifp;
|
|
ret->addr = addr;
|
|
ret->refcount = 1;
|
|
monotime(&ret->created);
|
|
gm_packets_init(ret->packets);
|
|
|
|
gm_subscribers_add(gm_ifp->subscribers, ret);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static void gm_subscriber_drop(struct gm_subscriber **subp)
|
|
{
|
|
struct gm_subscriber *sub = *subp;
|
|
struct gm_if *gm_ifp;
|
|
|
|
if (!sub)
|
|
return;
|
|
gm_ifp = sub->iface;
|
|
|
|
*subp = NULL;
|
|
sub->refcount--;
|
|
|
|
if (sub->refcount)
|
|
return;
|
|
|
|
gm_subscribers_del(gm_ifp->subscribers, sub);
|
|
XFREE(MTYPE_GM_SUBSCRIBER, sub);
|
|
}
|
|
|
|
/****************************************************************************/
|
|
|
|
/* bundle query timer values for combined v1/v2 handling */
|
|
struct gm_query_timers {
|
|
unsigned int qrv;
|
|
unsigned int max_resp_ms;
|
|
unsigned int qqic_ms;
|
|
|
|
struct timeval fuzz;
|
|
struct timeval expire_wait;
|
|
};
|
|
|
|
static void gm_expiry_calc(struct gm_query_timers *timers)
|
|
{
|
|
unsigned int expire =
|
|
(timers->qrv - 1) * timers->qqic_ms + timers->max_resp_ms;
|
|
ldiv_t exp_div = ldiv(expire, 1000);
|
|
|
|
timers->expire_wait.tv_sec = exp_div.quot;
|
|
timers->expire_wait.tv_usec = exp_div.rem * 1000;
|
|
timeradd(&timers->expire_wait, &timers->fuzz, &timers->expire_wait);
|
|
}
|
|
|
|
static void gm_sg_free(struct gm_sg *sg)
|
|
{
|
|
/* t_sg_expiry is handled before this is reached */
|
|
EVENT_OFF(sg->t_sg_query);
|
|
gm_packet_sg_subs_fini(sg->subs_negative);
|
|
gm_packet_sg_subs_fini(sg->subs_positive);
|
|
XFREE(MTYPE_GM_SG, sg);
|
|
}
|
|
|
|
/* clang-format off */
|
|
static const char *const gm_states[] = {
|
|
[GM_SG_NOINFO] = "NOINFO",
|
|
[GM_SG_JOIN] = "JOIN",
|
|
[GM_SG_JOIN_EXPIRING] = "JOIN_EXPIRING",
|
|
[GM_SG_PRUNE] = "PRUNE",
|
|
[GM_SG_NOPRUNE] = "NOPRUNE",
|
|
[GM_SG_NOPRUNE_EXPIRING] = "NOPRUNE_EXPIRING",
|
|
};
|
|
/* clang-format on */
|
|
|
|
/* TODO: S,G entries in EXCLUDE (i.e. prune) unsupported" */
|
|
|
|
/* tib_sg_gm_prune() below is an "un-join", it doesn't prune S,G when *,G is
|
|
* joined. Whether we actually want/need to support this is a separate
|
|
* question - it is almost never used. In fact this is exactly what RFC5790
|
|
* ("lightweight" MLDv2) does: it removes S,G EXCLUDE support.
|
|
*/
|
|
|
|
static void gm_sg_update(struct gm_sg *sg, bool has_expired)
|
|
{
|
|
struct gm_if *gm_ifp = sg->iface;
|
|
struct pim_interface *pim_ifp = gm_ifp->ifp->info;
|
|
enum gm_sg_state prev, desired;
|
|
bool new_join;
|
|
struct gm_sg *grp = NULL;
|
|
|
|
if (!pim_addr_is_any(sg->sgaddr.src))
|
|
grp = gm_sg_find(gm_ifp, sg->sgaddr.grp, PIMADDR_ANY);
|
|
else
|
|
assert(sg->state != GM_SG_PRUNE);
|
|
|
|
if (gm_packet_sg_subs_count(sg->subs_positive)) {
|
|
desired = GM_SG_JOIN;
|
|
assert(!sg->t_sg_expire);
|
|
} else if ((sg->state == GM_SG_JOIN ||
|
|
sg->state == GM_SG_JOIN_EXPIRING) &&
|
|
!has_expired)
|
|
desired = GM_SG_JOIN_EXPIRING;
|
|
else if (!grp || !gm_packet_sg_subs_count(grp->subs_positive))
|
|
desired = GM_SG_NOINFO;
|
|
else if (gm_packet_sg_subs_count(grp->subs_positive) ==
|
|
gm_packet_sg_subs_count(sg->subs_negative)) {
|
|
if ((sg->state == GM_SG_NOPRUNE ||
|
|
sg->state == GM_SG_NOPRUNE_EXPIRING) &&
|
|
!has_expired)
|
|
desired = GM_SG_NOPRUNE_EXPIRING;
|
|
else
|
|
desired = GM_SG_PRUNE;
|
|
} else if (gm_packet_sg_subs_count(sg->subs_negative))
|
|
desired = GM_SG_NOPRUNE;
|
|
else
|
|
desired = GM_SG_NOINFO;
|
|
|
|
if (desired != sg->state && !gm_ifp->stopping) {
|
|
if (PIM_DEBUG_GM_EVENTS)
|
|
zlog_debug(log_sg(sg, "%s => %s"), gm_states[sg->state],
|
|
gm_states[desired]);
|
|
|
|
if (desired == GM_SG_JOIN_EXPIRING ||
|
|
desired == GM_SG_NOPRUNE_EXPIRING) {
|
|
struct gm_query_timers timers;
|
|
|
|
timers.qrv = gm_ifp->cur_qrv;
|
|
timers.max_resp_ms = gm_ifp->cur_max_resp;
|
|
timers.qqic_ms = gm_ifp->cur_query_intv_trig;
|
|
timers.fuzz = gm_ifp->cfg_timing_fuzz;
|
|
|
|
gm_expiry_calc(&timers);
|
|
gm_sg_timer_start(gm_ifp, sg, timers.expire_wait);
|
|
|
|
EVENT_OFF(sg->t_sg_query);
|
|
sg->query_sbit = false;
|
|
/* Trigger the specific queries only for querier. */
|
|
if (IPV6_ADDR_SAME(&gm_ifp->querier, &pim_ifp->ll_lowest)) {
|
|
sg->n_query = gm_ifp->cur_lmqc;
|
|
gm_trigger_specific(sg);
|
|
}
|
|
}
|
|
}
|
|
prev = sg->state;
|
|
sg->state = desired;
|
|
|
|
if (in6_multicast_nofwd(&sg->sgaddr.grp) || gm_ifp->stopping)
|
|
new_join = false;
|
|
else
|
|
new_join = gm_sg_state_want_join(desired);
|
|
|
|
if (new_join && !sg->tib_joined) {
|
|
/* this will retry if join previously failed */
|
|
sg->tib_joined = tib_sg_gm_join(gm_ifp->pim, sg->sgaddr,
|
|
gm_ifp->ifp, &sg->oil);
|
|
if (!sg->tib_joined)
|
|
zlog_warn(
|
|
"MLD join for %pSG%%%s not propagated into TIB",
|
|
&sg->sgaddr, gm_ifp->ifp->name);
|
|
else
|
|
zlog_info(log_ifp("%pSG%%%s TIB joined"), &sg->sgaddr,
|
|
gm_ifp->ifp->name);
|
|
|
|
} else if (sg->tib_joined && !new_join) {
|
|
tib_sg_gm_prune(gm_ifp->pim, sg->sgaddr, gm_ifp->ifp, &sg->oil);
|
|
|
|
sg->oil = NULL;
|
|
sg->tib_joined = false;
|
|
}
|
|
|
|
if (desired == GM_SG_NOINFO) {
|
|
/* multiple paths can lead to the last state going away;
|
|
* t_sg_expire can still be running if we're arriving from
|
|
* another path.
|
|
*/
|
|
if (has_expired)
|
|
EVENT_OFF(sg->t_sg_expire);
|
|
|
|
assertf((!sg->t_sg_expire &&
|
|
!gm_packet_sg_subs_count(sg->subs_positive) &&
|
|
!gm_packet_sg_subs_count(sg->subs_negative)),
|
|
"%pSG%%%s hx=%u exp=%pTHD state=%s->%s pos=%zu neg=%zu grp=%p",
|
|
&sg->sgaddr, gm_ifp->ifp->name, has_expired,
|
|
sg->t_sg_expire, gm_states[prev], gm_states[desired],
|
|
gm_packet_sg_subs_count(sg->subs_positive),
|
|
gm_packet_sg_subs_count(sg->subs_negative), grp);
|
|
|
|
if (PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_sg(sg, "dropping"));
|
|
|
|
gm_sgs_del(gm_ifp->sgs, sg);
|
|
gm_sg_free(sg);
|
|
}
|
|
}
|
|
|
|
/****************************************************************************/
|
|
|
|
/* the following bunch of functions deals with transferring state from
|
|
* received packets into gm_packet_state. As a reminder, the querier is
|
|
* structured to keep all items received in one packet together, since they
|
|
* will share expiry timers and thus allows efficient handling.
|
|
*/
|
|
|
|
static void gm_packet_free(struct gm_packet_state *pkt)
|
|
{
|
|
gm_packet_expires_del(pkt->iface->expires, pkt);
|
|
gm_packets_del(pkt->subscriber->packets, pkt);
|
|
gm_subscriber_drop(&pkt->subscriber);
|
|
XFREE(MTYPE_GM_STATE, pkt);
|
|
}
|
|
|
|
static struct gm_packet_sg *gm_packet_sg_setup(struct gm_packet_state *pkt,
|
|
struct gm_sg *sg, bool is_excl,
|
|
bool is_src)
|
|
{
|
|
struct gm_packet_sg *item;
|
|
|
|
assert(pkt->n_active < pkt->n_sg);
|
|
|
|
item = &pkt->items[pkt->n_active];
|
|
item->sg = sg;
|
|
item->is_excl = is_excl;
|
|
item->is_src = is_src;
|
|
item->offset = pkt->n_active;
|
|
|
|
pkt->n_active++;
|
|
return item;
|
|
}
|
|
|
|
static bool gm_packet_sg_drop(struct gm_packet_sg *item)
|
|
{
|
|
struct gm_packet_state *pkt;
|
|
size_t i;
|
|
|
|
assert(item->sg);
|
|
|
|
pkt = gm_packet_sg2state(item);
|
|
if (item->sg->most_recent == item)
|
|
item->sg->most_recent = NULL;
|
|
|
|
for (i = 0; i < item->n_exclude; i++) {
|
|
struct gm_packet_sg *excl_item;
|
|
|
|
excl_item = item + 1 + i;
|
|
if (!excl_item->sg)
|
|
continue;
|
|
|
|
gm_packet_sg_subs_del(excl_item->sg->subs_negative, excl_item);
|
|
excl_item->sg = NULL;
|
|
pkt->n_active--;
|
|
|
|
assert(pkt->n_active > 0);
|
|
}
|
|
|
|
if (item->is_excl && item->is_src)
|
|
gm_packet_sg_subs_del(item->sg->subs_negative, item);
|
|
else
|
|
gm_packet_sg_subs_del(item->sg->subs_positive, item);
|
|
item->sg = NULL;
|
|
pkt->n_active--;
|
|
|
|
if (!pkt->n_active) {
|
|
gm_packet_free(pkt);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void gm_packet_drop(struct gm_packet_state *pkt, bool trace)
|
|
{
|
|
for (size_t i = 0; i < pkt->n_sg; i++) {
|
|
struct gm_sg *sg = pkt->items[i].sg;
|
|
bool deleted;
|
|
|
|
if (!sg)
|
|
continue;
|
|
|
|
if (trace && PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_sg(sg, "general-dropping from %pPA"),
|
|
&pkt->subscriber->addr);
|
|
deleted = gm_packet_sg_drop(&pkt->items[i]);
|
|
|
|
gm_sg_update(sg, true);
|
|
if (deleted)
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void gm_packet_sg_remove_sources(struct gm_if *gm_ifp,
|
|
struct gm_subscriber *subscriber,
|
|
pim_addr grp, pim_addr *srcs,
|
|
size_t n_src, enum gm_sub_sense sense)
|
|
{
|
|
struct gm_sg *sg;
|
|
struct gm_packet_sg *old_src;
|
|
size_t i;
|
|
|
|
for (i = 0; i < n_src; i++) {
|
|
sg = gm_sg_find(gm_ifp, grp, srcs[i]);
|
|
if (!sg)
|
|
continue;
|
|
|
|
old_src = gm_packet_sg_find(sg, sense, subscriber);
|
|
if (!old_src)
|
|
continue;
|
|
|
|
gm_packet_sg_drop(old_src);
|
|
gm_sg_update(sg, false);
|
|
}
|
|
}
|
|
|
|
static void gm_sg_expiry_cancel(struct gm_sg *sg)
|
|
{
|
|
if (sg->t_sg_expire && PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_sg(sg, "alive, cancelling expiry timer"));
|
|
EVENT_OFF(sg->t_sg_expire);
|
|
sg->query_sbit = true;
|
|
}
|
|
|
|
/* first pass: process all changes resulting in removal of state:
|
|
* - {TO,IS}_INCLUDE removes *,G EXCLUDE state (and S,G)
|
|
* - ALLOW_NEW_SOURCES, if *,G in EXCLUDE removes S,G state
|
|
* - BLOCK_OLD_SOURCES, if *,G in INCLUDE removes S,G state
|
|
* - {TO,IS}_EXCLUDE, if *,G in INCLUDE removes S,G state
|
|
* note *replacing* state is NOT considered *removing* state here
|
|
*
|
|
* everything else is thrown into pkt for creation of state in pass 2
|
|
*/
|
|
static void gm_handle_v2_pass1(struct gm_packet_state *pkt,
|
|
struct mld_v2_rec_hdr *rechdr, size_t n_src)
|
|
{
|
|
/* NB: pkt->subscriber can be NULL here if the subscriber was not
|
|
* previously seen!
|
|
*/
|
|
struct gm_subscriber *subscriber = pkt->subscriber;
|
|
struct gm_sg *grp;
|
|
struct gm_packet_sg *old_grp = NULL;
|
|
struct gm_packet_sg *item;
|
|
size_t j;
|
|
bool is_excl = false;
|
|
|
|
grp = gm_sg_find(pkt->iface, rechdr->grp, PIMADDR_ANY);
|
|
if (grp && subscriber)
|
|
old_grp = gm_packet_sg_find(grp, GM_SUB_POS, subscriber);
|
|
|
|
assert(old_grp == NULL || old_grp->is_excl);
|
|
|
|
switch (rechdr->type) {
|
|
case MLD_RECTYPE_IS_EXCLUDE:
|
|
case MLD_RECTYPE_CHANGE_TO_EXCLUDE:
|
|
/* this always replaces or creates state */
|
|
is_excl = true;
|
|
if (!grp)
|
|
grp = gm_sg_make(pkt->iface, rechdr->grp, PIMADDR_ANY);
|
|
|
|
item = gm_packet_sg_setup(pkt, grp, is_excl, false);
|
|
item->n_exclude = n_src;
|
|
|
|
/* [EXCL_INCL_SG_NOTE] referenced below
|
|
*
|
|
* in theory, we should drop any S,G that the host may have
|
|
* previously added in INCLUDE mode. In practice, this is both
|
|
* incredibly rare and entirely irrelevant. It only makes any
|
|
* difference if an S,G that the host previously had on the
|
|
* INCLUDE list is now on the blocked list for EXCLUDE, which
|
|
* we can cover in processing the S,G list in pass2_excl().
|
|
*
|
|
* Other S,G from the host are simply left to expire
|
|
* "naturally" through general expiry.
|
|
*/
|
|
break;
|
|
|
|
case MLD_RECTYPE_IS_INCLUDE:
|
|
case MLD_RECTYPE_CHANGE_TO_INCLUDE:
|
|
if (old_grp) {
|
|
/* INCLUDE has no *,G state, so old_grp here refers to
|
|
* previous EXCLUDE => delete it
|
|
*/
|
|
gm_packet_sg_drop(old_grp);
|
|
gm_sg_update(grp, false);
|
|
/* TODO "need S,G PRUNE => NO_INFO transition here" */
|
|
}
|
|
break;
|
|
|
|
case MLD_RECTYPE_ALLOW_NEW_SOURCES:
|
|
if (old_grp) {
|
|
/* remove S,Gs from EXCLUDE, and then we're done */
|
|
gm_packet_sg_remove_sources(pkt->iface, subscriber,
|
|
rechdr->grp, rechdr->srcs,
|
|
n_src, GM_SUB_NEG);
|
|
return;
|
|
}
|
|
/* in INCLUDE mode => ALLOW_NEW_SOURCES is functionally
|
|
* idential to IS_INCLUDE (because the list of sources in
|
|
* IS_INCLUDE is not exhaustive)
|
|
*/
|
|
break;
|
|
|
|
case MLD_RECTYPE_BLOCK_OLD_SOURCES:
|
|
if (old_grp) {
|
|
/* this is intentionally not implemented because it
|
|
* would be complicated as hell. we only take the list
|
|
* of blocked sources from full group state records
|
|
*/
|
|
return;
|
|
}
|
|
|
|
if (subscriber)
|
|
gm_packet_sg_remove_sources(pkt->iface, subscriber,
|
|
rechdr->grp, rechdr->srcs,
|
|
n_src, GM_SUB_POS);
|
|
return;
|
|
}
|
|
|
|
for (j = 0; j < n_src; j++) {
|
|
struct gm_sg *sg;
|
|
|
|
sg = gm_sg_find(pkt->iface, rechdr->grp, rechdr->srcs[j]);
|
|
if (!sg)
|
|
sg = gm_sg_make(pkt->iface, rechdr->grp,
|
|
rechdr->srcs[j]);
|
|
|
|
gm_packet_sg_setup(pkt, sg, is_excl, true);
|
|
}
|
|
}
|
|
|
|
/* second pass: creating/updating/refreshing state. All the items from the
|
|
* received packet have already been thrown into gm_packet_state.
|
|
*/
|
|
|
|
static void gm_handle_v2_pass2_incl(struct gm_packet_state *pkt, size_t i)
|
|
{
|
|
struct gm_packet_sg *item = &pkt->items[i];
|
|
struct gm_packet_sg *old = NULL;
|
|
struct gm_sg *sg = item->sg;
|
|
|
|
/* EXCLUDE state was already dropped in pass1 */
|
|
assert(!gm_packet_sg_find(sg, GM_SUB_NEG, pkt->subscriber));
|
|
|
|
old = gm_packet_sg_find(sg, GM_SUB_POS, pkt->subscriber);
|
|
if (old)
|
|
gm_packet_sg_drop(old);
|
|
|
|
pkt->n_active++;
|
|
gm_packet_sg_subs_add(sg->subs_positive, item);
|
|
|
|
sg->most_recent = item;
|
|
gm_sg_expiry_cancel(sg);
|
|
gm_sg_update(sg, false);
|
|
}
|
|
|
|
static void gm_handle_v2_pass2_excl(struct gm_packet_state *pkt, size_t offs)
|
|
{
|
|
struct gm_packet_sg *item = &pkt->items[offs];
|
|
struct gm_packet_sg *old_grp, *item_dup;
|
|
struct gm_sg *sg_grp = item->sg;
|
|
size_t i;
|
|
|
|
old_grp = gm_packet_sg_find(sg_grp, GM_SUB_POS, pkt->subscriber);
|
|
if (old_grp) {
|
|
for (i = 0; i < item->n_exclude; i++) {
|
|
struct gm_packet_sg *item_src, *old_src;
|
|
|
|
item_src = &pkt->items[offs + 1 + i];
|
|
old_src = gm_packet_sg_find(item_src->sg, GM_SUB_NEG,
|
|
pkt->subscriber);
|
|
if (old_src)
|
|
gm_packet_sg_drop(old_src);
|
|
|
|
/* See [EXCL_INCL_SG_NOTE] above - we can have old S,G
|
|
* items left over if the host previously had INCLUDE
|
|
* mode going. Remove them here if we find any.
|
|
*/
|
|
old_src = gm_packet_sg_find(item_src->sg, GM_SUB_POS,
|
|
pkt->subscriber);
|
|
if (old_src)
|
|
gm_packet_sg_drop(old_src);
|
|
}
|
|
|
|
/* the previous loop has removed the S,G entries which are
|
|
* still excluded after this update. So anything left on the
|
|
* old item was previously excluded but is now included
|
|
* => need to trigger update on S,G
|
|
*/
|
|
for (i = 0; i < old_grp->n_exclude; i++) {
|
|
struct gm_packet_sg *old_src;
|
|
struct gm_sg *old_sg_src;
|
|
|
|
old_src = old_grp + 1 + i;
|
|
old_sg_src = old_src->sg;
|
|
if (!old_sg_src)
|
|
continue;
|
|
|
|
gm_packet_sg_drop(old_src);
|
|
gm_sg_update(old_sg_src, false);
|
|
}
|
|
|
|
gm_packet_sg_drop(old_grp);
|
|
}
|
|
|
|
item_dup = gm_packet_sg_subs_add(sg_grp->subs_positive, item);
|
|
assert(!item_dup);
|
|
pkt->n_active++;
|
|
|
|
sg_grp->most_recent = item;
|
|
gm_sg_expiry_cancel(sg_grp);
|
|
|
|
for (i = 0; i < item->n_exclude; i++) {
|
|
struct gm_packet_sg *item_src;
|
|
|
|
item_src = &pkt->items[offs + 1 + i];
|
|
item_dup = gm_packet_sg_subs_add(item_src->sg->subs_negative,
|
|
item_src);
|
|
|
|
if (item_dup)
|
|
item_src->sg = NULL;
|
|
else {
|
|
pkt->n_active++;
|
|
gm_sg_update(item_src->sg, false);
|
|
}
|
|
}
|
|
|
|
/* TODO: determine best ordering between gm_sg_update(S,G) and (*,G)
|
|
* to get lower PIM churn/flapping
|
|
*/
|
|
gm_sg_update(sg_grp, false);
|
|
}
|
|
|
|
/* TODO: QRV/QQIC are not copied from queries to local state" */
|
|
|
|
/* on receiving a query, we need to update our robustness/query interval to
|
|
* match, so we correctly process group/source specific queries after last
|
|
* member leaves
|
|
*/
|
|
|
|
static void gm_handle_v2_report(struct gm_if *gm_ifp,
|
|
const struct sockaddr_in6 *pkt_src, char *data,
|
|
size_t len)
|
|
{
|
|
struct mld_v2_report_hdr *hdr;
|
|
size_t i, n_records, max_entries;
|
|
struct gm_packet_state *pkt;
|
|
|
|
if (len < sizeof(*hdr)) {
|
|
if (PIM_DEBUG_GM_PACKETS)
|
|
zlog_debug(log_pkt_src(
|
|
"malformed MLDv2 report (truncated header)"));
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
return;
|
|
}
|
|
|
|
hdr = (struct mld_v2_report_hdr *)data;
|
|
data += sizeof(*hdr);
|
|
len -= sizeof(*hdr);
|
|
|
|
n_records = ntohs(hdr->n_records);
|
|
if (n_records > len / sizeof(struct mld_v2_rec_hdr)) {
|
|
/* note this is only an upper bound, records with source lists
|
|
* are larger. This is mostly here to make coverity happy.
|
|
*/
|
|
zlog_warn(log_pkt_src(
|
|
"malformed MLDv2 report (infeasible record count)"));
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
return;
|
|
}
|
|
|
|
/* errors after this may at least partially process the packet */
|
|
gm_ifp->stats.rx_new_report++;
|
|
|
|
/* can't have more *,G and S,G items than there is space for ipv6
|
|
* addresses, so just use this to allocate temporary buffer
|
|
*/
|
|
max_entries = len / sizeof(pim_addr);
|
|
pkt = XCALLOC(MTYPE_GM_STATE,
|
|
offsetof(struct gm_packet_state, items[max_entries]));
|
|
pkt->n_sg = max_entries;
|
|
pkt->iface = gm_ifp;
|
|
pkt->subscriber = gm_subscriber_findref(gm_ifp, pkt_src->sin6_addr);
|
|
|
|
/* validate & remove state in v2_pass1() */
|
|
for (i = 0; i < n_records; i++) {
|
|
struct mld_v2_rec_hdr *rechdr;
|
|
size_t n_src, record_size;
|
|
|
|
if (len < sizeof(*rechdr)) {
|
|
zlog_warn(log_pkt_src(
|
|
"malformed MLDv2 report (truncated record header)"));
|
|
gm_ifp->stats.rx_trunc_report++;
|
|
break;
|
|
}
|
|
|
|
rechdr = (struct mld_v2_rec_hdr *)data;
|
|
data += sizeof(*rechdr);
|
|
len -= sizeof(*rechdr);
|
|
|
|
n_src = ntohs(rechdr->n_src);
|
|
record_size = n_src * sizeof(pim_addr) + rechdr->aux_len * 4;
|
|
|
|
if (len < record_size) {
|
|
zlog_warn(log_pkt_src(
|
|
"malformed MLDv2 report (truncated source list)"));
|
|
gm_ifp->stats.rx_trunc_report++;
|
|
break;
|
|
}
|
|
if (!IN6_IS_ADDR_MULTICAST(&rechdr->grp)) {
|
|
zlog_warn(
|
|
log_pkt_src(
|
|
"malformed MLDv2 report (invalid group %pI6)"),
|
|
&rechdr->grp);
|
|
gm_ifp->stats.rx_trunc_report++;
|
|
break;
|
|
}
|
|
|
|
data += record_size;
|
|
len -= record_size;
|
|
|
|
gm_handle_v2_pass1(pkt, rechdr, n_src);
|
|
}
|
|
|
|
if (!pkt->n_active) {
|
|
gm_subscriber_drop(&pkt->subscriber);
|
|
XFREE(MTYPE_GM_STATE, pkt);
|
|
return;
|
|
}
|
|
|
|
pkt = XREALLOC(MTYPE_GM_STATE, pkt,
|
|
offsetof(struct gm_packet_state, items[pkt->n_active]));
|
|
pkt->n_sg = pkt->n_active;
|
|
pkt->n_active = 0;
|
|
|
|
monotime(&pkt->received);
|
|
if (!pkt->subscriber)
|
|
pkt->subscriber = gm_subscriber_get(gm_ifp, pkt_src->sin6_addr);
|
|
gm_packets_add_tail(pkt->subscriber->packets, pkt);
|
|
gm_packet_expires_add_tail(gm_ifp->expires, pkt);
|
|
|
|
for (i = 0; i < pkt->n_sg; i++)
|
|
if (!pkt->items[i].is_excl)
|
|
gm_handle_v2_pass2_incl(pkt, i);
|
|
else {
|
|
gm_handle_v2_pass2_excl(pkt, i);
|
|
i += pkt->items[i].n_exclude;
|
|
}
|
|
|
|
if (pkt->n_active == 0)
|
|
gm_packet_free(pkt);
|
|
}
|
|
|
|
static void gm_handle_v1_report(struct gm_if *gm_ifp,
|
|
const struct sockaddr_in6 *pkt_src, char *data,
|
|
size_t len)
|
|
{
|
|
struct mld_v1_pkt *hdr;
|
|
struct gm_packet_state *pkt;
|
|
struct gm_sg *grp;
|
|
struct gm_packet_sg *item;
|
|
size_t max_entries;
|
|
|
|
if (len < sizeof(*hdr)) {
|
|
if (PIM_DEBUG_GM_PACKETS)
|
|
zlog_debug(log_pkt_src(
|
|
"malformed MLDv1 report (truncated)"));
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
return;
|
|
}
|
|
|
|
gm_ifp->stats.rx_old_report++;
|
|
|
|
hdr = (struct mld_v1_pkt *)data;
|
|
|
|
max_entries = 1;
|
|
pkt = XCALLOC(MTYPE_GM_STATE,
|
|
offsetof(struct gm_packet_state, items[max_entries]));
|
|
pkt->n_sg = max_entries;
|
|
pkt->iface = gm_ifp;
|
|
pkt->subscriber = gm_subscriber_findref(gm_ifp, gm_dummy_untracked);
|
|
|
|
/* { equivalent of gm_handle_v2_pass1() with IS_EXCLUDE */
|
|
|
|
grp = gm_sg_find(pkt->iface, hdr->grp, PIMADDR_ANY);
|
|
if (!grp)
|
|
grp = gm_sg_make(pkt->iface, hdr->grp, PIMADDR_ANY);
|
|
|
|
item = gm_packet_sg_setup(pkt, grp, true, false);
|
|
item->n_exclude = 0;
|
|
|
|
/* TODO "set v1-seen timer on grp here" */
|
|
|
|
/* } */
|
|
|
|
/* pass2 will count n_active back up to 1. Also since a v1 report
|
|
* has exactly 1 group, we can skip the realloc() that v2 needs here.
|
|
*/
|
|
assert(pkt->n_active == 1);
|
|
pkt->n_sg = pkt->n_active;
|
|
pkt->n_active = 0;
|
|
|
|
monotime(&pkt->received);
|
|
if (!pkt->subscriber)
|
|
pkt->subscriber = gm_subscriber_get(gm_ifp, gm_dummy_untracked);
|
|
gm_packets_add_tail(pkt->subscriber->packets, pkt);
|
|
gm_packet_expires_add_tail(gm_ifp->expires, pkt);
|
|
|
|
/* pass2 covers installing state & removing old state; all the v1
|
|
* compat is handled at this point.
|
|
*
|
|
* Note that "old state" may be v2; subscribers will switch from v2
|
|
* reports to v1 reports when the querier changes from v2 to v1. So,
|
|
* limiting this to v1 would be wrong.
|
|
*/
|
|
gm_handle_v2_pass2_excl(pkt, 0);
|
|
|
|
if (pkt->n_active == 0)
|
|
gm_packet_free(pkt);
|
|
}
|
|
|
|
static void gm_handle_v1_leave(struct gm_if *gm_ifp,
|
|
const struct sockaddr_in6 *pkt_src, char *data,
|
|
size_t len)
|
|
{
|
|
struct mld_v1_pkt *hdr;
|
|
struct gm_subscriber *subscriber;
|
|
struct gm_sg *grp;
|
|
struct gm_packet_sg *old_grp;
|
|
|
|
if (len < sizeof(*hdr)) {
|
|
if (PIM_DEBUG_GM_PACKETS)
|
|
zlog_debug(log_pkt_src(
|
|
"malformed MLDv1 leave (truncated)"));
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
return;
|
|
}
|
|
|
|
gm_ifp->stats.rx_old_leave++;
|
|
|
|
hdr = (struct mld_v1_pkt *)data;
|
|
|
|
subscriber = gm_subscriber_findref(gm_ifp, gm_dummy_untracked);
|
|
if (!subscriber)
|
|
return;
|
|
|
|
/* { equivalent of gm_handle_v2_pass1() with IS_INCLUDE */
|
|
|
|
grp = gm_sg_find(gm_ifp, hdr->grp, PIMADDR_ANY);
|
|
if (grp) {
|
|
old_grp = gm_packet_sg_find(grp, GM_SUB_POS, subscriber);
|
|
if (old_grp) {
|
|
gm_packet_sg_drop(old_grp);
|
|
gm_sg_update(grp, false);
|
|
|
|
/* TODO "need S,G PRUNE => NO_INFO transition here" */
|
|
|
|
}
|
|
}
|
|
|
|
/* } */
|
|
|
|
/* nothing more to do here, pass2 is no-op for leaves */
|
|
gm_subscriber_drop(&subscriber);
|
|
}
|
|
|
|
/* for each general query received (or sent), a timer is started to expire
|
|
* _everything_ at the appropriate time (including robustness multiplier).
|
|
*
|
|
* So when this timer hits, all packets - with all of their items - that were
|
|
* received *before* the query are aged out, and state updated accordingly.
|
|
* Note that when we receive a refresh/update, the previous/old packet is
|
|
* already dropped and replaced with a new one, so in normal steady-state
|
|
* operation, this timer won't be doing anything.
|
|
*
|
|
* Additionally, if a subscriber actively leaves a group, that goes through
|
|
* its own path too and won't hit this. This is really only triggered when a
|
|
* host straight up disappears.
|
|
*/
|
|
static void gm_t_expire(struct event *t)
|
|
{
|
|
struct gm_if *gm_ifp = EVENT_ARG(t);
|
|
struct gm_packet_state *pkt;
|
|
|
|
zlog_info(log_ifp("general expiry timer"));
|
|
|
|
while (gm_ifp->n_pending) {
|
|
struct gm_general_pending *pend = gm_ifp->pending;
|
|
struct timeval remain;
|
|
int64_t remain_ms;
|
|
|
|
remain_ms = monotime_until(&pend->expiry, &remain);
|
|
if (remain_ms > 0) {
|
|
if (PIM_DEBUG_GM_EVENTS)
|
|
zlog_debug(
|
|
log_ifp("next general expiry in %" PRId64 "ms"),
|
|
remain_ms / 1000);
|
|
|
|
event_add_timer_tv(router->master, gm_t_expire, gm_ifp,
|
|
&remain, &gm_ifp->t_expire);
|
|
return;
|
|
}
|
|
|
|
while ((pkt = gm_packet_expires_first(gm_ifp->expires))) {
|
|
if (timercmp(&pkt->received, &pend->query, >=))
|
|
break;
|
|
|
|
if (PIM_DEBUG_GM_PACKETS)
|
|
zlog_debug(log_ifp("expire packet %p"), pkt);
|
|
gm_packet_drop(pkt, true);
|
|
}
|
|
|
|
gm_ifp->n_pending--;
|
|
memmove(gm_ifp->pending, gm_ifp->pending + 1,
|
|
gm_ifp->n_pending * sizeof(gm_ifp->pending[0]));
|
|
}
|
|
|
|
if (PIM_DEBUG_GM_EVENTS)
|
|
zlog_debug(log_ifp("next general expiry waiting for query"));
|
|
}
|
|
|
|
/* NB: the receive handlers will also run when sending packets, since we
|
|
* receive our own packets back in.
|
|
*/
|
|
static void gm_handle_q_general(struct gm_if *gm_ifp,
|
|
struct gm_query_timers *timers)
|
|
{
|
|
struct timeval now, expiry;
|
|
struct gm_general_pending *pend;
|
|
|
|
monotime(&now);
|
|
timeradd(&now, &timers->expire_wait, &expiry);
|
|
|
|
while (gm_ifp->n_pending) {
|
|
pend = &gm_ifp->pending[gm_ifp->n_pending - 1];
|
|
|
|
if (timercmp(&pend->expiry, &expiry, <))
|
|
break;
|
|
|
|
/* if we end up here, the last item in pending[] has an expiry
|
|
* later than the expiry for this query. But our query time
|
|
* (now) is later than that of the item (because, well, that's
|
|
* how time works.) This makes this query meaningless since
|
|
* it's "supersetted" within the preexisting query
|
|
*/
|
|
|
|
if (PIM_DEBUG_GM_TRACE_DETAIL)
|
|
zlog_debug(
|
|
log_ifp("zapping supersetted general timer %pTVMu"),
|
|
&pend->expiry);
|
|
|
|
gm_ifp->n_pending--;
|
|
if (!gm_ifp->n_pending)
|
|
EVENT_OFF(gm_ifp->t_expire);
|
|
}
|
|
|
|
/* people might be messing with their configs or something */
|
|
if (gm_ifp->n_pending == array_size(gm_ifp->pending))
|
|
return;
|
|
|
|
pend = &gm_ifp->pending[gm_ifp->n_pending];
|
|
pend->query = now;
|
|
pend->expiry = expiry;
|
|
|
|
if (!gm_ifp->n_pending++) {
|
|
if (PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(
|
|
log_ifp("starting general timer @ 0: %pTVMu"),
|
|
&pend->expiry);
|
|
event_add_timer_tv(router->master, gm_t_expire, gm_ifp,
|
|
&timers->expire_wait, &gm_ifp->t_expire);
|
|
} else if (PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_ifp("appending general timer @ %u: %pTVMu"),
|
|
gm_ifp->n_pending, &pend->expiry);
|
|
}
|
|
|
|
static void gm_t_sg_expire(struct event *t)
|
|
{
|
|
struct gm_sg *sg = EVENT_ARG(t);
|
|
struct gm_if *gm_ifp = sg->iface;
|
|
struct gm_packet_sg *item;
|
|
|
|
assertf(sg->state == GM_SG_JOIN_EXPIRING ||
|
|
sg->state == GM_SG_NOPRUNE_EXPIRING,
|
|
"%pSG%%%s %pTHD", &sg->sgaddr, gm_ifp->ifp->name, t);
|
|
|
|
frr_each_safe (gm_packet_sg_subs, sg->subs_positive, item)
|
|
/* this will also drop EXCLUDE mode S,G lists together with
|
|
* the *,G entry
|
|
*/
|
|
gm_packet_sg_drop(item);
|
|
|
|
/* subs_negative items are only timed out together with the *,G entry
|
|
* since we won't get any reports for a group-and-source query
|
|
*/
|
|
gm_sg_update(sg, true);
|
|
}
|
|
|
|
static bool gm_sg_check_recent(struct gm_if *gm_ifp, struct gm_sg *sg,
|
|
struct timeval ref)
|
|
{
|
|
struct gm_packet_state *pkt;
|
|
|
|
if (!sg->most_recent) {
|
|
struct gm_packet_state *best_pkt = NULL;
|
|
struct gm_packet_sg *item;
|
|
|
|
frr_each (gm_packet_sg_subs, sg->subs_positive, item) {
|
|
pkt = gm_packet_sg2state(item);
|
|
|
|
if (!best_pkt ||
|
|
timercmp(&pkt->received, &best_pkt->received, >)) {
|
|
best_pkt = pkt;
|
|
sg->most_recent = item;
|
|
}
|
|
}
|
|
}
|
|
if (sg->most_recent) {
|
|
struct timeval fuzz;
|
|
|
|
pkt = gm_packet_sg2state(sg->most_recent);
|
|
|
|
/* this shouldn't happen on plain old real ethernet segment,
|
|
* but on something like a VXLAN or VPLS it is very possible
|
|
* that we get a report before the query that triggered it.
|
|
* (imagine a triangle scenario with 3 datacenters, it's very
|
|
* possible A->B + B->C is faster than A->C due to odd routing)
|
|
*
|
|
* This makes a little tolerance allowance to handle that case.
|
|
*/
|
|
timeradd(&pkt->received, &gm_ifp->cfg_timing_fuzz, &fuzz);
|
|
|
|
if (timercmp(&fuzz, &ref, >))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void gm_sg_timer_start(struct gm_if *gm_ifp, struct gm_sg *sg,
|
|
struct timeval expire_wait)
|
|
{
|
|
struct timeval now;
|
|
|
|
if (!sg)
|
|
return;
|
|
if (sg->state == GM_SG_PRUNE)
|
|
return;
|
|
|
|
monotime(&now);
|
|
if (gm_sg_check_recent(gm_ifp, sg, now))
|
|
return;
|
|
|
|
if (PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_sg(sg, "expiring in %pTVI"), &expire_wait);
|
|
|
|
if (sg->t_sg_expire) {
|
|
struct timeval remain;
|
|
|
|
remain = event_timer_remain(sg->t_sg_expire);
|
|
if (timercmp(&remain, &expire_wait, <=))
|
|
return;
|
|
|
|
EVENT_OFF(sg->t_sg_expire);
|
|
}
|
|
|
|
event_add_timer_tv(router->master, gm_t_sg_expire, sg, &expire_wait,
|
|
&sg->t_sg_expire);
|
|
}
|
|
|
|
static void gm_handle_q_groupsrc(struct gm_if *gm_ifp,
|
|
struct gm_query_timers *timers, pim_addr grp,
|
|
const pim_addr *srcs, size_t n_src)
|
|
{
|
|
struct gm_sg *sg;
|
|
size_t i;
|
|
|
|
for (i = 0; i < n_src; i++) {
|
|
sg = gm_sg_find(gm_ifp, grp, srcs[i]);
|
|
GM_UPDATE_SG_STATE(sg);
|
|
gm_sg_timer_start(gm_ifp, sg, timers->expire_wait);
|
|
}
|
|
}
|
|
|
|
static void gm_t_grp_expire(struct event *t)
|
|
{
|
|
/* if we're here, that means when we received the group-specific query
|
|
* there was one or more active S,G for this group. For *,G the timer
|
|
* in sg->t_sg_expire is running separately and gets cancelled when we
|
|
* receive a report, so that work is left to gm_t_sg_expire and we
|
|
* shouldn't worry about it here.
|
|
*/
|
|
struct gm_grp_pending *pend = EVENT_ARG(t);
|
|
struct gm_if *gm_ifp = pend->iface;
|
|
struct gm_sg *sg, *sg_start, sg_ref = {};
|
|
|
|
if (PIM_DEBUG_GM_EVENTS)
|
|
zlog_debug(log_ifp("*,%pPAs S,G timer expired"), &pend->grp);
|
|
|
|
/* gteq lookup - try to find *,G or S,G (S,G is > *,G)
|
|
* could technically be gt to skip a possible *,G
|
|
*/
|
|
sg_ref.sgaddr.grp = pend->grp;
|
|
sg_ref.sgaddr.src = PIMADDR_ANY;
|
|
sg_start = gm_sgs_find_gteq(gm_ifp->sgs, &sg_ref);
|
|
|
|
frr_each_from (gm_sgs, gm_ifp->sgs, sg, sg_start) {
|
|
struct gm_packet_sg *item;
|
|
|
|
if (pim_addr_cmp(sg->sgaddr.grp, pend->grp))
|
|
break;
|
|
if (pim_addr_is_any(sg->sgaddr.src))
|
|
/* handled by gm_t_sg_expire / sg->t_sg_expire */
|
|
continue;
|
|
if (gm_sg_check_recent(gm_ifp, sg, pend->query))
|
|
continue;
|
|
|
|
/* we may also have a group-source-specific query going on in
|
|
* parallel. But if we received nothing for the *,G query,
|
|
* the S,G query is kinda irrelevant.
|
|
*/
|
|
EVENT_OFF(sg->t_sg_expire);
|
|
|
|
frr_each_safe (gm_packet_sg_subs, sg->subs_positive, item)
|
|
/* this will also drop the EXCLUDE S,G lists */
|
|
gm_packet_sg_drop(item);
|
|
|
|
gm_sg_update(sg, true);
|
|
}
|
|
|
|
gm_grp_pends_del(gm_ifp->grp_pends, pend);
|
|
XFREE(MTYPE_GM_GRP_PENDING, pend);
|
|
}
|
|
|
|
static void gm_handle_q_group(struct gm_if *gm_ifp,
|
|
struct gm_query_timers *timers, pim_addr grp)
|
|
{
|
|
struct gm_sg *sg, sg_ref = {};
|
|
struct gm_grp_pending *pend, pend_ref = {};
|
|
|
|
sg_ref.sgaddr.grp = grp;
|
|
sg_ref.sgaddr.src = PIMADDR_ANY;
|
|
/* gteq lookup - try to find *,G or S,G (S,G is > *,G) */
|
|
sg = gm_sgs_find_gteq(gm_ifp->sgs, &sg_ref);
|
|
|
|
if (!sg || pim_addr_cmp(sg->sgaddr.grp, grp))
|
|
/* we have nothing at all for this group - don't waste RAM */
|
|
return;
|
|
|
|
if (pim_addr_is_any(sg->sgaddr.src)) {
|
|
/* actually found *,G entry here */
|
|
if (PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_ifp("*,%pPAs expiry timer starting"),
|
|
&grp);
|
|
GM_UPDATE_SG_STATE(sg);
|
|
gm_sg_timer_start(gm_ifp, sg, timers->expire_wait);
|
|
|
|
sg = gm_sgs_next(gm_ifp->sgs, sg);
|
|
if (!sg || pim_addr_cmp(sg->sgaddr.grp, grp))
|
|
/* no S,G for this group */
|
|
return;
|
|
}
|
|
|
|
pend_ref.grp = grp;
|
|
pend = gm_grp_pends_find(gm_ifp->grp_pends, &pend_ref);
|
|
|
|
if (pend) {
|
|
struct timeval remain;
|
|
|
|
remain = event_timer_remain(pend->t_expire);
|
|
if (timercmp(&remain, &timers->expire_wait, <=))
|
|
return;
|
|
|
|
EVENT_OFF(pend->t_expire);
|
|
} else {
|
|
pend = XCALLOC(MTYPE_GM_GRP_PENDING, sizeof(*pend));
|
|
pend->grp = grp;
|
|
pend->iface = gm_ifp;
|
|
gm_grp_pends_add(gm_ifp->grp_pends, pend);
|
|
}
|
|
|
|
monotime(&pend->query);
|
|
event_add_timer_tv(router->master, gm_t_grp_expire, pend,
|
|
&timers->expire_wait, &pend->t_expire);
|
|
|
|
if (PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_ifp("*,%pPAs S,G timer started: %pTHD"), &grp,
|
|
pend->t_expire);
|
|
}
|
|
|
|
static void gm_bump_querier(struct gm_if *gm_ifp)
|
|
{
|
|
struct pim_interface *pim_ifp = gm_ifp->ifp->info;
|
|
|
|
EVENT_OFF(gm_ifp->t_query);
|
|
|
|
if (pim_addr_is_any(pim_ifp->ll_lowest))
|
|
return;
|
|
if (!IPV6_ADDR_SAME(&gm_ifp->querier, &pim_ifp->ll_lowest))
|
|
return;
|
|
|
|
gm_ifp->n_startup = gm_ifp->cur_qrv;
|
|
|
|
event_execute(router->master, gm_t_query, gm_ifp, 0, NULL);
|
|
}
|
|
|
|
static void gm_t_other_querier(struct event *t)
|
|
{
|
|
struct gm_if *gm_ifp = EVENT_ARG(t);
|
|
struct pim_interface *pim_ifp = gm_ifp->ifp->info;
|
|
|
|
zlog_info(log_ifp("other querier timer expired"));
|
|
|
|
gm_ifp->querier = pim_ifp->ll_lowest;
|
|
gm_ifp->n_startup = gm_ifp->cur_qrv;
|
|
|
|
event_execute(router->master, gm_t_query, gm_ifp, 0, NULL);
|
|
}
|
|
|
|
static void gm_handle_query(struct gm_if *gm_ifp,
|
|
const struct sockaddr_in6 *pkt_src,
|
|
pim_addr *pkt_dst, char *data, size_t len)
|
|
{
|
|
struct mld_v2_query_hdr *hdr;
|
|
struct pim_interface *pim_ifp = gm_ifp->ifp->info;
|
|
struct gm_query_timers timers;
|
|
bool general_query;
|
|
|
|
if (len < sizeof(struct mld_v2_query_hdr) &&
|
|
len != sizeof(struct mld_v1_pkt)) {
|
|
zlog_warn(log_pkt_src("invalid query size"));
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
return;
|
|
}
|
|
|
|
hdr = (struct mld_v2_query_hdr *)data;
|
|
general_query = pim_addr_is_any(hdr->grp);
|
|
|
|
if (!general_query && !IN6_IS_ADDR_MULTICAST(&hdr->grp)) {
|
|
zlog_warn(log_pkt_src(
|
|
"malformed MLDv2 query (invalid group %pI6)"),
|
|
&hdr->grp);
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
return;
|
|
}
|
|
|
|
if (len >= sizeof(struct mld_v2_query_hdr)) {
|
|
size_t src_space = ntohs(hdr->n_src) * sizeof(pim_addr);
|
|
|
|
if (len < sizeof(struct mld_v2_query_hdr) + src_space) {
|
|
zlog_warn(log_pkt_src(
|
|
"malformed MLDv2 query (truncated source list)"));
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
return;
|
|
}
|
|
|
|
if (general_query && src_space) {
|
|
zlog_warn(log_pkt_src(
|
|
"malformed MLDv2 query (general query with non-empty source list)"));
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* accepting queries unicast to us (or addressed to a wrong group)
|
|
* can mess up querier election as well as cause us to terminate
|
|
* traffic (since after a unicast query no reports will be coming in)
|
|
*/
|
|
if (!IPV6_ADDR_SAME(pkt_dst, &gm_all_hosts)) {
|
|
if (pim_addr_is_any(hdr->grp)) {
|
|
zlog_warn(
|
|
log_pkt_src(
|
|
"wrong destination %pPA for general query"),
|
|
pkt_dst);
|
|
gm_ifp->stats.rx_drop_dstaddr++;
|
|
return;
|
|
}
|
|
|
|
if (!IPV6_ADDR_SAME(&hdr->grp, pkt_dst)) {
|
|
gm_ifp->stats.rx_drop_dstaddr++;
|
|
zlog_warn(
|
|
log_pkt_src(
|
|
"wrong destination %pPA for group specific query"),
|
|
pkt_dst);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (IPV6_ADDR_CMP(&pkt_src->sin6_addr, &gm_ifp->querier) < 0) {
|
|
if (PIM_DEBUG_GM_EVENTS)
|
|
zlog_debug(
|
|
log_pkt_src("replacing elected querier %pPA"),
|
|
&gm_ifp->querier);
|
|
|
|
gm_ifp->querier = pkt_src->sin6_addr;
|
|
}
|
|
|
|
if (len == sizeof(struct mld_v1_pkt)) {
|
|
timers.qrv = gm_ifp->cur_qrv;
|
|
timers.max_resp_ms = hdr->max_resp_code;
|
|
timers.qqic_ms = gm_ifp->cur_query_intv;
|
|
} else {
|
|
timers.qrv = (hdr->flags & 0x7) ?: 8;
|
|
timers.max_resp_ms = mld_max_resp_decode(hdr->max_resp_code);
|
|
timers.qqic_ms = igmp_msg_decode8to16(hdr->qqic) * 1000;
|
|
}
|
|
timers.fuzz = gm_ifp->cfg_timing_fuzz;
|
|
|
|
gm_expiry_calc(&timers);
|
|
|
|
if (PIM_DEBUG_GM_TRACE_DETAIL)
|
|
zlog_debug(
|
|
log_ifp("query timers: QRV=%u max_resp=%ums qqic=%ums expire_wait=%pTVI"),
|
|
timers.qrv, timers.max_resp_ms, timers.qqic_ms,
|
|
&timers.expire_wait);
|
|
|
|
if (IPV6_ADDR_CMP(&pkt_src->sin6_addr, &pim_ifp->ll_lowest) < 0) {
|
|
unsigned int other_ms;
|
|
|
|
EVENT_OFF(gm_ifp->t_query);
|
|
EVENT_OFF(gm_ifp->t_other_querier);
|
|
|
|
other_ms = timers.qrv * timers.qqic_ms + timers.max_resp_ms / 2;
|
|
event_add_timer_msec(router->master, gm_t_other_querier, gm_ifp,
|
|
other_ms, &gm_ifp->t_other_querier);
|
|
}
|
|
|
|
if (len == sizeof(struct mld_v1_pkt)) {
|
|
if (general_query) {
|
|
gm_handle_q_general(gm_ifp, &timers);
|
|
gm_ifp->stats.rx_query_old_general++;
|
|
} else {
|
|
gm_handle_q_group(gm_ifp, &timers, hdr->grp);
|
|
gm_ifp->stats.rx_query_old_group++;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* v2 query - [S]uppress bit */
|
|
if (hdr->flags & 0x8) {
|
|
gm_ifp->stats.rx_query_new_sbit++;
|
|
return;
|
|
}
|
|
|
|
if (general_query) {
|
|
gm_handle_q_general(gm_ifp, &timers);
|
|
gm_ifp->stats.rx_query_new_general++;
|
|
} else if (!ntohs(hdr->n_src)) {
|
|
gm_handle_q_group(gm_ifp, &timers, hdr->grp);
|
|
gm_ifp->stats.rx_query_new_group++;
|
|
} else {
|
|
/* this is checked above:
|
|
* if (len >= sizeof(struct mld_v2_query_hdr)) {
|
|
* size_t src_space = ntohs(hdr->n_src) * sizeof(pim_addr);
|
|
* if (len < sizeof(struct mld_v2_query_hdr) + src_space) {
|
|
*/
|
|
assume(ntohs(hdr->n_src) <=
|
|
(len - sizeof(struct mld_v2_query_hdr)) /
|
|
sizeof(pim_addr));
|
|
|
|
gm_handle_q_groupsrc(gm_ifp, &timers, hdr->grp, hdr->srcs,
|
|
ntohs(hdr->n_src));
|
|
gm_ifp->stats.rx_query_new_groupsrc++;
|
|
}
|
|
}
|
|
|
|
static void gm_rx_process(struct gm_if *gm_ifp,
|
|
const struct sockaddr_in6 *pkt_src, pim_addr *pkt_dst,
|
|
void *data, size_t pktlen)
|
|
{
|
|
struct icmp6_plain_hdr *icmp6 = data;
|
|
uint16_t pkt_csum, ref_csum;
|
|
struct ipv6_ph ph6 = {
|
|
.src = pkt_src->sin6_addr,
|
|
.dst = *pkt_dst,
|
|
.ulpl = htons(pktlen),
|
|
.next_hdr = IPPROTO_ICMPV6,
|
|
};
|
|
|
|
pkt_csum = icmp6->icmp6_cksum;
|
|
icmp6->icmp6_cksum = 0;
|
|
ref_csum = in_cksum_with_ph6(&ph6, data, pktlen);
|
|
|
|
if (pkt_csum != ref_csum) {
|
|
zlog_warn(
|
|
log_pkt_src(
|
|
"(dst %pPA) packet RX checksum failure, expected %04hx, got %04hx"),
|
|
pkt_dst, pkt_csum, ref_csum);
|
|
gm_ifp->stats.rx_drop_csum++;
|
|
return;
|
|
}
|
|
|
|
data = (icmp6 + 1);
|
|
pktlen -= sizeof(*icmp6);
|
|
|
|
switch (icmp6->icmp6_type) {
|
|
case ICMP6_MLD_QUERY:
|
|
gm_handle_query(gm_ifp, pkt_src, pkt_dst, data, pktlen);
|
|
break;
|
|
case ICMP6_MLD_V1_REPORT:
|
|
gm_handle_v1_report(gm_ifp, pkt_src, data, pktlen);
|
|
break;
|
|
case ICMP6_MLD_V1_DONE:
|
|
gm_handle_v1_leave(gm_ifp, pkt_src, data, pktlen);
|
|
break;
|
|
case ICMP6_MLD_V2_REPORT:
|
|
gm_handle_v2_report(gm_ifp, pkt_src, data, pktlen);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool ip6_check_hopopts_ra(uint8_t *hopopts, size_t hopopt_len,
|
|
uint16_t alert_type)
|
|
{
|
|
uint8_t *hopopt_end;
|
|
|
|
if (hopopt_len < 8)
|
|
return false;
|
|
if (hopopt_len < (hopopts[1] + 1U) * 8U)
|
|
return false;
|
|
|
|
hopopt_end = hopopts + (hopopts[1] + 1) * 8;
|
|
hopopts += 2;
|
|
|
|
while (hopopts < hopopt_end) {
|
|
if (hopopts[0] == IP6OPT_PAD1) {
|
|
hopopts++;
|
|
continue;
|
|
}
|
|
|
|
if (hopopts > hopopt_end - 2)
|
|
break;
|
|
if (hopopts > hopopt_end - 2 - hopopts[1])
|
|
break;
|
|
|
|
if (hopopts[0] == IP6OPT_ROUTER_ALERT && hopopts[1] == 2) {
|
|
uint16_t have_type = (hopopts[2] << 8) | hopopts[3];
|
|
|
|
if (have_type == alert_type)
|
|
return true;
|
|
}
|
|
|
|
hopopts += 2 + hopopts[1];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void gm_t_recv(struct event *t)
|
|
{
|
|
struct pim_instance *pim = EVENT_ARG(t);
|
|
union {
|
|
char buf[CMSG_SPACE(sizeof(struct in6_pktinfo)) +
|
|
CMSG_SPACE(256) /* hop options */ +
|
|
CMSG_SPACE(sizeof(int)) /* hopcount */];
|
|
struct cmsghdr align;
|
|
} cmsgbuf;
|
|
struct cmsghdr *cmsg;
|
|
struct in6_pktinfo *pktinfo = NULL;
|
|
uint8_t *hopopts = NULL;
|
|
size_t hopopt_len = 0;
|
|
int *hoplimit = NULL;
|
|
char rxbuf[2048];
|
|
struct msghdr mh[1] = {};
|
|
struct iovec iov[1];
|
|
struct sockaddr_in6 pkt_src[1] = {};
|
|
ssize_t nread;
|
|
size_t pktlen;
|
|
|
|
event_add_read(router->master, gm_t_recv, pim, pim->gm_socket,
|
|
&pim->t_gm_recv);
|
|
|
|
iov->iov_base = rxbuf;
|
|
iov->iov_len = sizeof(rxbuf);
|
|
|
|
mh->msg_name = pkt_src;
|
|
mh->msg_namelen = sizeof(pkt_src);
|
|
mh->msg_control = cmsgbuf.buf;
|
|
mh->msg_controllen = sizeof(cmsgbuf.buf);
|
|
mh->msg_iov = iov;
|
|
mh->msg_iovlen = array_size(iov);
|
|
mh->msg_flags = 0;
|
|
|
|
nread = recvmsg(pim->gm_socket, mh, MSG_PEEK | MSG_TRUNC);
|
|
if (nread <= 0) {
|
|
zlog_err("(VRF %s) RX error: %m", pim->vrf->name);
|
|
pim->gm_rx_drop_sys++;
|
|
return;
|
|
}
|
|
|
|
if ((size_t)nread > sizeof(rxbuf)) {
|
|
iov->iov_base = XMALLOC(MTYPE_GM_PACKET, nread);
|
|
iov->iov_len = nread;
|
|
}
|
|
nread = recvmsg(pim->gm_socket, mh, 0);
|
|
if (nread <= 0) {
|
|
zlog_err("(VRF %s) RX error: %m", pim->vrf->name);
|
|
pim->gm_rx_drop_sys++;
|
|
goto out_free;
|
|
}
|
|
|
|
struct interface *ifp;
|
|
|
|
ifp = if_lookup_by_index(pkt_src->sin6_scope_id, pim->vrf->vrf_id);
|
|
if (!ifp || !ifp->info)
|
|
goto out_free;
|
|
|
|
struct pim_interface *pim_ifp = ifp->info;
|
|
struct gm_if *gm_ifp = pim_ifp->mld;
|
|
|
|
if (!gm_ifp)
|
|
goto out_free;
|
|
|
|
for (cmsg = CMSG_FIRSTHDR(mh); cmsg; cmsg = CMSG_NXTHDR(mh, cmsg)) {
|
|
if (cmsg->cmsg_level != SOL_IPV6)
|
|
continue;
|
|
|
|
switch (cmsg->cmsg_type) {
|
|
case IPV6_PKTINFO:
|
|
pktinfo = (struct in6_pktinfo *)CMSG_DATA(cmsg);
|
|
break;
|
|
case IPV6_HOPOPTS:
|
|
hopopts = CMSG_DATA(cmsg);
|
|
hopopt_len = cmsg->cmsg_len - sizeof(*cmsg);
|
|
break;
|
|
case IPV6_HOPLIMIT:
|
|
hoplimit = (int *)CMSG_DATA(cmsg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!pktinfo || !hoplimit) {
|
|
zlog_err(log_ifp(
|
|
"BUG: packet without IPV6_PKTINFO or IPV6_HOPLIMIT"));
|
|
pim->gm_rx_drop_sys++;
|
|
goto out_free;
|
|
}
|
|
|
|
if (*hoplimit != 1) {
|
|
zlog_err(log_pkt_src("packet with hop limit != 1"));
|
|
/* spoofing attempt => count on srcaddr counter */
|
|
gm_ifp->stats.rx_drop_srcaddr++;
|
|
goto out_free;
|
|
}
|
|
|
|
if (!ip6_check_hopopts_ra(hopopts, hopopt_len, IP6_ALERT_MLD)) {
|
|
zlog_err(log_pkt_src(
|
|
"packet without IPv6 Router Alert MLD option"));
|
|
gm_ifp->stats.rx_drop_ra++;
|
|
goto out_free;
|
|
}
|
|
|
|
if (IN6_IS_ADDR_UNSPECIFIED(&pkt_src->sin6_addr))
|
|
/* reports from :: happen in normal operation for DAD, so
|
|
* don't spam log messages about this
|
|
*/
|
|
goto out_free;
|
|
|
|
if (!IN6_IS_ADDR_LINKLOCAL(&pkt_src->sin6_addr)) {
|
|
zlog_warn(log_pkt_src("packet from invalid source address"));
|
|
gm_ifp->stats.rx_drop_srcaddr++;
|
|
goto out_free;
|
|
}
|
|
|
|
pktlen = nread;
|
|
if (pktlen < sizeof(struct icmp6_plain_hdr)) {
|
|
zlog_warn(log_pkt_src("truncated packet"));
|
|
gm_ifp->stats.rx_drop_malformed++;
|
|
goto out_free;
|
|
}
|
|
|
|
gm_rx_process(gm_ifp, pkt_src, &pktinfo->ipi6_addr, iov->iov_base,
|
|
pktlen);
|
|
|
|
out_free:
|
|
if (iov->iov_base != rxbuf)
|
|
XFREE(MTYPE_GM_PACKET, iov->iov_base);
|
|
}
|
|
|
|
static void gm_send_query(struct gm_if *gm_ifp, pim_addr grp,
|
|
const pim_addr *srcs, size_t n_srcs, bool s_bit)
|
|
{
|
|
struct pim_interface *pim_ifp = gm_ifp->ifp->info;
|
|
struct sockaddr_in6 dstaddr = {
|
|
.sin6_family = AF_INET6,
|
|
.sin6_scope_id = gm_ifp->ifp->ifindex,
|
|
};
|
|
struct {
|
|
struct icmp6_plain_hdr hdr;
|
|
struct mld_v2_query_hdr v2_query;
|
|
} query = {
|
|
/* clang-format off */
|
|
.hdr = {
|
|
.icmp6_type = ICMP6_MLD_QUERY,
|
|
.icmp6_code = 0,
|
|
},
|
|
.v2_query = {
|
|
.grp = grp,
|
|
},
|
|
/* clang-format on */
|
|
};
|
|
struct ipv6_ph ph6 = {
|
|
.src = pim_ifp->ll_lowest,
|
|
.ulpl = htons(sizeof(query)),
|
|
.next_hdr = IPPROTO_ICMPV6,
|
|
};
|
|
union {
|
|
char buf[CMSG_SPACE(8) /* hop options */ +
|
|
CMSG_SPACE(sizeof(struct in6_pktinfo))];
|
|
struct cmsghdr align;
|
|
} cmsg = {};
|
|
struct cmsghdr *cmh;
|
|
struct msghdr mh[1] = {};
|
|
struct iovec iov[3];
|
|
size_t iov_len;
|
|
ssize_t ret, expect_ret;
|
|
uint8_t *dp;
|
|
struct in6_pktinfo *pktinfo;
|
|
|
|
if (if_is_loopback(gm_ifp->ifp)) {
|
|
/* Linux is a bit odd with multicast on loopback */
|
|
ph6.src = in6addr_loopback;
|
|
dstaddr.sin6_addr = in6addr_loopback;
|
|
} else if (pim_addr_is_any(grp))
|
|
dstaddr.sin6_addr = gm_all_hosts;
|
|
else
|
|
dstaddr.sin6_addr = grp;
|
|
|
|
query.v2_query.max_resp_code =
|
|
mld_max_resp_encode(gm_ifp->cur_max_resp);
|
|
query.v2_query.flags = (gm_ifp->cur_qrv < 8) ? gm_ifp->cur_qrv : 0;
|
|
if (s_bit)
|
|
query.v2_query.flags |= 0x08;
|
|
query.v2_query.qqic =
|
|
igmp_msg_encode16to8(gm_ifp->cur_query_intv / 1000);
|
|
query.v2_query.n_src = htons(n_srcs);
|
|
|
|
ph6.dst = dstaddr.sin6_addr;
|
|
|
|
/* ph6 not included in sendmsg */
|
|
iov[0].iov_base = &ph6;
|
|
iov[0].iov_len = sizeof(ph6);
|
|
iov[1].iov_base = &query;
|
|
if (gm_ifp->cur_version == GM_MLDV1) {
|
|
iov_len = 2;
|
|
iov[1].iov_len = sizeof(query.hdr) + sizeof(struct mld_v1_pkt);
|
|
} else if (!n_srcs) {
|
|
iov_len = 2;
|
|
iov[1].iov_len = sizeof(query);
|
|
} else {
|
|
iov[1].iov_len = sizeof(query);
|
|
iov[2].iov_base = (void *)srcs;
|
|
iov[2].iov_len = n_srcs * sizeof(srcs[0]);
|
|
iov_len = 3;
|
|
}
|
|
|
|
query.hdr.icmp6_cksum = in_cksumv(iov, iov_len);
|
|
|
|
if (PIM_DEBUG_GM_PACKETS)
|
|
zlog_debug(
|
|
log_ifp("MLD query %pPA -> %pI6 (grp=%pPA, %zu srcs)"),
|
|
&pim_ifp->ll_lowest, &dstaddr.sin6_addr, &grp, n_srcs);
|
|
|
|
mh->msg_name = &dstaddr;
|
|
mh->msg_namelen = sizeof(dstaddr);
|
|
mh->msg_iov = iov + 1;
|
|
mh->msg_iovlen = iov_len - 1;
|
|
mh->msg_control = &cmsg;
|
|
mh->msg_controllen = sizeof(cmsg.buf);
|
|
|
|
cmh = CMSG_FIRSTHDR(mh);
|
|
cmh->cmsg_level = IPPROTO_IPV6;
|
|
cmh->cmsg_type = IPV6_HOPOPTS;
|
|
cmh->cmsg_len = CMSG_LEN(8);
|
|
dp = CMSG_DATA(cmh);
|
|
*dp++ = 0; /* next header */
|
|
*dp++ = 0; /* length (8-byte blocks, minus 1) */
|
|
*dp++ = IP6OPT_ROUTER_ALERT; /* router alert */
|
|
*dp++ = 2; /* length */
|
|
*dp++ = 0; /* value (2 bytes) */
|
|
*dp++ = 0; /* value (2 bytes) (0 = MLD) */
|
|
*dp++ = 0; /* pad0 */
|
|
*dp++ = 0; /* pad0 */
|
|
|
|
cmh = CMSG_NXTHDR(mh, cmh);
|
|
cmh->cmsg_level = IPPROTO_IPV6;
|
|
cmh->cmsg_type = IPV6_PKTINFO;
|
|
cmh->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo));
|
|
pktinfo = (struct in6_pktinfo *)CMSG_DATA(cmh);
|
|
pktinfo->ipi6_ifindex = gm_ifp->ifp->ifindex;
|
|
pktinfo->ipi6_addr = gm_ifp->cur_ll_lowest;
|
|
|
|
expect_ret = iov[1].iov_len;
|
|
if (iov_len == 3)
|
|
expect_ret += iov[2].iov_len;
|
|
|
|
frr_with_privs (&pimd_privs) {
|
|
ret = sendmsg(gm_ifp->pim->gm_socket, mh, 0);
|
|
}
|
|
|
|
if (ret != expect_ret) {
|
|
zlog_warn(log_ifp("failed to send query: %m"));
|
|
gm_ifp->stats.tx_query_fail++;
|
|
} else {
|
|
if (gm_ifp->cur_version == GM_MLDV1) {
|
|
if (pim_addr_is_any(grp))
|
|
gm_ifp->stats.tx_query_old_general++;
|
|
else
|
|
gm_ifp->stats.tx_query_old_group++;
|
|
} else {
|
|
if (pim_addr_is_any(grp))
|
|
gm_ifp->stats.tx_query_new_general++;
|
|
else if (!n_srcs)
|
|
gm_ifp->stats.tx_query_new_group++;
|
|
else
|
|
gm_ifp->stats.tx_query_new_groupsrc++;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void gm_t_query(struct event *t)
|
|
{
|
|
struct gm_if *gm_ifp = EVENT_ARG(t);
|
|
unsigned int timer_ms = gm_ifp->cur_query_intv;
|
|
|
|
if (gm_ifp->n_startup) {
|
|
timer_ms /= 4;
|
|
gm_ifp->n_startup--;
|
|
}
|
|
|
|
event_add_timer_msec(router->master, gm_t_query, gm_ifp, timer_ms,
|
|
&gm_ifp->t_query);
|
|
|
|
gm_send_query(gm_ifp, PIMADDR_ANY, NULL, 0, false);
|
|
}
|
|
|
|
static void gm_t_sg_query(struct event *t)
|
|
{
|
|
struct gm_sg *sg = EVENT_ARG(t);
|
|
|
|
gm_trigger_specific(sg);
|
|
}
|
|
|
|
/* S,G specific queries (triggered by a member leaving) get a little slack
|
|
* time so we can bundle queries for [S1,S2,S3,...],G into the same query
|
|
*/
|
|
static void gm_send_specific(struct gm_gsq_pending *pend_gsq)
|
|
{
|
|
struct gm_if *gm_ifp = pend_gsq->iface;
|
|
|
|
gm_send_query(gm_ifp, pend_gsq->grp, pend_gsq->srcs, pend_gsq->n_src,
|
|
pend_gsq->s_bit);
|
|
|
|
gm_gsq_pends_del(gm_ifp->gsq_pends, pend_gsq);
|
|
XFREE(MTYPE_GM_GSQ_PENDING, pend_gsq);
|
|
}
|
|
|
|
static void gm_t_gsq_pend(struct event *t)
|
|
{
|
|
struct gm_gsq_pending *pend_gsq = EVENT_ARG(t);
|
|
|
|
gm_send_specific(pend_gsq);
|
|
}
|
|
|
|
static void gm_trigger_specific(struct gm_sg *sg)
|
|
{
|
|
struct gm_if *gm_ifp = sg->iface;
|
|
struct gm_gsq_pending *pend_gsq, ref = {};
|
|
|
|
sg->n_query--;
|
|
if (sg->n_query)
|
|
event_add_timer_msec(router->master, gm_t_sg_query, sg,
|
|
gm_ifp->cur_query_intv_trig,
|
|
&sg->t_sg_query);
|
|
|
|
/* As per RFC 2271, s6 p14:
|
|
* E.g. a router that starts as a Querier, receives a
|
|
* Done message for a group and then receives a Query from a router with
|
|
* a lower address (causing a transition to the Non-Querier state)
|
|
* continues to send multicast-address-specific queries for the group in
|
|
* question until it either receives a Report or its timer expires, at
|
|
* which time it starts performing the actions of a Non-Querier for this
|
|
* group.
|
|
*/
|
|
/* Therefore here we do not need to check if this router is querier or
|
|
* not. This is called only for querier, hence it will work even if the
|
|
* router transitions from querier to non-querier.
|
|
*/
|
|
|
|
if (gm_ifp->pim->gm_socket == -1)
|
|
return;
|
|
|
|
if (PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_sg(sg, "triggered query"));
|
|
|
|
if (pim_addr_is_any(sg->sgaddr.src)) {
|
|
gm_send_query(gm_ifp, sg->sgaddr.grp, NULL, 0, sg->query_sbit);
|
|
return;
|
|
}
|
|
|
|
ref.grp = sg->sgaddr.grp;
|
|
ref.s_bit = sg->query_sbit;
|
|
|
|
pend_gsq = gm_gsq_pends_find(gm_ifp->gsq_pends, &ref);
|
|
if (!pend_gsq) {
|
|
pend_gsq = XCALLOC(MTYPE_GM_GSQ_PENDING, sizeof(*pend_gsq));
|
|
pend_gsq->grp = sg->sgaddr.grp;
|
|
pend_gsq->s_bit = sg->query_sbit;
|
|
pend_gsq->iface = gm_ifp;
|
|
gm_gsq_pends_add(gm_ifp->gsq_pends, pend_gsq);
|
|
|
|
event_add_timer_tv(router->master, gm_t_gsq_pend, pend_gsq,
|
|
&gm_ifp->cfg_timing_fuzz, &pend_gsq->t_send);
|
|
}
|
|
|
|
assert(pend_gsq->n_src < array_size(pend_gsq->srcs));
|
|
|
|
pend_gsq->srcs[pend_gsq->n_src] = sg->sgaddr.src;
|
|
pend_gsq->n_src++;
|
|
|
|
if (pend_gsq->n_src == array_size(pend_gsq->srcs)) {
|
|
EVENT_OFF(pend_gsq->t_send);
|
|
gm_send_specific(pend_gsq);
|
|
pend_gsq = NULL;
|
|
}
|
|
}
|
|
|
|
static void gm_vrf_socket_incref(struct pim_instance *pim)
|
|
{
|
|
struct vrf *vrf = pim->vrf;
|
|
int ret, intval;
|
|
struct icmp6_filter filter[1];
|
|
|
|
if (pim->gm_socket_if_count++ && pim->gm_socket != -1)
|
|
return;
|
|
|
|
ICMP6_FILTER_SETBLOCKALL(filter);
|
|
ICMP6_FILTER_SETPASS(ICMP6_MLD_QUERY, filter);
|
|
ICMP6_FILTER_SETPASS(ICMP6_MLD_V1_REPORT, filter);
|
|
ICMP6_FILTER_SETPASS(ICMP6_MLD_V1_DONE, filter);
|
|
ICMP6_FILTER_SETPASS(ICMP6_MLD_V2_REPORT, filter);
|
|
|
|
frr_with_privs (&pimd_privs) {
|
|
pim->gm_socket = vrf_socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6,
|
|
vrf->vrf_id, vrf->name);
|
|
if (pim->gm_socket < 0) {
|
|
zlog_err("(VRF %s) could not create MLD socket: %m",
|
|
vrf->name);
|
|
return;
|
|
}
|
|
|
|
ret = setsockopt(pim->gm_socket, SOL_ICMPV6, ICMP6_FILTER,
|
|
filter, sizeof(filter));
|
|
if (ret)
|
|
zlog_err("(VRF %s) failed to set ICMP6_FILTER: %m",
|
|
vrf->name);
|
|
|
|
intval = 1;
|
|
ret = setsockopt(pim->gm_socket, SOL_IPV6, IPV6_RECVPKTINFO,
|
|
&intval, sizeof(intval));
|
|
if (ret)
|
|
zlog_err("(VRF %s) failed to set IPV6_RECVPKTINFO: %m",
|
|
vrf->name);
|
|
|
|
intval = 1;
|
|
ret = setsockopt(pim->gm_socket, SOL_IPV6, IPV6_RECVHOPOPTS,
|
|
&intval, sizeof(intval));
|
|
if (ret)
|
|
zlog_err("(VRF %s) failed to set IPV6_HOPOPTS: %m",
|
|
vrf->name);
|
|
|
|
intval = 1;
|
|
ret = setsockopt(pim->gm_socket, SOL_IPV6, IPV6_RECVHOPLIMIT,
|
|
&intval, sizeof(intval));
|
|
if (ret)
|
|
zlog_err("(VRF %s) failed to set IPV6_HOPLIMIT: %m",
|
|
vrf->name);
|
|
|
|
intval = 1;
|
|
ret = setsockopt(pim->gm_socket, SOL_IPV6, IPV6_MULTICAST_LOOP,
|
|
&intval, sizeof(intval));
|
|
if (ret)
|
|
zlog_err(
|
|
"(VRF %s) failed to disable IPV6_MULTICAST_LOOP: %m",
|
|
vrf->name);
|
|
|
|
intval = 1;
|
|
ret = setsockopt(pim->gm_socket, SOL_IPV6, IPV6_MULTICAST_HOPS,
|
|
&intval, sizeof(intval));
|
|
if (ret)
|
|
zlog_err(
|
|
"(VRF %s) failed to set IPV6_MULTICAST_HOPS: %m",
|
|
vrf->name);
|
|
|
|
/* NB: IPV6_MULTICAST_ALL does not completely bypass multicast
|
|
* RX filtering in Linux. It only means "receive all groups
|
|
* that something on the system has joined". To actually
|
|
* receive *all* MLD packets - which is what we need -
|
|
* multicast routing must be enabled on the interface. And
|
|
* this only works for MLD packets specifically.
|
|
*
|
|
* For reference, check ip6_mc_input() in net/ipv6/ip6_input.c
|
|
* and in particular the #ifdef CONFIG_IPV6_MROUTE block there.
|
|
*
|
|
* Also note that the code there explicitly checks for the IPv6
|
|
* router alert MLD option (which is required by the RFC to be
|
|
* on MLD packets.) That implies trying to support hosts which
|
|
* erroneously don't add that option is just not possible.
|
|
*/
|
|
intval = 1;
|
|
ret = setsockopt(pim->gm_socket, SOL_IPV6, IPV6_MULTICAST_ALL,
|
|
&intval, sizeof(intval));
|
|
if (ret)
|
|
zlog_info(
|
|
"(VRF %s) failed to set IPV6_MULTICAST_ALL: %m (OK on old kernels)",
|
|
vrf->name);
|
|
}
|
|
|
|
event_add_read(router->master, gm_t_recv, pim, pim->gm_socket,
|
|
&pim->t_gm_recv);
|
|
}
|
|
|
|
static void gm_vrf_socket_decref(struct pim_instance *pim)
|
|
{
|
|
if (--pim->gm_socket_if_count)
|
|
return;
|
|
|
|
EVENT_OFF(pim->t_gm_recv);
|
|
close(pim->gm_socket);
|
|
pim->gm_socket = -1;
|
|
}
|
|
|
|
static void gm_start(struct interface *ifp)
|
|
{
|
|
struct pim_interface *pim_ifp = ifp->info;
|
|
struct gm_if *gm_ifp;
|
|
|
|
assert(pim_ifp);
|
|
assert(pim_ifp->pim);
|
|
assert(pim_ifp->mroute_vif_index >= 0);
|
|
assert(!pim_ifp->mld);
|
|
|
|
gm_vrf_socket_incref(pim_ifp->pim);
|
|
|
|
gm_ifp = XCALLOC(MTYPE_GM_IFACE, sizeof(*gm_ifp));
|
|
gm_ifp->ifp = ifp;
|
|
pim_ifp->mld = gm_ifp;
|
|
gm_ifp->pim = pim_ifp->pim;
|
|
monotime(&gm_ifp->started);
|
|
|
|
zlog_info(log_ifp("starting MLD"));
|
|
|
|
if (pim_ifp->mld_version == 1)
|
|
gm_ifp->cur_version = GM_MLDV1;
|
|
else
|
|
gm_ifp->cur_version = GM_MLDV2;
|
|
|
|
gm_ifp->cur_qrv = pim_ifp->gm_default_robustness_variable;
|
|
gm_ifp->cur_query_intv = pim_ifp->gm_default_query_interval * 1000;
|
|
gm_ifp->cur_query_intv_trig =
|
|
pim_ifp->gm_specific_query_max_response_time_dsec * 100;
|
|
gm_ifp->cur_max_resp = pim_ifp->gm_query_max_response_time_dsec * 100;
|
|
gm_ifp->cur_lmqc = pim_ifp->gm_last_member_query_count;
|
|
|
|
gm_ifp->cfg_timing_fuzz.tv_sec = 0;
|
|
gm_ifp->cfg_timing_fuzz.tv_usec = 10 * 1000;
|
|
|
|
gm_sgs_init(gm_ifp->sgs);
|
|
gm_subscribers_init(gm_ifp->subscribers);
|
|
gm_packet_expires_init(gm_ifp->expires);
|
|
gm_grp_pends_init(gm_ifp->grp_pends);
|
|
gm_gsq_pends_init(gm_ifp->gsq_pends);
|
|
|
|
frr_with_privs (&pimd_privs) {
|
|
struct ipv6_mreq mreq;
|
|
int ret;
|
|
|
|
/* all-MLDv2 group */
|
|
mreq.ipv6mr_multiaddr = gm_all_routers;
|
|
mreq.ipv6mr_interface = ifp->ifindex;
|
|
ret = setsockopt(gm_ifp->pim->gm_socket, SOL_IPV6,
|
|
IPV6_JOIN_GROUP, &mreq, sizeof(mreq));
|
|
if (ret)
|
|
zlog_err("(%s) failed to join ff02::16 (all-MLDv2): %m",
|
|
ifp->name);
|
|
}
|
|
}
|
|
|
|
void gm_group_delete(struct gm_if *gm_ifp)
|
|
{
|
|
struct gm_sg *sg;
|
|
struct gm_packet_state *pkt;
|
|
struct gm_grp_pending *pend_grp;
|
|
struct gm_gsq_pending *pend_gsq;
|
|
struct gm_subscriber *subscriber;
|
|
|
|
while ((pkt = gm_packet_expires_first(gm_ifp->expires)))
|
|
gm_packet_drop(pkt, false);
|
|
|
|
while ((pend_grp = gm_grp_pends_pop(gm_ifp->grp_pends))) {
|
|
EVENT_OFF(pend_grp->t_expire);
|
|
XFREE(MTYPE_GM_GRP_PENDING, pend_grp);
|
|
}
|
|
|
|
while ((pend_gsq = gm_gsq_pends_pop(gm_ifp->gsq_pends))) {
|
|
EVENT_OFF(pend_gsq->t_send);
|
|
XFREE(MTYPE_GM_GSQ_PENDING, pend_gsq);
|
|
}
|
|
|
|
while ((sg = gm_sgs_pop(gm_ifp->sgs))) {
|
|
EVENT_OFF(sg->t_sg_expire);
|
|
assertf(!gm_packet_sg_subs_count(sg->subs_negative), "%pSG",
|
|
&sg->sgaddr);
|
|
assertf(!gm_packet_sg_subs_count(sg->subs_positive), "%pSG",
|
|
&sg->sgaddr);
|
|
|
|
gm_sg_free(sg);
|
|
}
|
|
while ((subscriber = gm_subscribers_pop(gm_ifp->subscribers))) {
|
|
assertf(!gm_packets_count(subscriber->packets), "%pPA",
|
|
&subscriber->addr);
|
|
XFREE(MTYPE_GM_SUBSCRIBER, subscriber);
|
|
}
|
|
}
|
|
|
|
void gm_ifp_teardown(struct interface *ifp)
|
|
{
|
|
struct pim_interface *pim_ifp = ifp->info;
|
|
struct gm_if *gm_ifp;
|
|
|
|
if (!pim_ifp || !pim_ifp->mld)
|
|
return;
|
|
|
|
gm_ifp = pim_ifp->mld;
|
|
gm_ifp->stopping = true;
|
|
if (PIM_DEBUG_GM_EVENTS)
|
|
zlog_debug(log_ifp("MLD stop"));
|
|
|
|
EVENT_OFF(gm_ifp->t_query);
|
|
EVENT_OFF(gm_ifp->t_other_querier);
|
|
EVENT_OFF(gm_ifp->t_expire);
|
|
|
|
frr_with_privs (&pimd_privs) {
|
|
struct ipv6_mreq mreq;
|
|
int ret;
|
|
|
|
/* all-MLDv2 group */
|
|
mreq.ipv6mr_multiaddr = gm_all_routers;
|
|
mreq.ipv6mr_interface = ifp->ifindex;
|
|
ret = setsockopt(gm_ifp->pim->gm_socket, SOL_IPV6,
|
|
IPV6_LEAVE_GROUP, &mreq, sizeof(mreq));
|
|
if (ret)
|
|
zlog_err(
|
|
"(%s) failed to leave ff02::16 (all-MLDv2): %m",
|
|
ifp->name);
|
|
}
|
|
|
|
gm_vrf_socket_decref(gm_ifp->pim);
|
|
|
|
gm_group_delete(gm_ifp);
|
|
|
|
gm_grp_pends_fini(gm_ifp->grp_pends);
|
|
gm_packet_expires_fini(gm_ifp->expires);
|
|
gm_subscribers_fini(gm_ifp->subscribers);
|
|
gm_sgs_fini(gm_ifp->sgs);
|
|
|
|
XFREE(MTYPE_GM_IFACE, gm_ifp);
|
|
pim_ifp->mld = NULL;
|
|
}
|
|
|
|
static void gm_update_ll(struct interface *ifp)
|
|
{
|
|
struct pim_interface *pim_ifp = ifp->info;
|
|
struct gm_if *gm_ifp = pim_ifp->mld;
|
|
bool was_querier;
|
|
|
|
was_querier =
|
|
!IPV6_ADDR_CMP(&gm_ifp->cur_ll_lowest, &gm_ifp->querier) &&
|
|
!pim_addr_is_any(gm_ifp->querier);
|
|
|
|
gm_ifp->cur_ll_lowest = pim_ifp->ll_lowest;
|
|
if (was_querier)
|
|
gm_ifp->querier = pim_ifp->ll_lowest;
|
|
EVENT_OFF(gm_ifp->t_query);
|
|
|
|
if (pim_addr_is_any(gm_ifp->cur_ll_lowest)) {
|
|
if (was_querier)
|
|
zlog_info(log_ifp(
|
|
"lost link-local address, stopping querier"));
|
|
return;
|
|
}
|
|
|
|
if (was_querier)
|
|
zlog_info(log_ifp("new link-local %pPA while querier"),
|
|
&gm_ifp->cur_ll_lowest);
|
|
else if (IPV6_ADDR_CMP(&gm_ifp->cur_ll_lowest, &gm_ifp->querier) < 0 ||
|
|
pim_addr_is_any(gm_ifp->querier)) {
|
|
zlog_info(log_ifp("new link-local %pPA, becoming querier"),
|
|
&gm_ifp->cur_ll_lowest);
|
|
gm_ifp->querier = gm_ifp->cur_ll_lowest;
|
|
} else
|
|
return;
|
|
|
|
gm_ifp->n_startup = gm_ifp->cur_qrv;
|
|
event_execute(router->master, gm_t_query, gm_ifp, 0, NULL);
|
|
}
|
|
|
|
void gm_ifp_update(struct interface *ifp)
|
|
{
|
|
struct pim_interface *pim_ifp = ifp->info;
|
|
struct gm_if *gm_ifp;
|
|
bool changed = false;
|
|
|
|
if (!pim_ifp)
|
|
return;
|
|
if (!if_is_operative(ifp) || !pim_ifp->pim ||
|
|
pim_ifp->mroute_vif_index < 0) {
|
|
gm_ifp_teardown(ifp);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* If ipv6 mld is not enabled on interface, do not start mld activites.
|
|
*/
|
|
if (!pim_ifp->gm_enable)
|
|
return;
|
|
|
|
if (!pim_ifp->mld) {
|
|
changed = true;
|
|
gm_start(ifp);
|
|
assume(pim_ifp->mld != NULL);
|
|
}
|
|
|
|
gm_ifp = pim_ifp->mld;
|
|
if (IPV6_ADDR_CMP(&pim_ifp->ll_lowest, &gm_ifp->cur_ll_lowest))
|
|
gm_update_ll(ifp);
|
|
|
|
unsigned int cfg_query_intv = pim_ifp->gm_default_query_interval * 1000;
|
|
|
|
if (gm_ifp->cur_query_intv != cfg_query_intv) {
|
|
gm_ifp->cur_query_intv = cfg_query_intv;
|
|
changed = true;
|
|
}
|
|
|
|
unsigned int cfg_query_intv_trig =
|
|
pim_ifp->gm_specific_query_max_response_time_dsec * 100;
|
|
|
|
if (gm_ifp->cur_query_intv_trig != cfg_query_intv_trig) {
|
|
gm_ifp->cur_query_intv_trig = cfg_query_intv_trig;
|
|
changed = true;
|
|
}
|
|
|
|
unsigned int cfg_max_response =
|
|
pim_ifp->gm_query_max_response_time_dsec * 100;
|
|
|
|
if (gm_ifp->cur_max_resp != cfg_max_response)
|
|
gm_ifp->cur_max_resp = cfg_max_response;
|
|
|
|
if (gm_ifp->cur_lmqc != pim_ifp->gm_last_member_query_count)
|
|
gm_ifp->cur_lmqc = pim_ifp->gm_last_member_query_count;
|
|
|
|
enum gm_version cfg_version;
|
|
|
|
if (pim_ifp->mld_version == 1)
|
|
cfg_version = GM_MLDV1;
|
|
else
|
|
cfg_version = GM_MLDV2;
|
|
if (gm_ifp->cur_version != cfg_version) {
|
|
gm_ifp->cur_version = cfg_version;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) {
|
|
if (PIM_DEBUG_GM_TRACE)
|
|
zlog_debug(log_ifp(
|
|
"MLD querier config changed, querying"));
|
|
gm_bump_querier(gm_ifp);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* CLI (show commands only)
|
|
*/
|
|
|
|
#include "lib/command.h"
|
|
|
|
#include "pimd/pim6_mld_clippy.c"
|
|
|
|
static struct vrf *gm_cmd_vrf_lookup(struct vty *vty, const char *vrf_str,
|
|
int *err)
|
|
{
|
|
struct vrf *ret;
|
|
|
|
if (!vrf_str)
|
|
return vrf_lookup_by_id(VRF_DEFAULT);
|
|
if (!strcmp(vrf_str, "all"))
|
|
return NULL;
|
|
ret = vrf_lookup_by_name(vrf_str);
|
|
if (ret)
|
|
return ret;
|
|
|
|
vty_out(vty, "%% VRF %pSQq does not exist\n", vrf_str);
|
|
*err = CMD_WARNING;
|
|
return NULL;
|
|
}
|
|
|
|
static void gm_show_if_one_detail(struct vty *vty, struct interface *ifp)
|
|
{
|
|
struct pim_interface *pim_ifp = (struct pim_interface *)ifp->info;
|
|
struct gm_if *gm_ifp;
|
|
bool querier;
|
|
size_t i;
|
|
|
|
if (!pim_ifp) {
|
|
vty_out(vty, "Interface %s: no PIM/MLD config\n\n", ifp->name);
|
|
return;
|
|
}
|
|
|
|
gm_ifp = pim_ifp->mld;
|
|
if (!gm_ifp) {
|
|
vty_out(vty, "Interface %s: MLD not running\n\n", ifp->name);
|
|
return;
|
|
}
|
|
|
|
querier = IPV6_ADDR_SAME(&gm_ifp->querier, &pim_ifp->ll_lowest);
|
|
|
|
vty_out(vty, "Interface %s: MLD running\n", ifp->name);
|
|
vty_out(vty, " Uptime: %pTVMs\n", &gm_ifp->started);
|
|
vty_out(vty, " MLD version: %d\n", gm_ifp->cur_version);
|
|
vty_out(vty, " Querier: %pPA%s\n", &gm_ifp->querier,
|
|
querier ? " (this system)" : "");
|
|
vty_out(vty, " Query timer: %pTH\n", gm_ifp->t_query);
|
|
vty_out(vty, " Other querier timer: %pTH\n",
|
|
gm_ifp->t_other_querier);
|
|
vty_out(vty, " Robustness value: %u\n", gm_ifp->cur_qrv);
|
|
vty_out(vty, " Query interval: %ums\n",
|
|
gm_ifp->cur_query_intv);
|
|
vty_out(vty, " Query response timer: %ums\n", gm_ifp->cur_max_resp);
|
|
vty_out(vty, " Last member query intv.: %ums\n",
|
|
gm_ifp->cur_query_intv_trig);
|
|
vty_out(vty, " %u expiry timers from general queries:\n",
|
|
gm_ifp->n_pending);
|
|
for (i = 0; i < gm_ifp->n_pending; i++) {
|
|
struct gm_general_pending *p = &gm_ifp->pending[i];
|
|
|
|
vty_out(vty, " %9pTVMs ago (query) -> %9pTVMu (expiry)\n",
|
|
&p->query, &p->expiry);
|
|
}
|
|
vty_out(vty, " %zu expiry timers from *,G queries\n",
|
|
gm_grp_pends_count(gm_ifp->grp_pends));
|
|
vty_out(vty, " %zu expiry timers from S,G queries\n",
|
|
gm_gsq_pends_count(gm_ifp->gsq_pends));
|
|
vty_out(vty, " %zu total *,G/S,G from %zu hosts in %zu bundles\n",
|
|
gm_sgs_count(gm_ifp->sgs),
|
|
gm_subscribers_count(gm_ifp->subscribers),
|
|
gm_packet_expires_count(gm_ifp->expires));
|
|
vty_out(vty, "\n");
|
|
}
|
|
|
|
static void gm_show_if_one(struct vty *vty, struct interface *ifp,
|
|
json_object *js_if, struct ttable *tt)
|
|
{
|
|
struct pim_interface *pim_ifp = (struct pim_interface *)ifp->info;
|
|
struct gm_if *gm_ifp = pim_ifp->mld;
|
|
bool querier;
|
|
|
|
assume(js_if || tt);
|
|
|
|
querier = IPV6_ADDR_SAME(&gm_ifp->querier, &pim_ifp->ll_lowest);
|
|
|
|
if (js_if) {
|
|
json_object_string_add(js_if, "name", ifp->name);
|
|
json_object_string_addf(js_if, "address", "%pPA",
|
|
&pim_ifp->primary_address);
|
|
json_object_string_add(js_if, "state", "up");
|
|
json_object_string_addf(js_if, "version", "%d",
|
|
gm_ifp->cur_version);
|
|
json_object_string_addf(js_if, "upTime", "%pTVMs",
|
|
&gm_ifp->started);
|
|
json_object_boolean_add(js_if, "querier", querier);
|
|
json_object_string_addf(js_if, "querierIp", "%pPA",
|
|
&gm_ifp->querier);
|
|
if (querier)
|
|
json_object_string_addf(js_if, "queryTimer", "%pTH",
|
|
gm_ifp->t_query);
|
|
else
|
|
json_object_string_addf(js_if, "otherQuerierTimer",
|
|
"%pTH",
|
|
gm_ifp->t_other_querier);
|
|
json_object_int_add(js_if, "timerRobustnessValue",
|
|
gm_ifp->cur_qrv);
|
|
json_object_int_add(js_if, "lastMemberQueryCount",
|
|
gm_ifp->cur_lmqc);
|
|
json_object_int_add(js_if, "timerQueryIntervalMsec",
|
|
gm_ifp->cur_query_intv);
|
|
json_object_int_add(js_if, "timerQueryResponseTimerMsec",
|
|
gm_ifp->cur_max_resp);
|
|
json_object_int_add(js_if, "timerLastMemberQueryIntervalMsec",
|
|
gm_ifp->cur_query_intv_trig);
|
|
} else {
|
|
ttable_add_row(tt, "%s|%s|%pPAs|%d|%s|%pPAs|%pTH|%pTVMs",
|
|
ifp->name, "up", &pim_ifp->primary_address,
|
|
gm_ifp->cur_version, querier ? "local" : "other",
|
|
&gm_ifp->querier, gm_ifp->t_query,
|
|
&gm_ifp->started);
|
|
}
|
|
}
|
|
|
|
static void gm_show_if_vrf(struct vty *vty, struct vrf *vrf, const char *ifname,
|
|
bool detail, json_object *js)
|
|
{
|
|
struct interface *ifp;
|
|
json_object *js_vrf = NULL;
|
|
struct pim_interface *pim_ifp;
|
|
struct ttable *tt = NULL;
|
|
char *table = NULL;
|
|
|
|
if (js) {
|
|
js_vrf = json_object_new_object();
|
|
json_object_object_add(js, vrf->name, js_vrf);
|
|
}
|
|
|
|
if (!js && !detail) {
|
|
/* Prepare table. */
|
|
tt = ttable_new(&ttable_styles[TTSTYLE_BLANK]);
|
|
ttable_add_row(
|
|
tt,
|
|
"Interface|State|Address|V|Querier|QuerierIp|Query Timer|Uptime");
|
|
tt->style.cell.rpad = 2;
|
|
tt->style.corner = '+';
|
|
ttable_restyle(tt);
|
|
}
|
|
|
|
FOR_ALL_INTERFACES (vrf, ifp) {
|
|
json_object *js_if = NULL;
|
|
|
|
if (ifname && strcmp(ifp->name, ifname))
|
|
continue;
|
|
if (detail && !js) {
|
|
gm_show_if_one_detail(vty, ifp);
|
|
continue;
|
|
}
|
|
|
|
pim_ifp = ifp->info;
|
|
|
|
if (!pim_ifp || !pim_ifp->mld)
|
|
continue;
|
|
|
|
if (js) {
|
|
js_if = json_object_new_object();
|
|
/*
|
|
* If we have js as true and detail as false
|
|
* and if Coverity thinks that js_if is NULL
|
|
* because of a failed call to new then
|
|
* when we call gm_show_if_one below
|
|
* the tt can be deref'ed and as such
|
|
* FRR will crash. But since we know
|
|
* that json_object_new_object never fails
|
|
* then let's tell Coverity that this assumption
|
|
* is true. I'm not worried about fast path
|
|
* here at all.
|
|
*/
|
|
assert(js_if);
|
|
json_object_object_add(js_vrf, ifp->name, js_if);
|
|
}
|
|
|
|
gm_show_if_one(vty, ifp, js_if, tt);
|
|
}
|
|
|
|
/* Dump the generated table. */
|
|
if (!js && !detail) {
|
|
table = ttable_dump(tt, "\n");
|
|
vty_out(vty, "%s\n", table);
|
|
XFREE(MTYPE_TMP_TTABLE, table);
|
|
ttable_del(tt);
|
|
}
|
|
}
|
|
|
|
static void gm_show_if(struct vty *vty, struct vrf *vrf, const char *ifname,
|
|
bool detail, json_object *js)
|
|
{
|
|
if (vrf)
|
|
gm_show_if_vrf(vty, vrf, ifname, detail, js);
|
|
else
|
|
RB_FOREACH (vrf, vrf_name_head, &vrfs_by_name)
|
|
gm_show_if_vrf(vty, vrf, ifname, detail, js);
|
|
}
|
|
|
|
DEFPY(gm_show_interface,
|
|
gm_show_interface_cmd,
|
|
"show ipv6 mld [vrf <VRF|all>$vrf_str] interface [IFNAME | detail$detail] [json$json]",
|
|
SHOW_STR
|
|
IPV6_STR
|
|
MLD_STR
|
|
VRF_FULL_CMD_HELP_STR
|
|
"MLD interface information\n"
|
|
"Interface name\n"
|
|
"Detailed output\n"
|
|
JSON_STR)
|
|
{
|
|
int ret = CMD_SUCCESS;
|
|
struct vrf *vrf;
|
|
json_object *js = NULL;
|
|
|
|
vrf = gm_cmd_vrf_lookup(vty, vrf_str, &ret);
|
|
if (ret != CMD_SUCCESS)
|
|
return ret;
|
|
|
|
if (json)
|
|
js = json_object_new_object();
|
|
gm_show_if(vty, vrf, ifname, !!detail, js);
|
|
return vty_json(vty, js);
|
|
}
|
|
|
|
static void gm_show_stats_one(struct vty *vty, struct gm_if *gm_ifp,
|
|
json_object *js_if)
|
|
{
|
|
struct gm_if_stats *stats = &gm_ifp->stats;
|
|
/* clang-format off */
|
|
struct {
|
|
const char *text;
|
|
const char *js_key;
|
|
uint64_t *val;
|
|
} *item, items[] = {
|
|
{ "v2 reports received", "rxV2Reports", &stats->rx_new_report },
|
|
{ "v1 reports received", "rxV1Reports", &stats->rx_old_report },
|
|
{ "v1 done received", "rxV1Done", &stats->rx_old_leave },
|
|
|
|
{ "v2 *,* queries received", "rxV2QueryGeneral", &stats->rx_query_new_general },
|
|
{ "v2 *,G queries received", "rxV2QueryGroup", &stats->rx_query_new_group },
|
|
{ "v2 S,G queries received", "rxV2QueryGroupSource", &stats->rx_query_new_groupsrc },
|
|
{ "v2 S-bit queries received", "rxV2QuerySBit", &stats->rx_query_new_sbit },
|
|
{ "v1 *,* queries received", "rxV1QueryGeneral", &stats->rx_query_old_general },
|
|
{ "v1 *,G queries received", "rxV1QueryGroup", &stats->rx_query_old_group },
|
|
|
|
{ "v2 *,* queries sent", "txV2QueryGeneral", &stats->tx_query_new_general },
|
|
{ "v2 *,G queries sent", "txV2QueryGroup", &stats->tx_query_new_group },
|
|
{ "v2 S,G queries sent", "txV2QueryGroupSource", &stats->tx_query_new_groupsrc },
|
|
{ "v1 *,* queries sent", "txV1QueryGeneral", &stats->tx_query_old_general },
|
|
{ "v1 *,G queries sent", "txV1QueryGroup", &stats->tx_query_old_group },
|
|
{ "TX errors", "txErrors", &stats->tx_query_fail },
|
|
|
|
{ "RX dropped (checksum error)", "rxDropChecksum", &stats->rx_drop_csum },
|
|
{ "RX dropped (invalid source)", "rxDropSrcAddr", &stats->rx_drop_srcaddr },
|
|
{ "RX dropped (invalid dest.)", "rxDropDstAddr", &stats->rx_drop_dstaddr },
|
|
{ "RX dropped (missing alert)", "rxDropRtrAlert", &stats->rx_drop_ra },
|
|
{ "RX dropped (malformed pkt.)", "rxDropMalformed", &stats->rx_drop_malformed },
|
|
{ "RX truncated reports", "rxTruncatedRep", &stats->rx_trunc_report },
|
|
};
|
|
/* clang-format on */
|
|
|
|
for (item = items; item < items + array_size(items); item++) {
|
|
if (js_if)
|
|
json_object_int_add(js_if, item->js_key, *item->val);
|
|
else
|
|
vty_out(vty, " %-30s %" PRIu64 "\n", item->text,
|
|
*item->val);
|
|
}
|
|
}
|
|
|
|
static void gm_show_stats_vrf(struct vty *vty, struct vrf *vrf,
|
|
const char *ifname, json_object *js)
|
|
{
|
|
struct interface *ifp;
|
|
json_object *js_vrf;
|
|
|
|
if (js) {
|
|
js_vrf = json_object_new_object();
|
|
json_object_object_add(js, vrf->name, js_vrf);
|
|
}
|
|
|
|
FOR_ALL_INTERFACES (vrf, ifp) {
|
|
struct pim_interface *pim_ifp;
|
|
struct gm_if *gm_ifp;
|
|
json_object *js_if = NULL;
|
|
|
|
if (ifname && strcmp(ifp->name, ifname))
|
|
continue;
|
|
|
|
if (!ifp->info)
|
|
continue;
|
|
pim_ifp = ifp->info;
|
|
if (!pim_ifp->mld)
|
|
continue;
|
|
gm_ifp = pim_ifp->mld;
|
|
|
|
if (js) {
|
|
js_if = json_object_new_object();
|
|
json_object_object_add(js_vrf, ifp->name, js_if);
|
|
} else {
|
|
vty_out(vty, "Interface: %s\n", ifp->name);
|
|
}
|
|
gm_show_stats_one(vty, gm_ifp, js_if);
|
|
if (!js)
|
|
vty_out(vty, "\n");
|
|
}
|
|
}
|
|
|
|
DEFPY(gm_show_interface_stats,
|
|
gm_show_interface_stats_cmd,
|
|
"show ipv6 mld [vrf <VRF|all>$vrf_str] statistics [interface IFNAME] [json$json]",
|
|
SHOW_STR
|
|
IPV6_STR
|
|
MLD_STR
|
|
VRF_FULL_CMD_HELP_STR
|
|
"MLD statistics\n"
|
|
INTERFACE_STR
|
|
"Interface name\n"
|
|
JSON_STR)
|
|
{
|
|
int ret = CMD_SUCCESS;
|
|
struct vrf *vrf;
|
|
json_object *js = NULL;
|
|
|
|
vrf = gm_cmd_vrf_lookup(vty, vrf_str, &ret);
|
|
if (ret != CMD_SUCCESS)
|
|
return ret;
|
|
|
|
if (json)
|
|
js = json_object_new_object();
|
|
|
|
if (vrf)
|
|
gm_show_stats_vrf(vty, vrf, ifname, js);
|
|
else
|
|
RB_FOREACH (vrf, vrf_name_head, &vrfs_by_name)
|
|
gm_show_stats_vrf(vty, vrf, ifname, js);
|
|
return vty_json(vty, js);
|
|
}
|
|
|
|
static void gm_show_joins_one(struct vty *vty, struct gm_if *gm_ifp,
|
|
const struct prefix_ipv6 *groups,
|
|
const struct prefix_ipv6 *sources, bool detail,
|
|
json_object *js_if)
|
|
{
|
|
struct gm_sg *sg, *sg_start;
|
|
json_object *js_group = NULL;
|
|
pim_addr js_grpaddr = PIMADDR_ANY;
|
|
struct gm_subscriber sub_ref = {}, *sub_untracked;
|
|
|
|
if (groups) {
|
|
struct gm_sg sg_ref = {};
|
|
|
|
sg_ref.sgaddr.grp = pim_addr_from_prefix(groups);
|
|
sg_start = gm_sgs_find_gteq(gm_ifp->sgs, &sg_ref);
|
|
} else
|
|
sg_start = gm_sgs_first(gm_ifp->sgs);
|
|
|
|
sub_ref.addr = gm_dummy_untracked;
|
|
sub_untracked = gm_subscribers_find(gm_ifp->subscribers, &sub_ref);
|
|
/* NB: sub_untracked may be NULL if no untracked joins exist */
|
|
|
|
frr_each_from (gm_sgs, gm_ifp->sgs, sg, sg_start) {
|
|
struct timeval *recent = NULL, *untracked = NULL;
|
|
json_object *js_src;
|
|
|
|
if (groups) {
|
|
struct prefix grp_p;
|
|
|
|
pim_addr_to_prefix(&grp_p, sg->sgaddr.grp);
|
|
if (!prefix_match(groups, &grp_p))
|
|
break;
|
|
}
|
|
|
|
if (sources) {
|
|
struct prefix src_p;
|
|
|
|
pim_addr_to_prefix(&src_p, sg->sgaddr.src);
|
|
if (!prefix_match(sources, &src_p))
|
|
continue;
|
|
}
|
|
|
|
if (sg->most_recent) {
|
|
struct gm_packet_state *packet;
|
|
|
|
packet = gm_packet_sg2state(sg->most_recent);
|
|
recent = &packet->received;
|
|
}
|
|
|
|
if (sub_untracked) {
|
|
struct gm_packet_state *packet;
|
|
struct gm_packet_sg *item;
|
|
|
|
item = gm_packet_sg_find(sg, GM_SUB_POS, sub_untracked);
|
|
if (item) {
|
|
packet = gm_packet_sg2state(item);
|
|
untracked = &packet->received;
|
|
}
|
|
}
|
|
|
|
if (!js_if) {
|
|
FMT_NSTD_BEGIN; /* %.0p */
|
|
vty_out(vty,
|
|
"%-30pPA %-30pPAs %-16s %10.0pTVMs %10.0pTVMs %10.0pTVMs\n",
|
|
&sg->sgaddr.grp, &sg->sgaddr.src,
|
|
gm_states[sg->state], recent, untracked,
|
|
&sg->created);
|
|
|
|
if (!detail)
|
|
continue;
|
|
|
|
struct gm_packet_sg *item;
|
|
struct gm_packet_state *packet;
|
|
|
|
frr_each (gm_packet_sg_subs, sg->subs_positive, item) {
|
|
packet = gm_packet_sg2state(item);
|
|
|
|
if (packet->subscriber == sub_untracked)
|
|
continue;
|
|
vty_out(vty, " %-58pPA %-16s %10.0pTVMs\n",
|
|
&packet->subscriber->addr, "(JOIN)",
|
|
&packet->received);
|
|
}
|
|
frr_each (gm_packet_sg_subs, sg->subs_negative, item) {
|
|
packet = gm_packet_sg2state(item);
|
|
|
|
if (packet->subscriber == sub_untracked)
|
|
continue;
|
|
vty_out(vty, " %-58pPA %-16s %10.0pTVMs\n",
|
|
&packet->subscriber->addr, "(PRUNE)",
|
|
&packet->received);
|
|
}
|
|
FMT_NSTD_END; /* %.0p */
|
|
continue;
|
|
}
|
|
/* if (js_if) */
|
|
|
|
if (!js_group || pim_addr_cmp(js_grpaddr, sg->sgaddr.grp)) {
|
|
js_group = json_object_new_object();
|
|
json_object_object_addf(js_if, js_group, "%pPA",
|
|
&sg->sgaddr.grp);
|
|
js_grpaddr = sg->sgaddr.grp;
|
|
}
|
|
|
|
js_src = json_object_new_object();
|
|
json_object_object_addf(js_group, js_src, "%pPAs",
|
|
&sg->sgaddr.src);
|
|
|
|
json_object_string_add(js_src, "state", gm_states[sg->state]);
|
|
json_object_string_addf(js_src, "created", "%pTVMs",
|
|
&sg->created);
|
|
json_object_string_addf(js_src, "lastSeen", "%pTVMs", recent);
|
|
|
|
if (untracked)
|
|
json_object_string_addf(js_src, "untrackedLastSeen",
|
|
"%pTVMs", untracked);
|
|
if (!detail)
|
|
continue;
|
|
|
|
json_object *js_subs;
|
|
struct gm_packet_sg *item;
|
|
struct gm_packet_state *packet;
|
|
|
|
js_subs = json_object_new_object();
|
|
json_object_object_add(js_src, "joinedBy", js_subs);
|
|
frr_each (gm_packet_sg_subs, sg->subs_positive, item) {
|
|
packet = gm_packet_sg2state(item);
|
|
if (packet->subscriber == sub_untracked)
|
|
continue;
|
|
|
|
json_object *js_sub;
|
|
|
|
js_sub = json_object_new_object();
|
|
json_object_object_addf(js_subs, js_sub, "%pPA",
|
|
&packet->subscriber->addr);
|
|
json_object_string_addf(js_sub, "lastSeen", "%pTVMs",
|
|
&packet->received);
|
|
}
|
|
|
|
js_subs = json_object_new_object();
|
|
json_object_object_add(js_src, "prunedBy", js_subs);
|
|
frr_each (gm_packet_sg_subs, sg->subs_negative, item) {
|
|
packet = gm_packet_sg2state(item);
|
|
if (packet->subscriber == sub_untracked)
|
|
continue;
|
|
|
|
json_object *js_sub;
|
|
|
|
js_sub = json_object_new_object();
|
|
json_object_object_addf(js_subs, js_sub, "%pPA",
|
|
&packet->subscriber->addr);
|
|
json_object_string_addf(js_sub, "lastSeen", "%pTVMs",
|
|
&packet->received);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void gm_show_joins_vrf(struct vty *vty, struct vrf *vrf,
|
|
const char *ifname,
|
|
const struct prefix_ipv6 *groups,
|
|
const struct prefix_ipv6 *sources, bool detail,
|
|
json_object *js)
|
|
{
|
|
struct interface *ifp;
|
|
json_object *js_vrf;
|
|
|
|
if (js) {
|
|
js_vrf = json_object_new_object();
|
|
json_object_string_add(js_vrf, "vrf", vrf->name);
|
|
json_object_object_add(js, vrf->name, js_vrf);
|
|
}
|
|
|
|
FOR_ALL_INTERFACES (vrf, ifp) {
|
|
struct pim_interface *pim_ifp;
|
|
struct gm_if *gm_ifp;
|
|
json_object *js_if = NULL;
|
|
|
|
if (ifname && strcmp(ifp->name, ifname))
|
|
continue;
|
|
|
|
if (!ifp->info)
|
|
continue;
|
|
pim_ifp = ifp->info;
|
|
if (!pim_ifp->mld)
|
|
continue;
|
|
gm_ifp = pim_ifp->mld;
|
|
|
|
if (js) {
|
|
js_if = json_object_new_object();
|
|
json_object_object_add(js_vrf, ifp->name, js_if);
|
|
}
|
|
|
|
if (!js && !ifname)
|
|
vty_out(vty, "\nOn interface %s:\n", ifp->name);
|
|
|
|
gm_show_joins_one(vty, gm_ifp, groups, sources, detail, js_if);
|
|
}
|
|
}
|
|
|
|
DEFPY(gm_show_interface_joins,
|
|
gm_show_interface_joins_cmd,
|
|
"show ipv6 mld [vrf <VRF|all>$vrf_str] joins [{interface IFNAME|groups X:X::X:X/M|sources X:X::X:X/M|detail$detail}] [json$json]",
|
|
SHOW_STR
|
|
IPV6_STR
|
|
MLD_STR
|
|
VRF_FULL_CMD_HELP_STR
|
|
"MLD joined groups & sources\n"
|
|
INTERFACE_STR
|
|
"Interface name\n"
|
|
"Limit output to group range\n"
|
|
"Show groups covered by this prefix\n"
|
|
"Limit output to source range\n"
|
|
"Show sources covered by this prefix\n"
|
|
"Show details, including tracked receivers\n"
|
|
JSON_STR)
|
|
{
|
|
int ret = CMD_SUCCESS;
|
|
struct vrf *vrf;
|
|
json_object *js = NULL;
|
|
|
|
vrf = gm_cmd_vrf_lookup(vty, vrf_str, &ret);
|
|
if (ret != CMD_SUCCESS)
|
|
return ret;
|
|
|
|
if (json)
|
|
js = json_object_new_object();
|
|
else
|
|
vty_out(vty, "%-30s %-30s %-16s %10s %10s %10s\n", "Group",
|
|
"Source", "State", "LastSeen", "NonTrkSeen", "Created");
|
|
|
|
if (vrf)
|
|
gm_show_joins_vrf(vty, vrf, ifname, groups, sources, !!detail,
|
|
js);
|
|
else
|
|
RB_FOREACH (vrf, vrf_name_head, &vrfs_by_name)
|
|
gm_show_joins_vrf(vty, vrf, ifname, groups, sources,
|
|
!!detail, js);
|
|
return vty_json(vty, js);
|
|
}
|
|
|
|
static void gm_show_groups(struct vty *vty, struct vrf *vrf, bool uj)
|
|
{
|
|
struct interface *ifp;
|
|
struct ttable *tt = NULL;
|
|
char *table;
|
|
json_object *json = NULL;
|
|
json_object *json_iface = NULL;
|
|
json_object *json_group = NULL;
|
|
json_object *json_groups = NULL;
|
|
struct pim_instance *pim = vrf->info;
|
|
|
|
if (uj) {
|
|
json = json_object_new_object();
|
|
json_object_int_add(json, "totalGroups", pim->gm_group_count);
|
|
json_object_int_add(json, "watermarkLimit",
|
|
pim->gm_watermark_limit);
|
|
} else {
|
|
/* Prepare table. */
|
|
tt = ttable_new(&ttable_styles[TTSTYLE_BLANK]);
|
|
ttable_add_row(tt, "Interface|Group|Version|Uptime");
|
|
tt->style.cell.rpad = 2;
|
|
tt->style.corner = '+';
|
|
ttable_restyle(tt);
|
|
|
|
vty_out(vty, "Total MLD groups: %u\n", pim->gm_group_count);
|
|
vty_out(vty, "Watermark warn limit(%s): %u\n",
|
|
pim->gm_watermark_limit ? "Set" : "Not Set",
|
|
pim->gm_watermark_limit);
|
|
}
|
|
|
|
/* scan interfaces */
|
|
FOR_ALL_INTERFACES (vrf, ifp) {
|
|
|
|
struct pim_interface *pim_ifp = ifp->info;
|
|
struct gm_if *gm_ifp;
|
|
struct gm_sg *sg;
|
|
|
|
if (!pim_ifp)
|
|
continue;
|
|
|
|
gm_ifp = pim_ifp->mld;
|
|
if (!gm_ifp)
|
|
continue;
|
|
|
|
/* scan mld groups */
|
|
frr_each (gm_sgs, gm_ifp->sgs, sg) {
|
|
|
|
if (uj) {
|
|
json_object_object_get_ex(json, ifp->name,
|
|
&json_iface);
|
|
|
|
if (!json_iface) {
|
|
json_iface = json_object_new_object();
|
|
json_object_pim_ifp_add(json_iface,
|
|
ifp);
|
|
json_object_object_add(json, ifp->name,
|
|
json_iface);
|
|
json_groups = json_object_new_array();
|
|
json_object_object_add(json_iface,
|
|
"groups",
|
|
json_groups);
|
|
}
|
|
|
|
json_group = json_object_new_object();
|
|
json_object_string_addf(json_group, "group",
|
|
"%pPAs",
|
|
&sg->sgaddr.grp);
|
|
|
|
json_object_int_add(json_group, "version",
|
|
pim_ifp->mld_version);
|
|
json_object_string_addf(json_group, "uptime",
|
|
"%pTVMs", &sg->created);
|
|
json_object_array_add(json_groups, json_group);
|
|
} else {
|
|
ttable_add_row(tt, "%s|%pPAs|%d|%pTVMs",
|
|
ifp->name, &sg->sgaddr.grp,
|
|
pim_ifp->mld_version,
|
|
&sg->created);
|
|
}
|
|
} /* scan gm groups */
|
|
} /* scan interfaces */
|
|
|
|
if (uj)
|
|
vty_json(vty, json);
|
|
else {
|
|
/* Dump the generated table. */
|
|
table = ttable_dump(tt, "\n");
|
|
vty_out(vty, "%s\n", table);
|
|
XFREE(MTYPE_TMP_TTABLE, table);
|
|
ttable_del(tt);
|
|
}
|
|
}
|
|
|
|
DEFPY(gm_show_mld_groups,
|
|
gm_show_mld_groups_cmd,
|
|
"show ipv6 mld [vrf <VRF|all>$vrf_str] groups [json$json]",
|
|
SHOW_STR
|
|
IPV6_STR
|
|
MLD_STR
|
|
VRF_FULL_CMD_HELP_STR
|
|
MLD_GROUP_STR
|
|
JSON_STR)
|
|
{
|
|
int ret = CMD_SUCCESS;
|
|
struct vrf *vrf;
|
|
|
|
vrf = gm_cmd_vrf_lookup(vty, vrf_str, &ret);
|
|
if (ret != CMD_SUCCESS)
|
|
return ret;
|
|
|
|
if (vrf)
|
|
gm_show_groups(vty, vrf, !!json);
|
|
else
|
|
RB_FOREACH (vrf, vrf_name_head, &vrfs_by_name)
|
|
gm_show_groups(vty, vrf, !!json);
|
|
|
|
return CMD_SUCCESS;
|
|
}
|
|
|
|
DEFPY(gm_debug_show,
|
|
gm_debug_show_cmd,
|
|
"debug show mld interface IFNAME",
|
|
DEBUG_STR
|
|
SHOW_STR
|
|
MLD_STR
|
|
INTERFACE_STR
|
|
"interface name\n")
|
|
{
|
|
struct interface *ifp;
|
|
struct pim_interface *pim_ifp;
|
|
struct gm_if *gm_ifp;
|
|
|
|
ifp = if_lookup_by_name(ifname, VRF_DEFAULT);
|
|
if (!ifp) {
|
|
vty_out(vty, "%% no such interface: %pSQq\n", ifname);
|
|
return CMD_WARNING;
|
|
}
|
|
|
|
pim_ifp = ifp->info;
|
|
if (!pim_ifp) {
|
|
vty_out(vty, "%% no PIM state for interface %pSQq\n", ifname);
|
|
return CMD_WARNING;
|
|
}
|
|
|
|
gm_ifp = pim_ifp->mld;
|
|
if (!gm_ifp) {
|
|
vty_out(vty, "%% no MLD state for interface %pSQq\n", ifname);
|
|
return CMD_WARNING;
|
|
}
|
|
|
|
vty_out(vty, "querier: %pPA\n", &gm_ifp->querier);
|
|
vty_out(vty, "ll_lowest: %pPA\n\n", &pim_ifp->ll_lowest);
|
|
vty_out(vty, "t_query: %pTHD\n", gm_ifp->t_query);
|
|
vty_out(vty, "t_other_querier: %pTHD\n", gm_ifp->t_other_querier);
|
|
vty_out(vty, "t_expire: %pTHD\n", gm_ifp->t_expire);
|
|
|
|
vty_out(vty, "\nn_pending: %u\n", gm_ifp->n_pending);
|
|
for (size_t i = 0; i < gm_ifp->n_pending; i++) {
|
|
int64_t query, expiry;
|
|
|
|
query = monotime_since(&gm_ifp->pending[i].query, NULL);
|
|
expiry = monotime_until(&gm_ifp->pending[i].expiry, NULL);
|
|
|
|
vty_out(vty, "[%zu]: query %"PRId64"ms ago, expiry in %"PRId64"ms\n",
|
|
i, query / 1000, expiry / 1000);
|
|
}
|
|
|
|
struct gm_sg *sg;
|
|
struct gm_packet_state *pkt;
|
|
struct gm_packet_sg *item;
|
|
struct gm_subscriber *subscriber;
|
|
|
|
vty_out(vty, "\n%zu S,G entries:\n", gm_sgs_count(gm_ifp->sgs));
|
|
frr_each (gm_sgs, gm_ifp->sgs, sg) {
|
|
vty_out(vty, "\t%pSG t_expire=%pTHD\n", &sg->sgaddr,
|
|
sg->t_sg_expire);
|
|
|
|
vty_out(vty, "\t @pos:%zu\n",
|
|
gm_packet_sg_subs_count(sg->subs_positive));
|
|
frr_each (gm_packet_sg_subs, sg->subs_positive, item) {
|
|
pkt = gm_packet_sg2state(item);
|
|
|
|
vty_out(vty, "\t\t+%s%s [%pPAs %p] %p+%u\n",
|
|
item->is_src ? "S" : "",
|
|
item->is_excl ? "E" : "",
|
|
&pkt->subscriber->addr, pkt->subscriber, pkt,
|
|
item->offset);
|
|
|
|
assert(item->sg == sg);
|
|
}
|
|
vty_out(vty, "\t @neg:%zu\n",
|
|
gm_packet_sg_subs_count(sg->subs_negative));
|
|
frr_each (gm_packet_sg_subs, sg->subs_negative, item) {
|
|
pkt = gm_packet_sg2state(item);
|
|
|
|
vty_out(vty, "\t\t-%s%s [%pPAs %p] %p+%u\n",
|
|
item->is_src ? "S" : "",
|
|
item->is_excl ? "E" : "",
|
|
&pkt->subscriber->addr, pkt->subscriber, pkt,
|
|
item->offset);
|
|
|
|
assert(item->sg == sg);
|
|
}
|
|
}
|
|
|
|
vty_out(vty, "\n%zu subscribers:\n",
|
|
gm_subscribers_count(gm_ifp->subscribers));
|
|
frr_each (gm_subscribers, gm_ifp->subscribers, subscriber) {
|
|
vty_out(vty, "\t%pPA %p %zu packets\n", &subscriber->addr,
|
|
subscriber, gm_packets_count(subscriber->packets));
|
|
|
|
frr_each (gm_packets, subscriber->packets, pkt) {
|
|
vty_out(vty, "\t\t%p %.3fs ago %u of %u items active\n",
|
|
pkt,
|
|
monotime_since(&pkt->received, NULL) *
|
|
0.000001f,
|
|
pkt->n_active, pkt->n_sg);
|
|
|
|
for (size_t i = 0; i < pkt->n_sg; i++) {
|
|
item = pkt->items + i;
|
|
|
|
vty_out(vty, "\t\t[%zu]", i);
|
|
|
|
if (!item->sg) {
|
|
vty_out(vty, " inactive\n");
|
|
continue;
|
|
}
|
|
|
|
vty_out(vty, " %s%s %pSG nE=%u\n",
|
|
item->is_src ? "S" : "",
|
|
item->is_excl ? "E" : "",
|
|
&item->sg->sgaddr, item->n_exclude);
|
|
}
|
|
}
|
|
}
|
|
|
|
return CMD_SUCCESS;
|
|
}
|
|
|
|
DEFPY(gm_debug_iface_cfg,
|
|
gm_debug_iface_cfg_cmd,
|
|
"debug ipv6 mld {"
|
|
"robustness (0-7)|"
|
|
"query-max-response-time (1-8387584)"
|
|
"}",
|
|
DEBUG_STR
|
|
IPV6_STR
|
|
"Multicast Listener Discovery\n"
|
|
"QRV\nQRV\n"
|
|
"maxresp\nmaxresp\n")
|
|
{
|
|
VTY_DECLVAR_CONTEXT(interface, ifp);
|
|
struct pim_interface *pim_ifp;
|
|
struct gm_if *gm_ifp;
|
|
bool changed = false;
|
|
|
|
pim_ifp = ifp->info;
|
|
if (!pim_ifp) {
|
|
vty_out(vty, "%% no PIM state for interface %pSQq\n",
|
|
ifp->name);
|
|
return CMD_WARNING;
|
|
}
|
|
gm_ifp = pim_ifp->mld;
|
|
if (!gm_ifp) {
|
|
vty_out(vty, "%% no MLD state for interface %pSQq\n",
|
|
ifp->name);
|
|
return CMD_WARNING;
|
|
}
|
|
|
|
if (robustness_str && gm_ifp->cur_qrv != robustness) {
|
|
gm_ifp->cur_qrv = robustness;
|
|
changed = true;
|
|
}
|
|
if (query_max_response_time_str &&
|
|
gm_ifp->cur_max_resp != (unsigned int)query_max_response_time) {
|
|
gm_ifp->cur_max_resp = query_max_response_time;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) {
|
|
vty_out(vty, "%% MLD querier config changed, bumping\n");
|
|
gm_bump_querier(gm_ifp);
|
|
}
|
|
return CMD_SUCCESS;
|
|
}
|
|
|
|
void gm_cli_init(void);
|
|
|
|
void gm_cli_init(void)
|
|
{
|
|
install_element(VIEW_NODE, &gm_show_interface_cmd);
|
|
install_element(VIEW_NODE, &gm_show_interface_stats_cmd);
|
|
install_element(VIEW_NODE, &gm_show_interface_joins_cmd);
|
|
install_element(VIEW_NODE, &gm_show_mld_groups_cmd);
|
|
|
|
install_element(VIEW_NODE, &gm_debug_show_cmd);
|
|
install_element(INTERFACE_NODE, &gm_debug_iface_cfg_cmd);
|
|
}
|