diff --git a/configure.ac b/configure.ac index f4de8f93..10089e5f 100644 --- a/configure.ac +++ b/configure.ac @@ -261,7 +261,7 @@ if test "$enable_mpls_kernel" != no ; then fi fi -all_protocols="$proto_bfd babel bgp ospf pipe radv rip $proto_rpki static" +all_protocols="$proto_bfd babel bgp igmp ospf pipe radv rip $proto_rpki static" all_protocols=`echo $all_protocols | sed 's/ /,/g'` diff --git a/lib/ip.h b/lib/ip.h index ec2d6966..f50d41ae 100644 --- a/lib/ip.h +++ b/lib/ip.h @@ -20,6 +20,7 @@ #define IP4_OSPF_ALL_ROUTERS ipa_build4(224, 0, 0, 5) #define IP4_OSPF_DES_ROUTERS ipa_build4(224, 0, 0, 6) #define IP4_RIP_ROUTERS ipa_build4(224, 0, 0, 9) +#define IP4_IGMP_ROUTERS ipa_build4(224, 0, 0, 22) #define IP6_ALL_NODES ipa_build6(0xFF020000, 0, 0, 1) #define IP6_ALL_ROUTERS ipa_build6(0xFF020000, 0, 0, 2) diff --git a/lib/timer.c b/lib/timer.c index ed731d26..a6d499af 100644 --- a/lib/timer.c +++ b/lib/timer.c @@ -172,6 +172,14 @@ tm_start(timer *t, btime after) tm_set(t, current_time() + MAX(after, 0)); } +void +tm_shift(timer *t, btime delta) +{ + btime now = current_time(); + if (t->expires && (t->expires > now)) + tm_set(t, MAX(now, t->expires + delta)); +} + void tm_stop(timer *t) { diff --git a/lib/timer.h b/lib/timer.h index ed8f0d02..80317581 100644 --- a/lib/timer.h +++ b/lib/timer.h @@ -53,6 +53,7 @@ extern btime boot_time; timer *tm_new(pool *p); void tm_set(timer *t, btime when); void tm_start(timer *t, btime after); +void tm_shift(timer *t, btime delta); void tm_stop(timer *t); static inline int diff --git a/nest/proto.c b/nest/proto.c index 15d6f4de..21ceca8d 100644 --- a/nest/proto.c +++ b/nest/proto.c @@ -1310,6 +1310,9 @@ protos_build(void) #ifdef CONFIG_RPKI proto_build(&proto_rpki); #endif +#ifdef CONFIG_IGMP + proto_build(&proto_igmp); +#endif proto_pool = rp_new(&root_pool, "Protocols"); proto_shutdown_timer = tm_new(proto_pool); diff --git a/nest/protocol.h b/nest/protocol.h index 9afd3a0a..cfafa587 100644 --- a/nest/protocol.h +++ b/nest/protocol.h @@ -79,8 +79,8 @@ void protos_dump_all(void); */ extern struct protocol - proto_device, proto_radv, proto_rip, proto_static, - proto_ospf, proto_pipe, proto_bgp, proto_bfd, proto_babel, proto_rpki; + proto_device, proto_radv, proto_rip, proto_static, proto_ospf, proto_pipe, + proto_bgp, proto_bfd, proto_babel, proto_rpki, proto_igmp; /* * Routing Protocol Instance diff --git a/nest/route.h b/nest/route.h index 27f11928..2f705946 100644 --- a/nest/route.h +++ b/nest/route.h @@ -422,7 +422,7 @@ typedef struct rta { #define RTS_PIPE 12 /* Inter-table wormhole */ #define RTS_BABEL 13 /* Babel route */ #define RTS_RPKI 14 /* Route Origin Authorization */ - +#define RTS_IGMP 15 /* IGMP multicast request */ #define RTC_UNICAST 0 #define RTC_BROADCAST 1 @@ -688,6 +688,7 @@ extern struct protocol *attr_class_to_protocol[EAP_MAX]; #define DEF_PREF_RIP 120 /* RIP */ #define DEF_PREF_BGP 100 /* BGP */ #define DEF_PREF_RPKI 100 /* RPKI */ +#define DEF_PREF_IGMP 100 /* IGMP */ #define DEF_PREF_INHERITED 10 /* Routes inherited from other routing daemons */ /* diff --git a/nest/rt-attr.c b/nest/rt-attr.c index a7b4a749..7b3824d2 100644 --- a/nest/rt-attr.c +++ b/nest/rt-attr.c @@ -1204,8 +1204,8 @@ rta_dump(rta *a) { static char *rts[] = { "RTS_DUMMY", "RTS_STATIC", "RTS_INHERIT", "RTS_DEVICE", "RTS_STAT_DEV", "RTS_REDIR", "RTS_RIP", - "RTS_OSPF", "RTS_OSPF_IA", "RTS_OSPF_EXT1", - "RTS_OSPF_EXT2", "RTS_BGP", "RTS_PIPE", "RTS_BABEL" }; + "RTS_OSPF", "RTS_OSPF_IA", "RTS_OSPF_EXT1", "RTS_OSPF_EXT2", + "RTS_BGP", "RTS_PIPE", "RTS_BABEL", "RTS_IGMP" }; static char *rtd[] = { "", " DEV", " HOLE", " UNREACH", " PROHIBIT" }; debug("p=%s uc=%d %s %s%s h=%04x", @@ -1257,7 +1257,7 @@ void rta_show(struct cli *c, rta *a, ea_list *eal) { static char *src_names[] = { "dummy", "static", "inherit", "device", "static-device", "redirect", - "RIP", "OSPF", "OSPF-IA", "OSPF-E1", "OSPF-E2", "BGP", "pipe" }; + "RIP", "OSPF", "OSPF-IA", "OSPF-E1", "OSPF-E2", "BGP", "pipe", "Babel", "RPKI", "IGMP" }; int i; cli_printf(c, -1008, "\tType: %s %s", src_names[a->source], ip_scope_text(a->scope)); diff --git a/proto/Doc b/proto/Doc index ef573d2a..fba4491a 100644 --- a/proto/Doc +++ b/proto/Doc @@ -2,6 +2,7 @@ H Protocols C babel C bfd C bgp +C igmp C ospf C pipe C radv diff --git a/proto/igmp/Doc b/proto/igmp/Doc new file mode 100644 index 00000000..1c8e46e4 --- /dev/null +++ b/proto/igmp/Doc @@ -0,0 +1,2 @@ +S igmp.c +S packets.c diff --git a/proto/igmp/Makefile b/proto/igmp/Makefile new file mode 100644 index 00000000..7b0030a6 --- /dev/null +++ b/proto/igmp/Makefile @@ -0,0 +1,6 @@ +src := igmp.c packets.c +obj := $(src-o-files) +$(all-daemon) +$(cf-local) + +tests_objs := $(tests_objs) $(src-o-files) diff --git a/proto/igmp/config.Y b/proto/igmp/config.Y new file mode 100644 index 00000000..7e4a2ebb --- /dev/null +++ b/proto/igmp/config.Y @@ -0,0 +1,86 @@ +/* + * BIRD -- Internet Group Management Protocol (IGMP) + * + * (c) 2016 Ondrej Hlavaty + * (c) 2018 Ondrej Zajicek + * (c) 2018 CZ.NIC z.s.p.o. + * + * Can be freely distributed and used under the terms of the GNU GPL. + */ + +CF_HDR + +#include "proto/igmp/igmp.h" + +CF_DEFINES + +#define IGMP_CFG ((struct igmp_config *) this_proto) +#define IGMP_IFACE ((struct igmp_iface_config *) this_ipatt) + +CF_DECLS + +CF_KEYWORDS(IGMP, ROBUSTNESS, STARTUP, QUERY, COUNT, INTERVAL, LAST, MEMBER, RESPONSE) + +CF_GRAMMAR + +CF_ADDTO(proto, igmp_proto) + +igmp_proto_start: proto_start IGMP +{ + this_proto = proto_config_new(&proto_igmp, $1); + init_list(&IGMP_CFG->iface_list); +}; + +igmp_proto_item: + proto_item + | proto_channel + | INTERFACE igmp_iface + ; + +igmp_proto_opts: + /* empty */ + | igmp_proto_opts igmp_proto_item ';' + ; + +igmp_proto: + igmp_proto_start proto_name '{' igmp_proto_opts '}'; + + +igmp_iface_start: +{ + this_ipatt = cfg_allocz(sizeof(struct igmp_iface_config)); + add_tail(&IGMP_CFG->iface_list, NODE this_ipatt); + init_list(&IGMP_IFACE->i.ipn_list); + + IGMP_IFACE->robustness = IGMP_DEFAULT_ROBUSTNESS; + IGMP_IFACE->query_int = IGMP_DEFAULT_QUERY_INT; + IGMP_IFACE->query_response_int = IGMP_DEFAULT_RESPONSE_INT; + IGMP_IFACE->last_member_query_int = IGMP_DEFAULT_LAST_MEMBER_INT; +}; + +igmp_iface_item: + ROBUSTNESS expr { IGMP_IFACE->robustness = $2; } + | QUERY INTERVAL expr_us { IGMP_IFACE->query_int = $3; } + | STARTUP QUERY COUNT expr { IGMP_IFACE->startup_query_cnt = $4; } + | STARTUP QUERY INTERVAL expr_us { IGMP_IFACE->startup_query_int = $4; } + | QUERY RESPONSE INTERVAL expr_us { IGMP_IFACE->query_response_int = $4; } + | LAST MEMBER QUERY COUNT expr { IGMP_IFACE->last_member_query_cnt = $5; } + | LAST MEMBER QUERY INTERVAL expr_us { IGMP_IFACE->last_member_query_int = $5; } + ; + +igmp_iface_opts: + /* empty */ + | igmp_iface_opts igmp_iface_item ';' + ; + +igmp_iface_opt_list: + /* empty */ + | '{' igmp_iface_opts '}' + ; + +igmp_iface: + igmp_iface_start iface_patt_list_nopx igmp_iface_opt_list { igmp_finish_iface_config(IGMP_IFACE); } + +CF_CODE + +CF_END diff --git a/proto/igmp/igmp.c b/proto/igmp/igmp.c new file mode 100644 index 00000000..8b4ab57a --- /dev/null +++ b/proto/igmp/igmp.c @@ -0,0 +1,624 @@ +/* + * BIRD -- Internet Group Management Protocol (IGMP) + * + * (c) 2016 Ondrej Hlavaty + * (c) 2018 Ondrej Zajicek + * (c) 2018 CZ.NIC z.s.p.o. + * + * 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 mutlicast request table. It needs to hold state for + * every group with local listeners. + */ + +#include "igmp.h" + + +#define HASH_GRP_KEY(n) n->address +#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) + +static const char *igmp_join_state_names[] = { + [IGMP_JS_NO_MEMBERS] = "NoMembers", + [IGMP_JS_MEMBERS] = "Members", + [IGMP_JS_V1_MEMBERS] = "MembersV1", + [IGMP_JS_CHECKING] = "Checking", +}; + +static const char *igmp_query_state_names[] = { + [IGMP_QS_INIT] = "Init", + [IGMP_QS_QUERIER] = "Querier", + [IGMP_QS_NONQUERIER] = "Listener", +}; + +static struct igmp_group *igmp_find_group(struct igmp_iface *ifa, ip4_addr addr); +static struct igmp_group *igmp_get_group(struct igmp_iface *ifa, ip4_addr addr); +static void igmp_remove_group(struct igmp_group *grp); +static void igmp_announce_group(struct igmp_group *grp, int up); +static void igmp_group_set_state(struct igmp_group *grp, uint state); +static void igmp_iface_set_state(struct igmp_iface *ifa, uint state); + + +/* + * IGMP protocol logic + */ + +static void +igmp_query_timeout(struct timer *tm) +{ + struct igmp_iface *ifa = tm->data; + + ASSERT(ifa->query_state == IGMP_QS_QUERIER); + + igmp_send_query(ifa, IP4_NONE, ifa->cf->query_response_int); + + if (ifa->startup_query_cnt > 0) + ifa->startup_query_cnt--; + + tm_start(ifa->query_timer, ifa->startup_query_cnt + ? ifa->cf->startup_query_int + : ifa->cf->query_int); +} + +static void +igmp_other_present_timeout(struct timer *tm) +{ + struct igmp_iface *ifa = tm->data; + + ASSERT(ifa->query_state == IGMP_QS_NONQUERIER); + + igmp_iface_set_state(ifa, IGMP_QS_QUERIER); + tm_start(ifa->query_timer, 0); +} + +static void +igmp_join_timeout(struct timer *tm) +{ + struct igmp_group *grp = tm->data; + + ASSERT(grp->join_state != IGMP_JS_NO_MEMBERS); + + igmp_group_set_state(grp, IGMP_JS_NO_MEMBERS); + igmp_announce_group(grp, 0); + igmp_remove_group(grp); +} + +static void +igmp_v1_host_timeout(struct timer *tm) +{ + struct igmp_group *grp = tm->data; + + ASSERT(grp->join_state == IGMP_JS_V1_MEMBERS); + + igmp_group_set_state(grp, IGMP_JS_MEMBERS); +} + +static void +igmp_rxmt_timeout(struct timer *tm) +{ + struct igmp_group *grp = tm->data; + struct igmp_iface *ifa = grp->ifa; + + ASSERT(grp->join_state == IGMP_JS_CHECKING); + + igmp_send_query(ifa, grp->address, ifa->cf->last_member_query_int); + tm_start(grp->rxmt_timer, ifa->cf->last_member_query_int); +} + +void +igmp_handle_query(struct igmp_iface *ifa, ip4_addr addr, ip4_addr from, btime resp_time) +{ + /* Another router with lower IP shall be the Querier */ + if ((ifa->query_state == IGMP_QS_QUERIER) && + (ip4_compare(from, ipa_to_ip4(ifa->sk->saddr)) < 0)) + { + igmp_iface_set_state(ifa, IGMP_QS_NONQUERIER); + ifa->startup_query_cnt = 0; + tm_stop(ifa->query_timer); + tm_start(ifa->other_present, ifa->cf->other_querier_int); + } + + if ((ifa->query_state == IGMP_QS_NONQUERIER) && ip4_nonzero(addr)) + { + struct igmp_group *grp = igmp_find_group(ifa, addr); + + if (grp && (grp->join_state == IGMP_JS_MEMBERS)) + { + igmp_group_set_state(grp, IGMP_JS_CHECKING); + tm_start(grp->join_timer, resp_time * ifa->cf->last_member_query_cnt); + tm_stop(grp->rxmt_timer); + } + } +} + +void +igmp_handle_report(struct igmp_iface *ifa, ip4_addr addr, int version) +{ + struct igmp_group *grp = igmp_get_group(ifa, addr); + uint last_state = grp->join_state; + + if (version == 1) + { + igmp_group_set_state(grp, IGMP_JS_V1_MEMBERS); + tm_start(grp->v1_host_timer, ifa->cf->group_member_int + 100 MS); + } + else + { + if (last_state != IGMP_JS_V1_MEMBERS) + igmp_group_set_state(grp, IGMP_JS_MEMBERS); + } + + tm_start(grp->join_timer, ifa->cf->group_member_int); + tm_stop(grp->rxmt_timer); + + if (last_state == IGMP_JS_NO_MEMBERS) + igmp_announce_group(grp, 1); +} + +void +igmp_handle_leave(struct igmp_iface *ifa, ip4_addr addr) +{ + if (ifa->query_state == IGMP_QS_QUERIER) + { + struct igmp_group *grp = igmp_find_group(ifa, addr); + + if (grp && (grp->join_state == IGMP_JS_MEMBERS)) + { + igmp_group_set_state(grp, IGMP_JS_CHECKING); + tm_start(grp->join_timer, ifa->cf->last_member_query_int * ifa->cf->last_member_query_cnt); + tm_start(grp->rxmt_timer, 0); + } + } +} + + +/* + * IGMP groups + */ + +static struct igmp_group * +igmp_find_group(struct igmp_iface *ifa, ip4_addr addr) +{ + return HASH_FIND(ifa->groups, HASH_GRP, addr); +} + +static struct igmp_group * +igmp_get_group(struct igmp_iface *ifa, ip4_addr addr) +{ + struct igmp_proto *p = ifa->proto; + struct igmp_group *grp = igmp_find_group(ifa, addr); + + if (grp) + return grp; + + grp = mb_allocz(p->p.pool, sizeof(struct igmp_group)); + grp->address = addr; + grp->ifa = ifa; + HASH_INSERT(ifa->groups, HASH_GRP, grp); + + grp->join_timer = tm_new_init(p->p.pool, igmp_join_timeout, grp, 0, 0); + grp->rxmt_timer = tm_new_init(p->p.pool, igmp_rxmt_timeout, grp, 0, 0); + grp->v1_host_timer = tm_new_init(p->p.pool, igmp_v1_host_timeout, grp, 0, 0); + + return grp; +} + +static void +igmp_remove_group(struct igmp_group *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_announce_group(struct igmp_group *grp, int up) +{ + struct igmp_proto *p = grp->ifa->proto; + net_addr_mreq4 addr = NET_ADDR_MREQ4(grp->address, grp->ifa->iface->index); + + if (up) + { + rta a0 = { + .src = p->p.main_source, + .source = RTS_IGMP, + .scope = SCOPE_UNIVERSE, + .dest = RTD_NONE, + .nh.iface = grp->ifa->iface, + }; + rta *a = rta_lookup(&a0); + rte *e = rte_get_temp(a); + + e->pflags = 0; + rte_update(&p->p, (net_addr *) &addr, e); + } + else + { + rte_update(&p->p, (net_addr *) &addr, NULL); + } +} + +static void +igmp_group_set_state(struct igmp_group *grp, uint state) +{ + struct igmp_proto *p = grp->ifa->proto; + uint last_state = grp->join_state; + + if (state == last_state) + return; + + TRACE(D_EVENTS, "Group %I4 on %s changed state from %s to %s", + grp->address, grp->ifa->iface->name, + igmp_join_state_names[last_state], igmp_join_state_names[state]); + + grp->join_state = state; +} + + +/* + * IGMP interfaces + */ + +static struct igmp_iface * +igmp_find_iface(struct igmp_proto *p, struct iface *what) +{ + struct igmp_iface *ifa; + + WALK_LIST(ifa, p->iface_list) + if (ifa->iface == what) + return ifa; + + return NULL; +} + + +static void +igmp_iface_locked(struct object_lock *lock) +{ + struct igmp_iface *ifa = lock->data; + struct igmp_proto *p = ifa->proto; + + if (!igmp_open_socket(ifa)) + { + log(L_ERR "%s: Cannot open socket for %s", p->p.name, ifa->iface->name); + return; + } + + igmp_iface_set_state(ifa, IGMP_QS_QUERIER); + ifa->startup_query_cnt = ifa->cf->startup_query_cnt; + tm_start(ifa->query_timer, 0); +} + +static void +igmp_add_iface(struct igmp_proto *p, struct iface *iface, struct igmp_iface_config *ic) +{ + struct igmp_iface *ifa; + + TRACE(D_EVENTS, "Adding interface %s", iface->name); + + ifa = mb_allocz(p->p.pool, sizeof(struct igmp_iface)); + add_tail(&p->iface_list, NODE ifa); + ifa->proto = p; + ifa->iface = iface; + ifa->cf = ic; + + ifa->query_timer = tm_new_init(p->p.pool, igmp_query_timeout, ifa, 0, 0); + ifa->other_present = tm_new_init(p->p.pool, igmp_other_present_timeout, ifa, 0, 0); + + HASH_INIT(ifa->groups, p->p.pool, 8); + + ifa->mif = mif_get(p->mif_group, iface); + if (!ifa->mif) + { + log(L_ERR "%s: Cannot enable multicast on %s, too many MIFs", p->p.name, ifa->iface->name); + return; + } + + struct object_lock *lock = olock_new(p->p.pool); + lock->type = OBJLOCK_IP; + lock->port = IGMP_PROTO; + lock->iface = iface; + lock->data = ifa; + lock->hook = igmp_iface_locked; + ifa->lock = lock; + + olock_acquire(lock); +} + +static void +igmp_remove_iface(struct igmp_proto *p, struct igmp_iface *ifa) +{ + TRACE(D_EVENTS, "Removing interface %s", ifa->iface->name); + + HASH_WALK_DELSAFE(ifa->groups, next, grp) + { + igmp_announce_group(grp, 0); + igmp_remove_group(grp); + } + HASH_WALK_END; + + rem_node(NODE ifa); + + /* This is not a resource */ + if (ifa->mif) + mif_free(p->mif_group, ifa->mif); + + rfree(ifa->sk); + rfree(ifa->lock); + rfree(ifa->query_timer); + rfree(ifa->other_present); + + mb_free(ifa); +} + +static void +igmp_iface_set_state(struct igmp_iface *ifa, uint state) +{ + struct igmp_proto *p = ifa->proto; + uint last_state = ifa->query_state; + + if (state == last_state) + return; + + TRACE(D_EVENTS, "Interface %s changed state from %s to %s", ifa->iface->name, + igmp_query_state_names[last_state], igmp_query_state_names[state]); + + ifa->query_state = state; +} + +static void +igmp_iface_dump(struct igmp_iface *ifa) +{ + debug("\tInterface %s: %s\n", ifa->iface->name, igmp_query_state_names[ifa->query_state]); + + HASH_WALK(ifa->groups, next, grp) + debug("\t\tGroup %I4: %s\n", grp->address, igmp_join_state_names[grp->join_state]); + HASH_WALK_END; +} + +void +igmp_finish_iface_config(struct igmp_iface_config *cf) +{ + if (cf->query_response_int >= cf->query_int) + cf_error("Query response interval must be less than query interval"); + + /* Dependent default values */ + if (!cf->startup_query_int) + cf->startup_query_int = cf->query_int / 4; + + if (!cf->startup_query_cnt) + cf->startup_query_cnt = cf->robustness; + + if (!cf->last_member_query_cnt) + cf->last_member_query_cnt = cf->robustness; + + cf->group_member_int = cf->robustness * cf->query_int + cf->query_response_int; + cf->other_querier_int = cf->robustness * cf->query_int + cf->query_response_int / 2; +} + +static int +igmp_reconfigure_iface(struct igmp_proto *p, struct igmp_iface *ifa, struct igmp_iface_config *new) +{ + struct igmp_iface_config *old = ifa->cf; + + TRACE(D_EVENTS, "Reconfiguring interface %s", ifa->iface->name); + + ifa->cf = new; + + /* We reconfigure just the query timer, as it is periodic */ + if (ifa->query_state == IGMP_QS_QUERIER) + { + if (ifa->startup_query_cnt) + { + if (new->startup_query_cnt != old->startup_query_cnt) + { + int delta = (int) new->startup_query_cnt - (int) old->startup_query_cnt; + ifa->startup_query_cnt = MAX(1, ifa->startup_query_cnt + delta); + } + + if (new->startup_query_int != old->startup_query_int) + tm_shift(ifa->query_timer, new->startup_query_int - old->startup_query_int); + } + else + { + if (new->query_int != old->query_int) + tm_shift(ifa->query_timer, new->query_int - old->query_int); + } + } + + return 1; +} + +static void +igmp_reconfigure_ifaces(struct igmp_proto *p, struct igmp_config *cf) +{ + struct iface *iface; + + WALK_LIST(iface, iface_list) + { + if (!(iface->flags & IF_UP)) + continue; + + /* Ignore non-multicast ifaces */ + if (!(iface->flags & IF_MULTICAST)) + continue; + + /* Ignore ifaces without IPv4 address */ + if (!iface->addr4) + continue; + + struct igmp_iface *ifa = igmp_find_iface(p, iface); + struct igmp_iface_config *ic = (void *) iface_patt_find(&cf->iface_list, iface, NULL); + + if (ifa && ic) + { + if (igmp_reconfigure_iface(p, ifa, ic)) + continue; + + /* Hard restart */ + log(L_INFO "%s: Restarting interface %s", p->p.name, ifa->iface->name); + igmp_remove_iface(p, ifa); + igmp_add_iface(p, iface, ic); + } + + if (ifa && !ic) + igmp_remove_iface(p, ifa); + + if (!ifa && ic) + igmp_add_iface(p, iface, ic); + } +} + + +/* + * IGMP protocol glue + */ + +void +igmp_postconfig(struct proto_config *CF) +{ + // struct igmp_config *cf = (void *) CF; + + /* Define default channel */ + if (EMPTY_LIST(CF->channels)) + channel_config_new(NULL, net_label[NET_MREQ4], NET_MREQ4, CF); +} + +static void +igmp_if_notify(struct proto *P, uint flags, struct iface *iface) +{ + struct igmp_proto *p = (void *) P; + struct igmp_config *cf = (void *) P->cf; + + if (iface->flags & IF_IGNORE) + return; + + if (flags & IF_CHANGE_UP) + { + struct igmp_iface_config *ic = (void *) iface_patt_find(&cf->iface_list, iface, NULL); + + /* Ignore non-multicast ifaces */ + if (!(iface->flags & IF_MULTICAST)) + return; + + /* Ignore ifaces without IPv4 address */ + if (!iface->addr4) + return; + + if (ic) + igmp_add_iface(p, iface, ic); + + return; + } + + struct igmp_iface *ifa = igmp_find_iface(p, iface); + + if (!ifa) + return; + + if (flags & IF_CHANGE_DOWN) + { + igmp_remove_iface(p, ifa); + return; + } +} + +static struct proto * +igmp_init(struct proto_config *CF) +{ + struct igmp_proto *p = proto_new(CF); + + p->p.main_channel = proto_add_channel(&p->p, proto_cf_main_channel(CF)); + + p->p.if_notify = igmp_if_notify; + + p->mif_group = global_mif_group; + + return &p->p; +} + +static int +igmp_start(struct proto *P) +{ + struct igmp_proto *p = (void *) P; + + init_list(&p->iface_list); + p->log_pkt_tbf = (struct tbf){ .rate = 1, .burst = 5 }; + + return PS_UP; +} + +static int +igmp_shutdown(struct proto *P) +{ + struct igmp_proto *p = (void *) P; + struct igmp_iface *ifa; + + WALK_LIST_FIRST(ifa, p->iface_list) + igmp_remove_iface(p, ifa); + + return PS_DOWN; +} + +static int +igmp_reconfigure(struct proto *P, struct proto_config *CF) +{ + struct igmp_proto *p = (void *) P; + struct igmp_config *new = (void *) CF; + + if (!proto_configure_channel(P, &P->main_channel, proto_cf_main_channel(CF))) + return 0; + + TRACE(D_EVENTS, "Reconfiguring"); + + p->p.cf = CF; + igmp_reconfigure_ifaces(p, new); + + return 1; +} + +static void +igmp_dump(struct proto *P) +{ + struct igmp_proto *p = (void *) P; + struct igmp_iface *ifa; + + WALK_LIST(ifa, p->iface_list) + igmp_iface_dump(ifa); +} + + +struct protocol proto_igmp = { + .name = "IGMP", + .template = "igmp%d", + .preference = DEF_PREF_IGMP, + .channel_mask = NB_MREQ4, + .proto_size = sizeof(struct igmp_proto), + .config_size = sizeof(struct igmp_config), + .postconfig = igmp_postconfig, + .init = igmp_init, + .dump = igmp_dump, + .start = igmp_start, + .shutdown = igmp_shutdown, + .reconfigure = igmp_reconfigure, +}; diff --git a/proto/igmp/igmp.h b/proto/igmp/igmp.h new file mode 100644 index 00000000..46ad462e --- /dev/null +++ b/proto/igmp/igmp.h @@ -0,0 +1,110 @@ +/* + * BIRD -- IGMP protocol + * + * (c) 2016 Ondrej Hlavaty + * (c) 2018 Ondrej Zajicek + * (c) 2018 CZ.NIC z.s.p.o. + * + * Can be freely distributed and used under the terms of the GNU GPL. + */ + +#ifndef _BIRD_IGMP_H_ +#define _BIRD_IGMP_H_ + +#include "nest/bird.h" +#include "nest/iface.h" +#include "nest/locks.h" +#include "nest/protocol.h" +#include "nest/route.h" +#include "conf/conf.h" +#include "lib/lists.h" +#include "lib/hash.h" +#include "lib/socket.h" +#include "lib/timer.h" + + +#define IGMP_PROTO 2 + +#define IGMP_DEFAULT_ROBUSTNESS 2 +#define IGMP_DEFAULT_QUERY_INT (125 S_) +#define IGMP_DEFAULT_RESPONSE_INT (10 S_) +#define IGMP_DEFAULT_LAST_MEMBER_INT (1 S_) + + +struct igmp_config +{ + struct proto_config c; + list iface_list; /* List of ifaces (struct igmp_iface_config) */ +}; + +struct igmp_iface_config +{ + struct iface_patt i; + + /* These are configurable */ + uint robustness, startup_query_cnt, last_member_query_cnt; + btime query_int, query_response_int, startup_query_int, last_member_query_int; + + /* These are not */ + btime group_member_int, other_querier_int; +}; + +struct igmp_proto +{ + struct proto p; + list iface_list; /* List of interfaces (struct igmp_iface) */ + struct mif_group *mif_group; /* Associated MIF group for multicast routes */ + + struct tbf log_pkt_tbf; /* TBF for packet messages */ +}; + +struct igmp_iface +{ + node n; /* Member of igmp_proto->iface_list */ + struct igmp_proto *proto; + struct iface *iface; /* Underyling core interface */ + struct mif *mif; /* Associated multicast iface */ + struct igmp_iface_config *cf; /* Related config, must be updated in reconfigure */ + struct object_lock *lock; /* Interface lock */ + sock *sk; /* IGMP socket */ + + HASH(struct igmp_group) groups; + + uint query_state; /* initial / querier / non-querier */ + int startup_query_cnt; /* Remaining startup queries to send */ + timer *query_timer, *other_present; +}; + +struct igmp_group +{ + struct igmp_group *next; /* Member of igmp_iface->groups */ + struct igmp_iface *ifa; + ip4_addr address; + + uint join_state; + timer *join_timer, *v1_host_timer, *rxmt_timer; +}; + + +#define IGMP_JS_NO_MEMBERS 0 +#define IGMP_JS_MEMBERS 1 +#define IGMP_JS_V1_MEMBERS 2 +#define IGMP_JS_CHECKING 3 + +#define IGMP_QS_INIT 0 +#define IGMP_QS_QUERIER 1 +#define IGMP_QS_NONQUERIER 2 + + +/* igmp.c */ +void igmp_handle_query(struct igmp_iface *ifa, ip4_addr addr, ip4_addr from, btime resp_time); +void igmp_handle_report(struct igmp_iface *ifa, ip4_addr addr, int version); +void igmp_handle_leave(struct igmp_iface *ifa, ip4_addr addr); +void igmp_finish_iface_config(struct igmp_iface_config *cf); + +/* packets.c */ +int igmp_open_socket(struct igmp_iface *ifa); +void igmp_send_query(struct igmp_iface *ifa, ip4_addr addr, btime resp_time); + + +#endif diff --git a/proto/igmp/packets.c b/proto/igmp/packets.c new file mode 100644 index 00000000..c387144a --- /dev/null +++ b/proto/igmp/packets.c @@ -0,0 +1,167 @@ +/* + * BIRD -- Internet Group Management Protocol (IGMP) + * + * (c) 2016 Ondrej Hlavaty + * (c) 2018 Ondrej Zajicek + * (c) 2018 CZ.NIC z.s.p.o. + * + * Can be freely distributed and used under the terms of the GNU GPL. + */ + +#include "igmp.h" +#include "lib/checksum.h" + +#include + + +struct igmp_packet +{ + u8 type; + u8 resp_time; + u16 checksum; + u32 addr; +} PACKED; + +#define IGMP_MSG_QUERY 0x11 +#define IGMP_MSG_V1_REPORT 0x12 +#define IGMP_MSG_V2_REPORT 0x16 +#define IGMP_MSG_LEAVE 0x17 + + +#define DROP(DSC,VAL) do { err_dsc = DSC; err_val = VAL; goto drop; } while(0) + +#define LOG_PKT(msg, args...) \ + log_rl(&p->log_pkt_tbf, L_REMOTE "%s: " msg, p->p.name, args) + +int +igmp_rx_hook(sock *sk, uint len) +{ + struct igmp_iface *ifa = sk->data; + struct igmp_proto *p = ifa->proto; + const char *err_dsc = NULL; + uint err_val = 0; + + struct igmp_packet *pkt = (void *) sk_rx_buffer(sk, &len); + + if (pkt == NULL) + DROP("bad IP header", len); + + if (len < sizeof(struct igmp_packet)) + DROP("too short", len); + + if (!ipsum_verify(pkt, len, NULL)) + DROP("invalid checksum", pkt->checksum); + + ip4_addr from = ipa_to_ip4(sk->faddr); + ip4_addr addr = get_ip4(&pkt->addr); + + switch (pkt->type) + { + case IGMP_MSG_QUERY: + TRACE(D_PACKETS, "Query received from %I4 on %s", from, ifa->iface->name); + /* FIXME: Warning if resp_time == 0 */ + igmp_handle_query(ifa, addr, from, pkt->resp_time * (btime) 100000); + break; + + case IGMP_MSG_V1_REPORT: + TRACE(D_PACKETS, "Report (v1) received from %I4 on %s for %I4", from, ifa->iface->name, addr); + igmp_handle_report(ifa, addr, 1); + break; + + case IGMP_MSG_V2_REPORT: + TRACE(D_PACKETS, "Report (v2) received from %I4 on %s for %I4", from, ifa->iface->name, addr); + igmp_handle_report(ifa, addr, 2); + break; + + case IGMP_MSG_LEAVE: + TRACE(D_PACKETS, "Leave received from %I4 on %s for %I4", from, ifa->iface->name, addr); + igmp_handle_leave(ifa, addr); + break; + + default: + TRACE(D_PACKETS, "Unknown IGMP packet (0x%x) from %I4 on %s", pkt->type, from, ifa->iface->name); + break; + } + return 1; + +drop: + LOG_PKT("Bad packet from %I on %s - %s (%u)", + sk->faddr, sk->iface->name, err_dsc, err_val); + + return 1; +} + +void +igmp_err_hook(sock *sk, int err) +{ + struct igmp_iface *ifa = sk->data; + struct igmp_proto *p = ifa->proto; + + log(L_ERR "%s: Socket error on %s: %M", p->p.name, ifa->iface->name, err); +} + +void +igmp_send_query(struct igmp_iface *ifa, ip4_addr addr, btime resp_time) +{ + struct igmp_proto *p = ifa->proto; + struct igmp_packet *pkt = (void *) ifa->sk->tbuf; + + pkt->type = IGMP_MSG_QUERY; + pkt->resp_time = resp_time / 100000; + put_ip4(&pkt->addr, addr); + + pkt->checksum = 0; + pkt->checksum = ipsum_calculate(pkt, sizeof(struct igmp_packet), NULL); + + ifa->sk->daddr = ip4_zero(addr) ? IP4_ALL_NODES : ipa_from_ip4(addr); + + if (ip4_zero(addr)) + TRACE(D_PACKETS, "Sending query on %s", ifa->iface->name); + else + TRACE(D_PACKETS, "Sending query on %s for %I4", ifa->iface->name, addr); + + sk_send(ifa->sk, sizeof(struct igmp_packet)); +} + +int +igmp_open_socket(struct igmp_iface *ifa) +{ + struct igmp_proto *p = ifa->proto; + + sock *sk = sk_new(p->p.pool); + sk->type = SK_IP; + sk->dport = IGMP_PROTO; + sk->saddr = ifa->iface->addr4->ip; + sk->iface = ifa->iface; + + sk->rx_hook = igmp_rx_hook; + sk->err_hook = igmp_err_hook; + sk->data = ifa; + + sk->tbsize = ifa->iface->mtu; + sk->tos = IP_PREC_INTERNET_CONTROL; + sk->ttl = 1; + + if (sk_open(sk) < 0) + goto err; + + if (sk_setup_multicast(sk) < 0) + goto err; + + if (sk_setup_igmp(sk, p->mif_group, ifa->mif) < 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 1; + +err: + log(L_ERR "%s: Socket error: %s%#m", ifa->proto->p.name, sk->err); + rfree(sk); + return 0; +}