zephyr/subsys/net/ip/igmp.c
Robert Lubos e0a1e910ac net: ip: igmp: Ensure IGMP APIs work with offloaded interfaces
IGMP APIs are commonly used across the codebase to configure IPv4
multicast addresses on network interfaces. Sending IGMP reports however
works only for native interfaces as it uses low-level APIs. Therefore,
in order to make the APIs at least semi-functional for offloaded
interfaces as well (i.e. allow to configure multicast address on
the interface), return early in case interface is offloaded.

Signed-off-by: Robert Lubos <robert.lubos@nordicsemi.no>
2025-05-09 18:00:14 +02:00

731 lines
18 KiB
C

/** @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"
#include "igmp.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 IGMPV2_PAYLOAD_MIN_LEN 8
#define IGMPV3_PAYLOAD_MIN_LEN 12
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)
{
int ret;
NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE(igmpv2_access, struct net_ipv4_igmp_v2_query);
struct net_ipv4_igmp_v2_query *igmpv2_hdr;
int igmp_buf_len =
pkt->buffer->len - (net_pkt_ip_hdr_len(pkt) + net_pkt_ipv4_opts_len(pkt));
#if defined(CONFIG_NET_IPV4_IGMPV3)
struct net_ipv4_igmp_v3_query *igmpv3_hdr;
enum igmp_version version;
NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE(igmpv3_access, struct net_ipv4_igmp_v3_query);
/* Detect IGMP type (RFC 3376 ch 7.1) */
if (igmp_buf_len == IGMPV2_PAYLOAD_MIN_LEN) {
/* IGMPv1/2 detected */
version = IGMPV2;
} else if (igmp_buf_len >= IGMPV3_PAYLOAD_MIN_LEN) {
/* IGMPv3 detected */
version = IGMPV3;
} else {
#else
if (igmp_buf_len < IGMPV2_PAYLOAD_MIN_LEN) {
#endif
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 = 0;
#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 (net_if_is_offloaded(iface)) {
goto out;
}
#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
out:
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 = 0;
maddr = net_if_ipv4_maddr_lookup(addr, &iface);
if (!maddr) {
return -ENOENT;
}
if (net_if_is_offloaded(iface)) {
goto out;
}
#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;
}
out:
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);
}