/** @file * @brief IPv4 IGMP related functions */ /* * Copyright (c) 2021 Intel Corporation * * SPDX-License-Identifier: Apache-2.0 */ #include LOG_MODULE_DECLARE(net_ipv4, CONFIG_NET_IPV4_LOG_LEVEL); #include #include #include #include #include #include #include #include "net_private.h" #include "connection.h" #include "ipv4.h" #include "net_stats.h" /* Timeout for various buffer allocations in this file. */ #define PKT_WAIT_TIME K_MSEC(50) #define IPV4_OPT_HDR_ROUTER_ALERT_LEN 4 #define IGMPV3_MODE_IS_INCLUDE 0x01 #define IGMPV3_MODE_IS_EXCLUDE 0x02 #define IGMPV3_CHANGE_TO_INCLUDE_MODE 0x03 #define IGMPV3_CHANGE_TO_EXCLUDE_MODE 0x04 #define IGMPV3_ALLOW_NEW_SOURCES 0x05 #define IGMPV3_BLOCK_OLD_SOURCES 0x06 static const struct in_addr all_systems = { { { 224, 0, 0, 1 } } }; #if defined(CONFIG_NET_IPV4_IGMPV3) static const struct in_addr igmp_multicast_addr = { { { 224, 0, 0, 22 } } }; #else static const struct in_addr all_routers = { { { 224, 0, 0, 2 } } }; #endif #define dbg_addr(action, pkt_str, src, dst) \ NET_DBG("%s %s from %s to %s", action, pkt_str, \ net_sprint_ipv4_addr(src), \ net_sprint_ipv4_addr(dst)); #define dbg_addr_recv(pkt_str, src, dst) \ dbg_addr("Received", pkt_str, src, dst) enum igmp_version { IGMPV1, IGMPV2, IGMPV3, }; static int igmp_v2_create(struct net_pkt *pkt, const struct in_addr *addr, uint8_t type) { NET_PKT_DATA_ACCESS_DEFINE(igmp_access, struct net_ipv4_igmp_v2_report); struct net_ipv4_igmp_v2_report *igmp; igmp = (struct net_ipv4_igmp_v2_report *) net_pkt_get_data(pkt, &igmp_access); if (!igmp) { return -ENOBUFS; } igmp->type = type; igmp->max_rsp = 0U; net_ipaddr_copy(&igmp->address, addr); igmp->chksum = 0; if (net_pkt_set_data(pkt, &igmp_access)) { return -ENOBUFS; } igmp->chksum = net_calc_chksum_igmp(pkt); net_pkt_set_overwrite(pkt, true); net_pkt_cursor_init(pkt); net_pkt_skip(pkt, offsetof(struct net_ipv4_igmp_v2_report, chksum)); if (net_pkt_write(pkt, &igmp->chksum, sizeof(igmp->chksum))) { return -ENOBUFS; } return 0; } #if defined(CONFIG_NET_IPV4_IGMPV3) static int igmp_v3_create(struct net_pkt *pkt, uint8_t type, struct net_if_mcast_addr mcast[], size_t mcast_len) { NET_PKT_DATA_ACCESS_DEFINE(igmp_access, struct net_ipv4_igmp_v3_report); NET_PKT_DATA_ACCESS_DEFINE(group_record_access, struct net_ipv4_igmp_v3_group_record); struct net_ipv4_igmp_v3_report *igmp; struct net_ipv4_igmp_v3_group_record *group_record; uint16_t group_count = 0; igmp = (struct net_ipv4_igmp_v3_report *)net_pkt_get_data(pkt, &igmp_access); if (!igmp) { return -ENOBUFS; } for (int i = 0; i < mcast_len; i++) { /* We don't need to send an IGMP membership report to the IGMP * all systems multicast address of 224.0.0.1 so skip over it. * Since the IGMP all systems multicast address is marked as * used and joined during init time, we have to check this * address separately to skip over it. */ if (!mcast[i].is_used || !mcast[i].is_joined || net_ipv4_addr_cmp_raw((uint8_t *)&mcast[i].address.in_addr, (uint8_t *)&all_systems)) { continue; } group_count++; } igmp->type = type; igmp->reserved_1 = 0U; igmp->reserved_2 = 0U; igmp->groups_len = htons(group_count); /* Setting initial value of chksum to 0 to calculate chksum as described in RFC 3376 * ch 4.1.2 */ igmp->chksum = 0; if (net_pkt_set_data(pkt, &igmp_access)) { return -ENOBUFS; } for (int i = 0; i < mcast_len; i++) { /* We don't need to send an IGMP membership report to the IGMP * all systems multicast address of 224.0.0.1 so skip over it. * Since the IGMP all systems multicast address is marked as * used and joined during init time, we have to check this * address separately to skip over it. */ if (!mcast[i].is_used || !mcast[i].is_joined || net_ipv4_addr_cmp_raw((uint8_t *)&mcast[i].address.in_addr, (uint8_t *)&all_systems)) { continue; } group_record = (struct net_ipv4_igmp_v3_group_record *)net_pkt_get_data( pkt, &group_record_access); if (!group_record) { return -ENOBUFS; } group_record->type = mcast[i].record_type; group_record->aux_len = 0U; net_ipaddr_copy(&group_record->address, &mcast[i].address.in_addr); group_record->sources_len = htons(mcast[i].sources_len); if (net_pkt_set_data(pkt, &group_record_access)) { return -ENOBUFS; } for (int j = 0; j < mcast[i].sources_len; j++) { if (net_pkt_write(pkt, &mcast[i].sources[j].in_addr.s_addr, sizeof(mcast[i].sources[j].in_addr.s_addr))) { return -ENOBUFS; } } } igmp->chksum = net_calc_chksum_igmp(pkt); net_pkt_set_overwrite(pkt, true); net_pkt_cursor_init(pkt); net_pkt_skip(pkt, offsetof(struct net_ipv4_igmp_v3_report, chksum)); if (net_pkt_write(pkt, &igmp->chksum, sizeof(igmp->chksum))) { return -ENOBUFS; } return 0; } #endif static int igmp_v2_create_packet(struct net_pkt *pkt, const struct in_addr *dst, const struct in_addr *group, uint8_t type) { const uint32_t router_alert = 0x94040000; /* RFC 2213 ch 2.1 */ int ret; /* TTL set to 1, RFC 3376 ch 2 */ net_pkt_set_ipv4_ttl(pkt, 1U); ret = net_ipv4_create_full(pkt, net_if_ipv4_select_src_addr( net_pkt_iface(pkt), dst), dst, 0U, 0U, 0U, 0U); if (ret) { return -ENOBUFS; } /* Add router alert option, RFC 3376 ch 2 */ if (net_pkt_write_be32(pkt, router_alert)) { return -ENOBUFS; } net_pkt_set_ipv4_opts_len(pkt, IPV4_OPT_HDR_ROUTER_ALERT_LEN); return igmp_v2_create(pkt, group, type); } #if defined(CONFIG_NET_IPV4_IGMPV3) static int igmp_v3_create_packet(struct net_pkt *pkt, const struct in_addr *dst, struct net_if_mcast_addr mcast[], size_t mcast_len, uint8_t type) { const uint32_t router_alert = 0x94040000; /* RFC 2213 ch 2.1 */ int ret; /* TTL set to 1, RFC 3376 ch 2 */ net_pkt_set_ipv4_ttl(pkt, 1U); ret = net_ipv4_create_full(pkt, net_if_ipv4_select_src_addr(net_pkt_iface(pkt), dst), dst, 0U, 0U, 0U, 0U); if (ret) { return -ENOBUFS; } /* Add router alert option, RFC 3376 ch 2 */ if (net_pkt_write_be32(pkt, router_alert)) { return -ENOBUFS; } net_pkt_set_ipv4_opts_len(pkt, IPV4_OPT_HDR_ROUTER_ALERT_LEN); return igmp_v3_create(pkt, type, mcast, mcast_len); } #endif static int igmp_send(struct net_pkt *pkt) { int ret; net_pkt_cursor_init(pkt); net_ipv4_finalize(pkt, IPPROTO_IGMP); ret = net_send_data(pkt); if (ret < 0) { net_stats_update_ipv4_igmp_drop(net_pkt_iface(pkt)); return ret; } net_stats_update_ipv4_igmp_sent(net_pkt_iface(pkt)); return 0; } static int send_igmp_report(struct net_if *iface, struct net_ipv4_igmp_v2_query *igmp_v2_hdr) { struct net_if_ipv4 *ipv4 = iface->config.ip.ipv4; struct net_pkt *pkt = NULL; int i, count = 0; int ret = 0; if (!ipv4) { return -ENOENT; } for (i = 0; i < NET_IF_MAX_IPV4_MADDR; i++) { /* We don't need to send an IGMP membership report to the IGMP * all systems multicast address of 224.0.0.1 so skip over it. * Since the IGMP all systems multicast address is marked as * used and joined during init time, we have to check this * address separately to skip over it. */ if (!ipv4->mcast[i].is_used || !ipv4->mcast[i].is_joined || net_ipv4_addr_cmp_raw((uint8_t *)&ipv4->mcast[i].address.in_addr, (uint8_t *)&all_systems)) { continue; } count++; } if (count == 0) { return -ESRCH; } for (i = 0; i < NET_IF_MAX_IPV4_MADDR; i++) { /* We don't need to send an IGMP membership report to the IGMP * all systems multicast address of 224.0.0.1 so skip over it. * Since the IGMP all systems multicast address is marked as * used and joined during init time, we have to check this * address separately to skip over it. */ if (!ipv4->mcast[i].is_used || !ipv4->mcast[i].is_joined || net_ipv4_addr_cmp_raw((uint8_t *)&ipv4->mcast[i].address.in_addr, (uint8_t *)&all_systems)) { continue; } pkt = net_pkt_alloc_with_buffer(iface, IPV4_OPT_HDR_ROUTER_ALERT_LEN + sizeof(struct net_ipv4_igmp_v2_report), AF_INET, IPPROTO_IGMP, PKT_WAIT_TIME); if (!pkt) { return -ENOMEM; } /* Send the IGMP V2 membership report to the group multicast * address, as per RFC 2236 Section 9. */ ret = igmp_v2_create_packet(pkt, &ipv4->mcast[i].address.in_addr, &ipv4->mcast[i].address.in_addr, NET_IPV4_IGMP_REPORT_V2); if (ret < 0) { goto drop; } ret = igmp_send(pkt); if (ret < 0) { goto drop; } /* So that we do not free the data while it is being sent */ pkt = NULL; } drop: if (pkt) { net_pkt_unref(pkt); } return ret; } #if defined(CONFIG_NET_IPV4_IGMPV3) static int send_igmp_v3_report(struct net_if *iface, struct net_ipv4_igmp_v3_query *igmp_v3_hdr) { struct net_if_ipv4 *ipv4 = iface->config.ip.ipv4; struct net_pkt *pkt = NULL; int i, group_count = 0, source_count = 0; int ret = 0; if (!ipv4) { return -ENOENT; } for (i = 0; i < NET_IF_MAX_IPV4_MADDR; i++) { /* We don't need to send an IGMP membership report to the IGMP * all systems multicast address of 224.0.0.1 so skip over it. * Since the IGMP all systems multicast address is marked as * used and joined during init time, we have to check this * address separately to skip over it. */ if (!ipv4->mcast[i].is_used || !ipv4->mcast[i].is_joined || net_ipv4_addr_cmp_raw((uint8_t *)&ipv4->mcast[i].address.in_addr, (uint8_t *)&all_systems)) { continue; } group_count++; source_count += ipv4->mcast[i].sources_len; } if (group_count == 0) { return -ESRCH; } pkt = net_pkt_alloc_with_buffer( iface, IPV4_OPT_HDR_ROUTER_ALERT_LEN + sizeof(struct net_ipv4_igmp_v3_report) + sizeof(struct net_ipv4_igmp_v3_group_record) * group_count + sizeof(struct in_addr) * source_count, AF_INET, IPPROTO_IGMP, PKT_WAIT_TIME); if (!pkt) { return -ENOMEM; } /* Send the IGMP V3 membership report to the igmp multicast * address, as per RFC 3376 Section 4.2.14. */ ret = igmp_v3_create_packet(pkt, &igmp_multicast_addr, ipv4->mcast, NET_IF_MAX_IPV4_MADDR, NET_IPV4_IGMP_REPORT_V3); if (ret < 0) { goto drop; } ret = igmp_send(pkt); if (ret < 0) { goto drop; } /* So that we do not free the data while it is being sent */ pkt = NULL; drop: if (pkt) { net_pkt_unref(pkt); } return ret; } #endif enum net_verdict net_ipv4_igmp_input(struct net_pkt *pkt, struct net_ipv4_hdr *ip_hdr) { #if defined(CONFIG_NET_IPV4_IGMPV3) NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE(igmpv3_access, struct net_ipv4_igmp_v3_query); struct net_ipv4_igmp_v3_query *igmpv3_hdr; #endif NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE(igmpv2_access, struct net_ipv4_igmp_v2_query); struct net_ipv4_igmp_v2_query *igmpv2_hdr; enum igmp_version version; int ret; int igmp_buf_len = pkt->buffer->len - net_pkt_ip_hdr_len(pkt); /* Detect IGMP type (RFC 3376 ch 7.1) */ if (igmp_buf_len == 8) { /* IGMPv1/2 detected */ version = IGMPV2; } else if (igmp_buf_len >= 12) { /* IGMPv3 detected */ version = IGMPV3; #if !defined(CONFIG_NET_IPV4_IGMPV3) NET_DBG("DROP: %sv3 msg received but %s support is disabled", "IGMP", "IGMP"); return NET_DROP; #endif } else { NET_DBG("DROP: unsupported payload length"); return NET_DROP; } if (!net_ipv4_addr_cmp_raw(ip_hdr->dst, (uint8_t *)&all_systems)) { NET_DBG("DROP: Invalid dst address"); return NET_DROP; } #if defined(CONFIG_NET_IPV4_IGMPV3) if (version == IGMPV3) { igmpv3_hdr = (struct net_ipv4_igmp_v3_query *)net_pkt_get_data(pkt, &igmpv3_access); if (!igmpv3_hdr) { NET_DBG("DROP: NULL %sv3 header", "IGMP"); return NET_DROP; } } else { #endif igmpv2_hdr = (struct net_ipv4_igmp_v2_query *)net_pkt_get_data(pkt, &igmpv2_access); if (!igmpv2_hdr) { NET_DBG("DROP: NULL %s header", "IGMP"); return NET_DROP; } #if defined(CONFIG_NET_IPV4_IGMPV3) } #endif ret = net_calc_chksum_igmp(pkt); if (ret != 0u) { NET_DBG("DROP: Invalid checksum"); goto drop; } #if defined(CONFIG_NET_IPV4_IGMPV3) if (version == IGMPV3) { net_pkt_acknowledge_data(pkt, &igmpv3_access); } else { #endif net_pkt_acknowledge_data(pkt, &igmpv2_access); #if defined(CONFIG_NET_IPV4_IGMPV3) } #endif dbg_addr_recv("Internet Group Management Protocol", &ip_hdr->src, &ip_hdr->dst); net_stats_update_ipv4_igmp_recv(net_pkt_iface(pkt)); #if defined(CONFIG_NET_IPV4_IGMPV3) if (version == IGMPV3) { (void)send_igmp_v3_report(net_pkt_iface(pkt), igmpv3_hdr); } else { #endif (void)send_igmp_report(net_pkt_iface(pkt), igmpv2_hdr); #if defined(CONFIG_NET_IPV4_IGMPV3) } #endif net_pkt_unref(pkt); return NET_OK; drop: net_stats_update_ipv4_igmp_drop(net_pkt_iface(pkt)); return NET_DROP; } #if !defined(CONFIG_NET_IPV4_IGMPV3) static int igmp_send_generic(struct net_if *iface, const struct in_addr *addr, bool join) { struct net_pkt *pkt; int ret; pkt = net_pkt_alloc_with_buffer(iface, IPV4_OPT_HDR_ROUTER_ALERT_LEN + sizeof(struct net_ipv4_igmp_v2_report), AF_INET, IPPROTO_IGMP, PKT_WAIT_TIME); if (!pkt) { return -ENOMEM; } /* Send the IGMP V2 membership report to the group multicast * address, as per RFC 2236 Section 9. The leave report * should be sent to the ALL ROUTERS multicast address (224.0.0.2) */ ret = igmp_v2_create_packet(pkt, join ? addr : &all_routers, addr, join ? NET_IPV4_IGMP_REPORT_V2 : NET_IPV4_IGMP_LEAVE); if (ret < 0) { goto drop; } ret = igmp_send(pkt); if (ret < 0) { goto drop; } return 0; drop: net_pkt_unref(pkt); return ret; } #endif #if defined(CONFIG_NET_IPV4_IGMPV3) static int igmpv3_send_generic(struct net_if *iface, struct net_if_mcast_addr *mcast) { struct net_pkt *pkt; int ret; pkt = net_pkt_alloc_with_buffer(iface, IPV4_OPT_HDR_ROUTER_ALERT_LEN + sizeof(struct net_ipv4_igmp_v3_report) + sizeof(struct net_ipv4_igmp_v3_group_record) + sizeof(struct in_addr) * mcast->sources_len, AF_INET, IPPROTO_IGMP, PKT_WAIT_TIME); if (!pkt) { return -ENOMEM; } ret = igmp_v3_create_packet(pkt, &igmp_multicast_addr, mcast, 1, NET_IPV4_IGMP_REPORT_V3); if (ret < 0) { goto drop; } ret = igmp_send(pkt); if (ret < 0) { goto drop; } return 0; drop: net_pkt_unref(pkt); return ret; } #endif int net_ipv4_igmp_join(struct net_if *iface, const struct in_addr *addr, const struct igmp_param *param) { struct net_if_mcast_addr *maddr; int ret; #if defined(CONFIG_NET_IPV4_IGMPV3) if (param != NULL) { if (param->sources_len > CONFIG_NET_IF_MCAST_IPV4_SOURCE_COUNT) { return -ENOMEM; } } #endif maddr = net_if_ipv4_maddr_lookup(addr, &iface); if (maddr && net_if_ipv4_maddr_is_joined(maddr)) { return -EALREADY; } if (!maddr) { maddr = net_if_ipv4_maddr_add(iface, addr); if (!maddr) { return -ENOMEM; } } #if defined(CONFIG_NET_IPV4_IGMPV3) if (param != NULL) { maddr->record_type = param->include ? IGMPV3_CHANGE_TO_INCLUDE_MODE : IGMPV3_CHANGE_TO_EXCLUDE_MODE; maddr->sources_len = param->sources_len; for (int i = 0; i < param->sources_len; i++) { net_ipaddr_copy(&maddr->sources[i].in_addr.s_addr, ¶m->source_list[i].s_addr); } } else { maddr->record_type = IGMPV3_CHANGE_TO_EXCLUDE_MODE; } #endif net_if_ipv4_maddr_join(iface, maddr); #if defined(CONFIG_NET_IPV4_IGMPV3) ret = igmpv3_send_generic(iface, maddr); #else ret = igmp_send_generic(iface, addr, true); #endif if (ret < 0) { net_if_ipv4_maddr_leave(iface, maddr); return ret; } #if defined(CONFIG_NET_IPV4_IGMPV3) if (param != NULL) { /* Updating the record type for further use after sending the join report */ maddr->record_type = param->include ? IGMPV3_MODE_IS_INCLUDE : IGMPV3_MODE_IS_EXCLUDE; } #endif net_if_mcast_monitor(iface, &maddr->address, true); net_mgmt_event_notify_with_info(NET_EVENT_IPV4_MCAST_JOIN, iface, &maddr->address.in_addr, sizeof(struct in_addr)); return ret; } int net_ipv4_igmp_leave(struct net_if *iface, const struct in_addr *addr) { struct net_if_mcast_addr *maddr; int ret; maddr = net_if_ipv4_maddr_lookup(addr, &iface); if (!maddr) { return -ENOENT; } #if defined(CONFIG_NET_IPV4_IGMPV3) maddr->record_type = IGMPV3_CHANGE_TO_INCLUDE_MODE; maddr->sources_len = 0; ret = igmpv3_send_generic(iface, maddr); #else ret = igmp_send_generic(iface, addr, false); #endif if (ret < 0) { return ret; } if (!net_if_ipv4_maddr_rm(iface, addr)) { return -EINVAL; } net_if_ipv4_maddr_leave(iface, maddr); net_if_mcast_monitor(iface, &maddr->address, false); net_mgmt_event_notify_with_info(NET_EVENT_IPV4_MCAST_LEAVE, iface, &maddr->address.in_addr, sizeof(struct in_addr)); return ret; } void net_ipv4_igmp_init(struct net_if *iface) { struct net_if_mcast_addr *maddr; /* Ensure multicast addresses are available */ if (CONFIG_NET_IF_MCAST_IPV4_ADDR_COUNT < 1) { return; } /* This code adds the IGMP all systems 224.0.0.1 multicast address * to the list of multicast addresses of the given interface. * The address is marked as joined. However, an IGMP membership * report is not generated for this address. Populating this * address in the list of multicast addresses of the interface * and marking it as joined is helpful for multicast hash filter * implementations that need a list of multicast addresses it needs * to add to the multicast hash filter after a multicast address * has been removed from the membership list. */ maddr = net_if_ipv4_maddr_lookup(&all_systems, &iface); if (maddr && net_if_ipv4_maddr_is_joined(maddr)) { return; } if (!maddr) { maddr = net_if_ipv4_maddr_add(iface, &all_systems); if (!maddr) { return; } } net_if_ipv4_maddr_join(iface, maddr); net_if_mcast_monitor(iface, &maddr->address, true); }