blob: f4be802f968d15c287032881a52684b505ae694c [file] [log] [blame]
/** @file
* @brief IPv4 IGMP related functions
*/
/*
* Copyright (c) 2021 Intel Corporation
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(net_ipv4, CONFIG_NET_IPV4_LOG_LEVEL);
#include <errno.h>
#include <zephyr/net/net_core.h>
#include <zephyr/net/net_pkt.h>
#include <zephyr/net/net_stats.h>
#include <zephyr/net/net_context.h>
#include <zephyr/net/net_mgmt.h>
#include <zephyr/net/igmp.h>
#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,
&param->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);
}