/* * PIMv6 MLD querier * Copyright (C) 2021-2022 David Lamparter for NetDEF, Inc. * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; see the file COPYING; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ /* * 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 #include #include "lib/memory.h" #include "lib/jhash.h" #include "lib/prefix.h" #include "lib/checksum.h" #include "lib/thread.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 thread *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 */ #if PIM_IPV == 6 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, }, }; #else /* 224.0.0.1 */ static const pim_addr gm_all_hosts = { .s_addr = htonl(0xe0000001), }; /* 224.0.0.22 */ static const pim_addr gm_all_routers = { .s_addr = htonl(0xe0000016), }; static const pim_addr gm_dummy_untracked = { .s_addr = 0xffffffff, }; #endif /* 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; } static inline 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) */ static int gm_sg_cmp(const struct gm_sg *a, const struct gm_sg *b) { return pim_sgaddr_cmp(a->sgaddr, b->sgaddr); } DECLARE_RBTREE_UNIQ(gm_sgs, struct gm_sg, itm, gm_sg_cmp); 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 */ THREAD_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 */ CPP_NOTICE("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; 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); THREAD_OFF(sg->t_sg_query); sg->n_query = gm_ifp->cur_lmqc; sg->query_sbit = false; 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) { 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")); THREAD_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) { /* 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 n_src = ntohs(rechdr->n_src); 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); CPP_NOTICE("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); } CPP_NOTICE("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; } /* errors after this may at least partially process the packet */ gm_ifp->stats.rx_new_report++; hdr = (struct mld_v2_report_hdr *)data; data += sizeof(*hdr); len -= sizeof(*hdr); /* 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); n_records = ntohs(hdr->n_records); /* 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); } 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; CPP_NOTICE("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); CPP_NOTICE("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 thread *t) { struct gm_if *gm_ifp = THREAD_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); thread_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) THREAD_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); thread_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 thread *t) { struct gm_sg *sg = THREAD_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 = thread_timer_remain(sg->t_sg_expire); if (timercmp(&remain, &expire_wait, <=)) return; THREAD_OFF(sg->t_sg_expire); } thread_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_sg_timer_start(gm_ifp, sg, timers->expire_wait); } } static void gm_t_grp_expire(struct thread *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 = THREAD_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. */ THREAD_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_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 = thread_timer_remain(pend->t_expire); if (timercmp(&remain, &timers->expire_wait, <=)) return; THREAD_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); thread_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; THREAD_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; thread_execute(router->master, gm_t_query, gm_ifp, 0); } static void gm_t_other_querier(struct thread *t) { struct gm_if *gm_ifp = THREAD_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; thread_execute(router->master, gm_t_query, gm_ifp, 0); } 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; THREAD_OFF(gm_ifp->t_query); THREAD_OFF(gm_ifp->t_other_querier); other_ms = timers.qrv * timers.qqic_ms + timers.max_resp_ms / 2; thread_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 { 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 thread *t) { struct pim_instance *pim = THREAD_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; thread_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 thread *t) { struct gm_if *gm_ifp = THREAD_ARG(t); unsigned int timer_ms = gm_ifp->cur_query_intv; if (gm_ifp->n_startup) { timer_ms /= 4; gm_ifp->n_startup--; } thread_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 thread *t) { struct gm_sg *sg = THREAD_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 thread *t) { struct gm_gsq_pending *pend_gsq = THREAD_ARG(t); gm_send_specific(pend_gsq); } static void gm_trigger_specific(struct gm_sg *sg) { struct gm_if *gm_ifp = sg->iface; struct pim_interface *pim_ifp = gm_ifp->ifp->info; struct gm_gsq_pending *pend_gsq, ref = {}; sg->n_query--; if (sg->n_query) thread_add_timer_msec(router->master, gm_t_sg_query, sg, gm_ifp->cur_query_intv_trig, &sg->t_sg_query); if (!IPV6_ADDR_SAME(&gm_ifp->querier, &pim_ifp->ll_lowest)) return; 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); thread_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)) { THREAD_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); } thread_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; THREAD_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))) { THREAD_OFF(pend_grp->t_expire); XFREE(MTYPE_GM_GRP_PENDING, pend_grp); } while ((pend_gsq = gm_gsq_pends_pop(gm_ifp->gsq_pends))) { THREAD_OFF(pend_gsq->t_send); XFREE(MTYPE_GM_GSQ_PENDING, pend_gsq); } while ((sg = gm_sgs_pop(gm_ifp->sgs))) { THREAD_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")); THREAD_OFF(gm_ifp->t_query); THREAD_OFF(gm_ifp->t_other_querier); THREAD_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; THREAD_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; thread_execute(router->master, gm_t_query, gm_ifp, 0); } 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); } 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" #ifndef VTYSH_EXTRACT_PL #include "pimd/pim6_mld_clippy.c" #endif 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 pim_interface *pim_ifp = (struct pim_interface *)ifp->info; struct gm_if *gm_ifp = pim_ifp->mld; bool querier; if (!gm_ifp) { if (js_if) json_object_string_add(js_if, "state", "down"); else vty_out(vty, "%-16s %5s\n", ifp->name, "down"); return; } 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_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 { vty_out(vty, "%-16s %-5s %d %-25pPA %-5s %11pTH %pTVMs\n", ifp->name, "up", gm_ifp->cur_version, &gm_ifp->querier, querier ? "query" : "other", querier ? gm_ifp->t_query : gm_ifp->t_other_querier, &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; if (js) { js_vrf = json_object_new_object(); json_object_object_add(js, vrf->name, js_vrf); } 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; } if (!ifp->info) continue; if (js) { js_if = json_object_new_object(); json_object_object_add(js_vrf, ifp->name, js_if); } gm_show_if_one(vty, ifp, js_if); } } static void gm_show_if(struct vty *vty, struct vrf *vrf, const char *ifname, bool detail, json_object *js) { if (!js && !detail) vty_out(vty, "%-16s %-5s V %-25s %-18s %s\n", "Interface", "State", "Querier", "Timer", "Uptime"); 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_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_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, "%pPA", &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_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_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, table); ttable_del(tt); } } DEFPY(gm_show_mld_groups, gm_show_mld_groups_cmd, "show ipv6 mld [vrf $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" INTERFACE_STR "interface name") { 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); }