--- /dev/null
+/*
+ * BIRD --IGMP protocol
+ *
+ * (c) 2016 Ondrej Hlavaty <aearsis@eideo.cz>
+ *
+ * Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+/*
+ * DOC: Internet Group Management Protocol, Version 2
+ *
+ * The Internet Group Management Protocol (IGMP) is used by IP hosts to report
+ * their multicast group memberships to any immediately- neighboring multicast
+ * routers. This memo describes only the use of IGMP between hosts and routers
+ * to determine group membership. Routers that are members of multicast groups
+ * are expected to behave as hosts as well as routers, and may even respond to
+ * their own queries. IGMP may also be used between routers, but such use is
+ * not specified here.
+ *
+ * Its implementation is split into three files, |packets.c| handling low-level
+ * packet formats and |igmp.c| implementing BIRD interface and protocol logic.
+ *
+ * IGMP communicates with hosts, and publishes requests for groups and
+ * interfaces to the BIRD's mutlicast request table. It needs to hold state for
+ * every group with local listeners.
+ */
+
+#include "igmp.h"
+#include "lib/ip.h"
+#include "conf/conf.h"
+
+#define HASH_GRP_KEY(n) n->ga
+#define HASH_GRP_NEXT(n) n->next
+#define HASH_GRP_EQ(a,b) ip4_equal(a,b)
+#define HASH_GRP_FN(k) ip4_hash(k)
+
+
+/*
+ * A change occured, update the request tables.
+ */
+static void
+igmp_notify_routing(struct igmp_grp *grp, int join)
+{
+ net_addr_mreq4 addr = NET_ADDR_MREQ4(grp->ga, grp->ifa->iface->index);
+ struct igmp_proto *p = grp->ifa->proto;
+
+ TRACE(D_EVENTS, "iface %s %s group %I", grp->ifa->iface->name, join ? "joined" : "left", ipa_from_ip4(grp->ga));
+
+ net *n = net_get(p->mreq_channel->table, (net_addr *) &addr);
+ if (join)
+ {
+ rta a0 = {
+ .src = p->p.main_source,
+ .source = RTS_IGMP,
+ .dest = RTD_MREQUEST,
+ .iface = grp->ifa->iface,
+ };
+ rta *a = rta_lookup(&a0);
+ rte *e = rte_get_temp(a);
+
+ e->net = n;
+ rte_update2(p->mreq_channel, (net_addr *) &addr, e, p->p.main_source);
+ }
+ else
+ {
+ rte_update2(p->mreq_channel, (net_addr *) &addr, NULL, p->p.main_source);
+ }
+}
+
+static void
+igmp_gen_query_hook(struct timer *tm)
+{
+ struct igmp_iface *ifa = tm->data;
+
+ if (ifa->startup_query_cnt > 0)
+ ifa->startup_query_cnt--;
+
+ igmp_tx_query(ifa, IP4_NONE);
+
+ tm_start(ifa->gen_query, ifa->startup_query_cnt
+ ? ifa->cf->startup_query_int TO_S
+ : ifa->cf->query_int TO_S);
+}
+
+static void
+igmp_other_present_expire(struct timer *tm)
+{
+ struct igmp_iface *ifa = tm->data;
+
+ ifa->query_state = IGMP_QS_QUERIER;
+ tm_start(ifa->gen_query, 0);
+}
+
+/******************************************************************************
+ Group state management
+ ******************************************************************************/
+
+
+static void
+igmp_grp_free(struct igmp_grp *grp)
+{
+ rfree(grp->join_timer);
+ rfree(grp->v1_host_timer);
+ rfree(grp->rxmt_timer);
+
+ HASH_REMOVE(grp->ifa->groups, HASH_GRP, grp);
+ mb_free(grp);
+}
+
+static void
+igmp_grp_v1_timer_expire(struct timer *tm)
+{
+ struct igmp_grp *grp = tm->data;
+
+ if (grp->join_state == IGMP_JS_V1MEMB)
+ grp->join_state = IGMP_JS_MEMB;
+ else
+ bug("V1 timer expired without v1 hosts");
+}
+
+static void
+igmp_grp_join_timer_expire(struct timer *tm)
+{
+ struct igmp_grp *grp = tm->data;
+
+ switch (grp->join_state) {
+ case IGMP_JS_NOMEMB:
+ bug("IGMP GRP without members expired.");
+ return;
+ case IGMP_JS_V1MEMB:
+ case IGMP_JS_MEMB:
+ case IGMP_JS_CHECK:
+ grp->join_state = IGMP_JS_NOMEMB;
+ igmp_notify_routing(grp, 0);
+ igmp_grp_free(grp);
+ }
+}
+
+static void
+igmp_grp_retransmit_expire(struct timer *tm)
+{
+ struct igmp_grp *grp = tm->data;
+
+ if (grp->join_state != IGMP_JS_CHECK)
+ return;
+
+ igmp_tx_query(grp->ifa, grp->ga);
+ tm_start(grp->rxmt_timer, grp->ifa->cf->last_member_query_int TO_S);
+}
+
+struct igmp_grp *
+igmp_grp_new(struct igmp_iface *ifa, ip4_addr *ga)
+{
+ struct igmp_proto *p = ifa->proto;
+ struct igmp_grp *grp = mb_allocz(p->p.pool, sizeof(struct igmp_grp));
+ grp->ga = *ga;
+ grp->ifa = ifa;
+ HASH_INSERT(ifa->groups, HASH_GRP, grp);
+
+ grp->join_timer = tm_new_set(ifa->proto->p.pool, igmp_grp_join_timer_expire, grp, 0, 0);
+ grp->rxmt_timer = tm_new_set(ifa->proto->p.pool, igmp_grp_retransmit_expire, grp, 0, 0);
+ grp->v1_host_timer = tm_new_set(ifa->proto->p.pool, igmp_grp_v1_timer_expire, grp, 0, 0);
+
+ return grp;
+}
+
+struct igmp_grp *
+igmp_grp_find(struct igmp_iface *ifa, ip4_addr *ga)
+{
+ return HASH_FIND(ifa->groups, HASH_GRP, *ga);
+}
+
+
+/******************************************************************************
+ Iface management
+ ******************************************************************************/
+
+static inline int
+igmp_iface_is_up(struct igmp_iface *ifa)
+{
+ return !!ifa->sk;
+}
+
+static struct igmp_iface *
+igmp_iface_new(struct igmp_proto *p, struct iface *iface, struct igmp_iface_config *ic)
+{
+ struct igmp_iface *ifa = mb_allocz(p->p.pool, sizeof(struct igmp_iface));
+ add_tail(&p->iface_list, NODE ifa);
+ ifa->iface = iface;
+ ifa->cf = ic;
+ ifa->proto = p;
+ ifa->gen_id = random_u32();
+ ifa->startup_query_cnt = ifa->cf->startup_query_cnt;
+
+ ifa->gen_query = tm_new_set(p->p.pool, igmp_gen_query_hook, ifa, 0, 0);
+ ifa->other_present = tm_new_set(p->p.pool, igmp_other_present_expire, ifa, 0, 0);
+
+ HASH_INIT(ifa->groups, p->p.pool, 8);
+
+ if (igmp_sk_open(ifa))
+ log(L_ERR "Failed opening socket for IGMP");
+
+ ifa->query_state = IGMP_QS_QUERIER;
+ tm_start(ifa->gen_query, 0);
+
+ return ifa;
+}
+
+static int
+igmp_iface_down(struct igmp_iface *ifa)
+{
+ if (!igmp_iface_is_up(ifa))
+ return 0;
+
+ rfree(ifa->sk);
+ ifa->sk = NULL;
+ return 0;
+}
+
+static int
+igmp_iface_free(struct igmp_iface* ifa)
+{
+ rem_node(NODE ifa);
+
+ HASH_WALK_DELSAFE(ifa->groups, next, grp)
+ {
+ igmp_notify_routing(grp, 0);
+ igmp_grp_free(grp);
+ }
+ HASH_WALK_END;
+
+ rfree(ifa->gen_query);
+ rfree(ifa->other_present);
+ mb_free(ifa);
+ return 0;
+}
+
+static char *join_states[] = {
+ [IGMP_JS_NOMEMB] = "no members",
+ [IGMP_JS_MEMB] = "members",
+ [IGMP_JS_V1MEMB] = "v1 members",
+ [IGMP_JS_CHECK] = "about to expire",
+};
+
+static char *query_states[] = {
+ [IGMP_QS_INIT] = "initializing",
+ [IGMP_QS_QUERIER] = "querier",
+ [IGMP_QS_NONQUERIER] = "other querier present",
+};
+
+static void
+igmp_iface_dump(struct igmp_iface *ifa)
+{
+ struct igmp_proto *p = ifa->proto;
+
+ debug("\tInterface %s is %s, %s\n", ifa->iface->name, igmp_iface_is_up(ifa) ? "up" : "down", query_states[ifa->query_state]);
+
+ HASH_WALK(ifa->groups, next, grp)
+ TRACE(D_EVENTS, "\t\tGroup %I4: %s\n", grp->ga, join_states[grp->join_state]);
+ HASH_WALK_END;
+}
+
+struct igmp_iface *
+igmp_iface_find(struct igmp_proto *p, struct iface * ifa)
+{
+ struct igmp_iface * pif;
+ WALK_LIST(pif, p->iface_list)
+ if (pif->iface == ifa)
+ return pif;
+
+ return NULL;
+}
+
+void
+igmp_iface_config_init(struct igmp_iface_config * ifc)
+{
+ init_list(&ifc->i.ipn_list);
+
+ ifc->robustness = 2;
+ ifc->query_int = 125 S;
+ ifc->query_response_int = 10 S;
+ ifc->last_member_query_int = 1 S;
+ ifc->last_member_query_cnt = -1U;
+
+ ifc->startup_query_cnt = -1U;
+ ifc->startup_query_int = -1U;
+}
+
+void
+igmp_iface_config_finish(struct igmp_iface_config * ifc)
+{
+ /* Explicit constraints - probably bail out? */
+ if (ifc->robustness == 0)
+ cf_error("IGMP: Robustness must be at least 1.");
+
+ if (ifc->query_response_int > ifc->query_int)
+ cf_error("IGMP: The query response interval must not be greater than the query interval.");
+
+ /* Dependent default values */
+ if (ifc->startup_query_int == -1U)
+ ifc->startup_query_int = ifc->query_int / 4;
+
+ if (ifc->startup_query_cnt == -1U)
+ ifc->startup_query_cnt = ifc->robustness;
+
+ if (ifc->last_member_query_cnt == -1U)
+ ifc->last_member_query_cnt = ifc->robustness;
+
+ ifc->group_memb_int = ifc->robustness * ifc->query_int + ifc->query_response_int;
+ ifc->other_querier_int = ifc->robustness * ifc->query_int + ifc->query_response_int / 2;
+}
+
+/******************************************************************************
+ Protocol logic
+ ******************************************************************************/
+
+int
+igmp_query_received(struct igmp_iface *ifa, ip4_addr from)
+{
+
+ if (ifa->query_state != IGMP_QS_QUERIER)
+ return 0;
+
+ /* Find first IPv4 address of the interface */
+ /* XXX: cache and update in if_notify? */
+ struct ifa *my_addr = ifa_find_match(ifa->iface, NB_IP4);
+
+ /* Another router with lower IP shall be the Querier */
+ if (ip4_compare(ipa_to_ip4(my_addr->ip), from) > 0)
+ {
+ ifa->query_state = IGMP_QS_NONQUERIER;
+ tm_start(ifa->other_present, ifa->cf->other_querier_int TO_S);
+ }
+
+ return 0;
+}
+
+int
+igmp_membership_report(struct igmp_grp *grp, u8 igmp_version, u8 resp_time)
+{
+ struct igmp_proto *p = grp->ifa->proto;
+ uint last_state = grp->join_state;
+ TRACE(D_PACKETS, "Membership report received for group %I4 on iface %s", grp->ga, grp->ifa->iface->name);
+
+ if (grp->ifa->query_state == IGMP_QS_QUERIER && igmp_version == 1)
+ {
+ grp->join_state = IGMP_JS_V1MEMB;
+ tm_start(grp->v1_host_timer, grp->ifa->cf->group_memb_int TO_S);
+ }
+
+ if (grp->join_state != IGMP_JS_V1MEMB)
+ grp->join_state = IGMP_JS_MEMB;
+
+ tm_stop(grp->rxmt_timer);
+ tm_start(grp->join_timer, grp->ifa->cf->group_memb_int TO_S);
+
+ if (last_state == IGMP_JS_NOMEMB)
+ igmp_notify_routing(grp, 1);
+
+ return 0;
+}
+
+int
+igmp_leave(struct igmp_grp *grp, u8 resp_time)
+{
+ if (!grp)
+ return 0;
+
+ struct igmp_proto *p = grp->ifa->proto;
+
+ TRACE(D_PACKETS, "Leave received for group %I4 on iface %s", grp->ga, grp->ifa->iface->name);
+ grp->join_state = IGMP_JS_CHECK;
+ tm_start(grp->rxmt_timer, 0);
+ tm_start(grp->join_timer, (grp->ifa->cf->last_member_query_int TO_S) * grp->ifa->cf->last_member_query_cnt);
+ return 0;
+}
+
+/******************************************************************************
+ Others
+ ******************************************************************************/
+
+void
+igmp_config_init(struct igmp_config *cf)
+{
+ init_list(&cf->patt_list);
+ igmp_iface_config_init(&cf->default_iface_cf);
+ igmp_iface_config_finish(&cf->default_iface_cf);
+}
+
+void
+igmp_config_finish(struct proto_config *c)
+{
+ if (NULL == proto_cf_find_channel(c, NET_MREQ4))
+ channel_config_new(NULL, NET_MREQ4, c);
+}
+
+static void
+igmp_if_notify(struct proto *P, uint flags, struct iface *iface)
+{
+ struct igmp_proto *p = (struct igmp_proto *) P;
+ struct igmp_config *c = (struct igmp_config *) P->cf;
+
+ if (iface->flags & IF_IGNORE)
+ return;
+
+ if (flags & IF_CHANGE_UP)
+ {
+ struct igmp_iface_config *ic;
+ ic = (struct igmp_iface_config *) iface_patt_find(&c->patt_list, iface, iface->addr);
+ if (!ic)
+ ic = &c->default_iface_cf;
+ igmp_iface_new(p, iface, ic);
+ return;
+ }
+
+
+ if (flags & IF_CHANGE_DOWN)
+ {
+ struct igmp_iface * ifa = igmp_iface_find(p, iface);
+ igmp_iface_down(ifa);
+ igmp_iface_free(ifa);
+ }
+}
+
+static int
+igmp_start(struct proto *P)
+{
+ struct igmp_proto *p = (struct igmp_proto *) P;
+ init_list(&p->iface_list);
+ return PS_UP;
+}
+
+static int
+igmp_shutdown(struct proto *P)
+{
+ struct igmp_proto *p = (struct igmp_proto *) P;
+ struct igmp_iface *ifa;
+ WALK_LIST_FIRST(ifa, p->iface_list)
+ {
+ igmp_iface_down(ifa);
+ igmp_iface_free(ifa);
+ }
+
+ return PS_DOWN;
+}
+
+static void
+igmp_dump(struct proto *P)
+{
+ struct igmp_proto *p = (struct igmp_proto *) P;
+ struct igmp_iface *ifa;
+ WALK_LIST(ifa, p->iface_list)
+ igmp_iface_dump(ifa);
+}
+
+/*
+ * We do not want to receive any route updates.
+ */
+static int
+igmp_reject(struct proto *p, rte **e, ea_list **attrs, struct linpool *pool)
+{ return -1; }
+
+static struct proto *
+igmp_init(struct proto_config *C)
+{
+ struct proto *P = proto_new(C);
+ struct igmp_proto *p = (struct igmp_proto *) P;
+
+ p->mreq_channel = proto_add_channel(P, proto_cf_find_channel(C, NET_MREQ4));
+
+ p->cf = (struct igmp_config *) C;
+ P->if_notify = igmp_if_notify;
+ P->import_control = igmp_reject;
+
+ return P;
+}
+
+struct protocol proto_igmp = {
+ .name = "IGMP",
+ .template = "igmp%d",
+ .preference = DEF_PREF_STATIC,
+ .proto_size = sizeof(struct igmp_proto),
+ .config_size = sizeof(struct igmp_config),
+ .channel_mask = NB_MREQ4,
+ .init = igmp_init,
+ .dump = igmp_dump,
+ .start = igmp_start,
+ .shutdown = igmp_shutdown,
+};
+
--- /dev/null
+/*
+ * BIRD --IGMP protocol
+ *
+ * (c) 2016 Ondrej Hlavaty <aearsis@eideo.cz>
+ *
+ * Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#include "igmp.h"
+#include "lib/checksum.h"
+
+struct igmp_pkt {
+ u8 type;
+ u8 resp_time;
+ u16 checksum;
+ u32 addr;
+};
+
+#define IGMP_TP_MS_QUERY 0x11
+#define IGMP_TP_V1_MS_REPORT 0x12
+#define IGMP_TP_V2_MS_REPORT 0x16
+#define IGMP_TP_LEAVE 0x17
+
+#define DROP(args...) do { TRACE(D_PACKETS, "Dropping packet: " args); goto drop; } while(0)
+int
+igmp_accept(struct igmp_iface *ifa, ip4_addr from, struct igmp_pkt *pkt)
+{
+ struct igmp_proto *p = ifa->proto;
+
+ if (pkt->type == IGMP_TP_MS_QUERY)
+ return igmp_query_received(ifa, from);
+
+ ip4_addr addr = get_ip4(&pkt->addr);
+ struct igmp_grp *grp = igmp_grp_find(ifa, &addr);
+
+ if (pkt->type == IGMP_TP_LEAVE)
+ return igmp_leave(grp, pkt->resp_time);
+
+ if (!grp)
+ grp = igmp_grp_new(ifa, &addr);
+
+ switch (pkt->type) {
+ case IGMP_TP_V1_MS_REPORT:
+ igmp_membership_report(grp, 1, 10);
+ break;
+
+ case IGMP_TP_V2_MS_REPORT:
+ igmp_membership_report(grp, 2, pkt->resp_time);
+ break;
+
+ default:
+ DROP("Unknown type");
+ break;
+ }
+
+drop:
+ return 0;
+}
+
+int
+igmp_rx_hook(sock *sk, int len)
+{
+ struct igmp_iface *ifa = sk->data;
+ struct igmp_proto *p = ifa->proto;
+
+ struct igmp_pkt *pkt = (struct igmp_pkt *) sk_rx_buffer(sk, &len);
+
+ if (len < sizeof(struct igmp_pkt))
+ DROP("Shorter than 8 bytes");
+
+ /* Longer packets are in IGMPv3 */
+ if (len != 8)
+ DROP("Expected pkt length 8, not %i (probably newer IGMP)", len);
+
+ if (!ipsum_verify(pkt, len, NULL))
+ DROP("Invalid checksum");
+
+ return igmp_accept(sk->data, ipa_to_ip4(sk->faddr), pkt);
+
+drop:
+ return 0;
+}
+
+void
+igmp_err_hook(sock *sk, int err)
+{
+ struct igmp_iface *ifa = sk->data;
+ struct igmp_proto *p = ifa->proto;
+
+ TRACE(D_EVENTS, "IGMP err %m", err);
+}
+
+int
+igmp_tx_query(struct igmp_iface *ifa, ip4_addr addr)
+{
+ struct igmp_proto *p = ifa->proto;
+ struct igmp_pkt *pkt = (struct igmp_pkt *) ifa->sk->tbuf;
+
+ pkt->type = IGMP_TP_MS_QUERY;
+ pkt->resp_time = (ifa->cf->query_response_int TO_MS) / 100;
+ put_ip4(&pkt->addr, addr);
+
+ pkt->checksum = 0;
+ pkt->checksum = ipsum_calculate(pkt, sizeof(struct igmp_pkt), NULL);
+
+ ifa->sk->daddr = ip4_zero(addr) ? IP4_ALL_NODES : ipa_from_ip4(addr);
+
+ if (ip4_zero(addr))
+ TRACE(D_PACKETS, "Sending general query on iface %s", ifa->iface->name);
+ else
+ TRACE(D_PACKETS, "Sending query to grp %I4 on iface %s", addr, ifa->iface->name);
+
+ sk_send(ifa->sk, 8);
+ return 0;
+}
+
+int
+igmp_sk_open(struct igmp_iface *ifa)
+{
+ sock *sk = sk_new(ifa->proto->p.pool);
+ sk->type = SK_IGMP;
+ sk->saddr = ifa->iface->addr->ip;
+ sk->iface = ifa->iface;
+
+ sk->data = ifa;
+ sk->ttl = 1;
+ sk->tos = IP_PREC_INTERNET_CONTROL;
+ sk->rx_hook = igmp_rx_hook;
+ sk->err_hook = igmp_err_hook;
+
+ sk->tbsize = ifa->iface->mtu;
+
+ if (sk_open(sk) < 0)
+ goto err;
+
+ if (sk_setup_multicast(sk) < 0)
+ goto err;
+
+ if (sk_join_group(sk, IP4_IGMP_ROUTERS) < 0)
+ goto err;
+
+ if (sk_join_group(sk, IP4_ALL_ROUTERS) < 0)
+ goto err;
+
+ ifa->sk = sk;
+ return 0;
+
+err:
+ log(L_ERR "%s: Socket error: %s%#m", ifa->proto->p.name, sk->err);
+ rfree(sk);
+ return -1;
+}