Repository: zephyrproject-rtos/zephyr
Pull request #87167: Add support for IP_MULTICAST_LOOP socket option
Author: login: Abd002
Labels: name: area: Networking
name: area: Sockets
description: Networking sockets
Created: 2025-03-16T13:04:20Z | Merged: 2025-03-24T08:54:05Z

net: sockets: Add support for IP_MULTICAST_LOOP socket option

This PR adds support for the IP_MULTICAST_LOOP socket option,
which controls whether multicast packets are looped back to the
sending host.

Fixes: #86977
Signed-off-by: RocketBoy


Diff:
diff --git a/include/zephyr/net/net_context.h b/include/zephyr/net/net_context.h
index f1e5bdbb4bb89..e43113a3ab395 100644
--- a/include/zephyr/net/net_context.h
+++ b/include/zephyr/net/net_context.h
@@ -394,6 +394,7 @@ __net_socket struct net_context {
 		/** Flag to enable/disable multicast loop */
 		union {
 			bool ipv6_mcast_loop;  /**< IPv6 multicast loop */
+			bool ipv4_mcast_loop;  /**< IPv4 multicast loop */
 		};
 #endif /* CONFIG_NET_IPV6 || CONFIG_NET_IPV4 */
 
@@ -843,6 +844,38 @@ static inline void net_context_set_ipv4_mcast_ttl(struct net_context *context,
 	context->ipv4_mcast_ttl = ttl;
 }
 
+#if defined(CONFIG_NET_IPV4)
+/**
+ * @brief Get IPv4 multicast loop value for this context.
+ *
+ * @details This function returns the IPv4 multicast loop value
+ *	    that is set to this context.
+ *
+ * @param context Network context.
+ *
+ * @return IPv4 multicast loop value
+ */
+static inline bool net_context_get_ipv4_mcast_loop(struct net_context *context)
+{
+	return context->options.ipv4_mcast_loop;
+}
+
+/**
+ * @brief Set IPv4 multicast loop value for this context.
+ *
+ * @details This function sets the IPv4 multicast loop value for
+ *	    this context.
+ *
+ * @param context Network context.
+ * @param ipv4_mcast_loop IPv4 multicast loop value.
+ */
+static inline void net_context_set_ipv4_mcast_loop(struct net_context *context,
+						   bool ipv4_mcast_loop)
+{
+	context->options.ipv4_mcast_loop = ipv4_mcast_loop;
+}
+#endif
+
 /**
  * @brief Get IPv6 hop limit value for this context.
  *
@@ -1365,6 +1398,7 @@ enum net_context_option {
 	NET_OPT_MTU               = 20, /**< IPv4 socket path MTU */
 	NET_OPT_LOCAL_PORT_RANGE  = 21, /**< Clamp local port range */
 	NET_OPT_IPV6_MCAST_LOOP	  = 22, /**< IPV6 multicast loop */
+	NET_OPT_IPV4_MCAST_LOOP	  = 23, /**< IPV4 multicast loop */
 };
 
 /**
diff --git a/include/zephyr/net/socket.h b/include/zephyr/net/socket.h
index 69e15776daadb..52ef479580dd0 100644
--- a/include/zephyr/net/socket.h
+++ b/include/zephyr/net/socket.h
@@ -965,6 +965,8 @@ struct in_pktinfo {
 #define IP_MULTICAST_IF 32
 /** Set IPv4 multicast TTL value. */
 #define IP_MULTICAST_TTL 33
+/** Set IPv4 multicast loop value. */
+#define IP_MULTICAST_LOOP 34
 /** Join IPv4 multicast group. */
 #define IP_ADD_MEMBERSHIP 35
 /** Leave IPv4 multicast group. */
diff --git a/subsys/net/ip/Kconfig.ipv4 b/subsys/net/ip/Kconfig.ipv4
index 2246a7e2592a9..842a02edd7b5e 100644
--- a/subsys/net/ip/Kconfig.ipv4
+++ b/subsys/net/ip/Kconfig.ipv4
@@ -73,6 +73,13 @@ config NET_INITIAL_MCAST_TTL
 	  don't leave the local network unless the application explicitly
 	  requests it.
 
+config NET_INITIAL_IPV4_MCAST_LOOP
+	bool "Control whether the socket sees multicast packets sent by itself"
+	default y
+	help
+	Assign initial value to IPV4_MULTICAST_LOOP in socket options,
+	if not set by the user using setsockopt().
+
 config NET_IF_MCAST_IPV4_SOURCE_COUNT
 	int "Max number of IPv4 sources per mcast address to be included or excluded"
 	default 1
diff --git a/subsys/net/ip/net_context.c b/subsys/net/ip/net_context.c
index c332c9a879c27..f289ff97239ba 100644
--- a/subsys/net/ip/net_context.c
+++ b/subsys/net/ip/net_context.c
@@ -566,6 +566,10 @@ int net_context_get(sa_family_t family, enum net_sock_type type, uint16_t proto,
 #if defined(CONFIG_NET_IPV4_MAPPING_TO_IPV6)
 		/* By default IPv4 and IPv6 are in different port spaces */
 		contexts[i].options.ipv6_v6only = true;
+#endif
+#if defined(CONFIG_NET_IPV4)
+		contexts[i].options.ipv4_mcast_loop =
+			IS_ENABLED(CONFIG_NET_INITIAL_IPV4_MCAST_LOOP);
 #endif
 		if (IS_ENABLED(CONFIG_NET_IP)) {
 			(void)memset(&contexts[i].remote, 0, sizeof(struct sockaddr));
@@ -1783,6 +1787,19 @@ static int get_context_mcast_ttl(struct net_context *context,
 #endif
 }
 
+static int get_context_ipv4_mcast_loop(struct net_context *context,
+				  void *value, size_t *len)
+{
+#if defined(CONFIG_NET_IPV4)
+	return get_bool_option(context->options.ipv4_mcast_loop, value, len);
+#else
+	ARG_UNUSED(context);
+	ARG_UNUSED(value);
+	ARG_UNUSED(len);
+	return -ENOTSUP;
+#endif
+}
+
 static int get_context_mcast_hop_limit(struct net_context *context,
 				       void *value, size_t *len)
 {
@@ -3293,6 +3310,20 @@ static int set_context_mcast_ttl(struct net_context *context,
 #endif
 }
 
+static int set_context_ipv4_mcast_loop(struct net_context *context,
+				       const void *value, size_t len)
+{
+#if defined(CONFIG_NET_IPV4)
+	return set_bool_option(&context->options.ipv4_mcast_loop, value, len);
+#else
+	ARG_UNUSED(context);
+	ARG_UNUSED(value);
+	ARG_UNUSED(len);
+
+	return -ENOTSUP;
+#endif
+}
+
 static int set_context_mcast_hop_limit(struct net_context *context,
 				       const void *value, size_t len)
 {
@@ -3688,6 +3719,9 @@ int net_context_set_option(struct net_context *context,
 	case NET_OPT_IPV6_MCAST_LOOP:
 		ret = set_context_ipv6_mcast_loop(context, value, len);
 		break;
+	case NET_OPT_IPV4_MCAST_LOOP:
+		ret = set_context_ipv4_mcast_loop(context, value, len);
+		break;
 	}
 
 	k_mutex_unlock(&context->lock);
@@ -3776,6 +3810,9 @@ int net_context_get_option(struct net_context *context,
 	case NET_OPT_IPV6_MCAST_LOOP:
 		ret = get_context_ipv6_mcast_loop(context, value, len);
 		break;
+	case NET_OPT_IPV4_MCAST_LOOP:
+		ret = get_context_ipv4_mcast_loop(context, value, len);
+		break;
 	}
 
 	k_mutex_unlock(&context->lock);
diff --git a/subsys/net/ip/net_core.c b/subsys/net/ip/net_core.c
index 3d657e378e933..107924e60d402 100644
--- a/subsys/net/ip/net_core.c
+++ b/subsys/net/ip/net_core.c
@@ -366,6 +366,34 @@ static inline int check_ip(struct net_pkt *pkt)
 	return ret;
 }
 
+#if defined(CONFIG_NET_IPV4) || defined(CONFIG_NET_IPV6)
+static inline bool process_multicast(struct net_pkt *pkt)
+{
+	struct net_context *ctx = net_pkt_context(pkt);
+	sa_family_t family = net_pkt_family(pkt);
+
+	if (ctx == NULL) {
+		return false;
+	}
+
+#if defined(CONFIG_NET_IPV4)
+	if (family == AF_INET) {
+		const struct in_addr *dst = (const struct in_addr *)&NET_IPV4_HDR(pkt)->dst;
+
+		return net_ipv4_is_addr_mcast(dst) && net_context_get_ipv4_mcast_loop(ctx);
+	}
+#endif
+#if defined(CONFIG_NET_IPV6)
+	if (family == AF_INET6) {
+		const struct in6_addr *dst = (const struct in6_addr *)&NET_IPV6_HDR(pkt)->dst;
+
+		return net_ipv6_is_addr_mcast(dst) && net_context_get_ipv6_mcast_loop(ctx);
+	}
+#endif
+	return false;
+}
+#endif
+
 int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 {
 	int status;
@@ -410,26 +438,31 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4) || defined(CONFIG_NET_IPV6)
+	if (process_multicast(pkt)) {
+		struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+		if (clone != NULL) {
+			net_pkt_set_iface(clone, net_pkt_iface(pkt));
+			if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+				if (IS_ENABLED(CONFIG_NET_STATISTICS)) {
+					switch (net_pkt_family(pkt)) {
+#if defined(CONFIG_NET_IPV4)
+					case AF_INET:
+						net_stats_update_ipv4_sent(net_pkt_iface(pkt));
+						break;
+#endif
 #if defined(CONFIG_NET_IPV6)
-	if (net_pkt_family(pkt) == AF_INET6) {
-		const struct in6_addr *dest = (const struct in6_addr *)&NET_IPV6_HDR(pkt)->dst;
-		struct net_context *ctx = net_pkt_context(pkt);
-
-		if (net_ipv6_is_addr_mcast(dest) && ctx != NULL &&
-		    net_context_get_ipv6_mcast_loop(ctx)) {
-			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
-
-			if (clone != NULL) {
-				net_pkt_set_iface(clone, net_pkt_iface(pkt));
-				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
-					if (IS_ENABLED(CONFIG_NET_STATISTICS)) {
-						net_stats_update_ipv6_drop(net_pkt_iface(pkt));
+					case AF_INET6:
+						net_stats_update_ipv6_sent(net_pkt_iface(pkt));
+						break;
+#endif
 					}
-					net_pkt_unref(clone);
 				}
-			} else {
-				NET_DBG("Failed to clone multicast packet");
+				net_pkt_unref(clone);
 			}
+		} else {
+			NET_DBG("Failed to clone multicast packet");
 		}
 	}
 #endif
diff --git a/subsys/net/lib/sockets/sockets_inet.c b/subsys/net/lib/sockets/sockets_inet.c
index bf9d63f997e65..b115ede5420ba 100644
--- a/subsys/net/lib/sockets/sockets_inet.c
+++ b/subsys/net/lib/sockets/sockets_inet.c
@@ -1974,6 +1974,18 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			ret = net_context_get_option(ctx,
+						     NET_OPT_IPV4_MCAST_LOOP,
+						     optval, optlen);
+			if (ret < 0) {
+				errno  = -ret;
+				return -1;
+			}
+
+			return 0;
+#endif
 		}
 
 		break;
@@ -2604,6 +2616,18 @@ int zsock_setsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			ret = net_context_set_option(ctx,
+						     NET_OPT_IPV4_MCAST_LOOP,
+						     optval, optlen);
+			if (ret < 0) {
+				errno  = -ret;
+				return -1;
+			}
+
+			return 0;
+#endif
 		}
 
 		break;


Reviews:
user: login: pdgendt
body: As Carles suggested, please read the contribution guidelines. I'd expect only 2 commits:

- Introduction of the `IP_MULTICAST_LOOP` definition
- Implementation for `net_context`
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T07:27:25Z
user: login: jukkar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T07:43:34Z
user: login: sayoojkkarun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T08:15:14Z
user: login: sayoojkkarun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T09:09:50Z
user: login: sayoojkkarun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T10:00:16Z
user: login: sayoojkkarun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T10:15:40Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T10:25:22Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T10:27:39Z
user: login: jukkar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T13:02:22Z
user: login: sayoojkkarun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T13:41:45Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T17:28:20Z
user: login: jukkar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T17:49:54Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T18:27:08Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T20:25:50Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T08:23:07Z
user: login: sayoojkkarun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:12:44Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T15:48:01Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T16:04:36Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T21:14:39Z
user: login: sayoojkkarun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T06:36:29Z
user: login: pdgendt
body: I'm in favor of explicit typing.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T09:51:07Z
user: login: jukkar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T12:00:15Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T07:32:35Z
user: login: sayoojkkarun
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T07:19:57Z
user: login: pdgendt
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T07:39:33Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:25:16Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:33:27Z
user: login: pdgendt
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:56:10Z
user: login: jukkar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T10:50:12Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:22:59Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:23:03Z
user: login: jukkar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T13:51:56Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:51:02Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:08:48Z
user: login: pdgendt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:12:40Z
user: login: Abd002
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:25:08Z
user: login: pdgendt
body: Thanks, few more nits
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T14:46:50Z
user: login: pdgendt
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T11:30:06Z
user: login: jukkar
body: Thanks! Looks very nice now.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-24T08:43:00Z

Review comments:
diff_hunk: @@ -205,6 +205,8 @@ struct net_conn_handle;
  * anyway. This saves 12 bytes / context in IPv6.
  */
 __net_socket struct net_context {
+	/* Controls multicast loopback */
+	bool multicast_loop;
path: include/zephyr/net/net_context.h
user: login: pdgendt
body: This should not be the first member, please read the comment below.
created_at: 2025-03-17T07:22:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,56 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+			struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->multicast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+	
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+	
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} 
+			else {
+					NET_ERR("Failed to clone IPv4 multicast packet");
+			}
+		}
+	}
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: Indentation looks wrong, I'd suggest running clang-format on this block.
created_at: 2025-03-17T07:23:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -845,20 +845,41 @@ static inline int z_vrfy_zsock_inet_pton(sa_family_t family,
 #endif
 
 int z_impl_zsock_getsockopt(int sock, int level, int optname,
-			    void *optval, socklen_t *optlen)
+	void* optval, socklen_t* optlen)
path: subsys/net/lib/sockets/sockets.c
user: login: pdgendt
body: Don't change formatting here.
created_at: 2025-03-17T07:24:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,56 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+			struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->multicast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+	
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+	
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} 
+			else {
+					NET_ERR("Failed to clone IPv4 multicast packet");
+			}
+		}
+	}
+#endif
+
+#if defined(CONFIG_NET_IPV6)
+	if (net_pkt_family(pkt) == AF_INET6 &&
+	net_ipv6_is_addr_mcast((const struct in6_addr *)&NET_IPV6_HDR(pkt)->dst)) {
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: IPv6 is controlled by a separate socket option, `IPV6_MULTICAST_LOOP`. @sayooj-aerlync was looking at that too.
created_at: 2025-03-17T07:26:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+Socket Options
+==============
+
+IPv4 Options
+------------
+
+- ``IP_MULTICAST_LOOP``: Controls whether multicast packets are looped back to
+  the sending host. Set to 1 to enable loopback, 0 to disable.
path: doc/reference/networking/sockets.rst
user: login: jukkar
body: This file is not really needed, or if we want to have it it should contain documentation of all the options and not just for this one. Anyway, that is something for another PR.
created_at: 2025-03-17T07:35:51Z
author_association: MEMBER
diff_hunk: @@ -205,6 +205,8 @@ struct net_conn_handle;
  * anyway. This saves 12 bytes / context in IPv6.
  */
 __net_socket struct net_context {
+	/* Controls multicast loopback */
+	bool multicast_loop;
path: include/zephyr/net/net_context.h
user: login: jukkar
body: Place this option to options section like the other options. Also please rename it to `ipv4_multicast_loop`
Also make sure this is only available if `CONFIG_NET_IPV4` is enabled. For example like this.
```
IF_ENABLED(CONFIG_NET_IPV4, (bool ipv4_multicast_loop));
```

created_at: 2025-03-17T07:37:43Z
author_association: MEMBER
diff_hunk: @@ -2150,6 +2150,7 @@ static struct net_pkt *context_alloc_pkt(struct net_context *context,
 					 size_t len, k_timeout_t timeout)
 {
 	struct net_pkt *pkt;
+	context->multicast_loop = true;
path: subsys/net/ip/net_context.c
user: login: jukkar
body: Place the default value setting to `net_context_get()` like the other options.
created_at: 2025-03-17T07:39:37Z
author_association: MEMBER
diff_hunk: @@ -845,20 +845,41 @@ static inline int z_vrfy_zsock_inet_pton(sa_family_t family,
 #endif
 
 int z_impl_zsock_getsockopt(int sock, int level, int optname,
-			    void *optval, socklen_t *optlen)
+	void* optval, socklen_t* optlen)
 {
 	int ret;
 
 	SYS_PORT_TRACING_OBJ_FUNC_ENTER(socket, getsockopt, sock, level, optname);
 
+	if (level == IPPROTO_IP && optname == IP_MULTICAST_LOOP) {
+		if (*optlen < sizeof(int)) {
+			errno = EINVAL;
+			ret = -1;
+			goto exit;
+		}
+
+		struct net_context* ctx = zsock_get_context_object(sock);
+		if (ctx == NULL) {
+			errno = EBADF;
+			ret = -1;
+			goto exit;
+		}
+
+		*(int*)optval = ctx->multicast_loop;
+		*optlen = sizeof(int);
+		ret = 0;
+		goto exit;
+	}
+exit:
path: subsys/net/lib/sockets/sockets.c
user: login: jukkar
body: Place the option setting / getting to `subsys/net/lib/sockets/sockets_inet.c` where the other options are set
created_at: 2025-03-17T07:43:22Z
author_association: MEMBER
diff_hunk: @@ -205,6 +205,8 @@ struct net_conn_handle;
  * anyway. This saves 12 bytes / context in IPv6.
  */
 __net_socket struct net_context {
+	/* Controls multicast loopback */
+	bool multicast_loop;
path: include/zephyr/net/net_context.h
user: login: sayoojkkarun
body: Add the option below the following code:
` 
	union {
		struct {
			uint8_t ipv6_hop_limit;       /**< IPv6 hop limit */
			uint8_t ipv6_mcast_hop_limit; /**< IPv6 multicast hop limit */
		};
		struct {
			uint8_t ipv4_ttl;       /**< IPv4 TTL */
			uint8_t ipv4_mcast_ttl; /**< IPv4 multicast TTL */
		};
	};
`
created_at: 2025-03-17T08:15:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -205,6 +205,8 @@ struct net_conn_handle;
  * anyway. This saves 12 bytes / context in IPv6.
  */
 __net_socket struct net_context {
+	/* Controls multicast loopback */
+	bool multicast_loop;
path: include/zephyr/net/net_context.h
user: login: sayoojkkarun
body: Also let's follow the naming convention used in the file. we can write the variable as `bool ipv4_mcast_loop`
created_at: 2025-03-17T09:09:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,56 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+			struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->multicast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+	
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+	
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} 
+			else {
+					NET_ERR("Failed to clone IPv4 multicast packet");
path: subsys/net/ip/net_core.c
user: login: sayoojkkarun
body: Keep the Log priority as NET_DBG, as it may flood the console if errors occur. Let the logs be present only if debug mode is enabled.
created_at: 2025-03-17T10:00:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,56 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+			struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->multicast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+	
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+	
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} 
+			else {
+					NET_ERR("Failed to clone IPv4 multicast packet");
path: subsys/net/ip/net_core.c
user: login: sayoojkkarun
body: @pdgendt @jukkar Should it go to ` goto err;` if the copy fails?
created_at: 2025-03-17T10:15:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,56 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+			struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->multicast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+	
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+	
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} 
+			else {
+					NET_ERR("Failed to clone IPv4 multicast packet");
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: The packet is already sent, so I don't think we should `goto err;`, but we should increment some error stat count.
created_at: 2025-03-17T10:25:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,56 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+			struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->multicast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+	
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+	
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} 
+			else {
+					NET_ERR("Failed to clone IPv4 multicast packet");
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: I wonder if we need to clone actually, why can't we just call `processing_data(pkt, true)` here?

CC: @jukkar 
created_at: 2025-03-17T10:27:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,56 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+			struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->multicast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+	
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+	
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} 
+			else {
+					NET_ERR("Failed to clone IPv4 multicast packet");
path: subsys/net/ip/net_core.c
user: login: jukkar
body: I think we need to clone the packet as the lower levels might have tweaked the data we just sent. This also means that the cloning needs to be done **before** we have actually sent the data.

created_at: 2025-03-17T13:02:22Z
author_association: MEMBER
diff_hunk: @@ -411,6 +411,32 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
path: subsys/net/ip/net_core.c
user: login: sayoojkkarun
body: please run checkpatch.pl and see the intendation.
created_at: 2025-03-17T13:41:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -418,7 +418,11 @@ __net_socket struct net_context {
 			uint8_t ipv4_mcast_ttl; /**< IPv4 multicast TTL */
 		};
 	};
-
+	
+#if defined(CONFIG_NET_IPV4)
+    	bool ipv4_mcast_loop;
path: include/zephyr/net/net_context.h
user: login: pdgendt
body: No spaces before a tab

```suggestion
	bool ipv4_mcast_loop;
```
created_at: 2025-03-17T17:23:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2150,6 +2153,7 @@ static struct net_pkt *context_alloc_pkt(struct net_context *context,
 					 size_t len, k_timeout_t timeout)
 {
 	struct net_pkt *pkt;
+	
path: subsys/net/ip/net_context.c
user: login: pdgendt
body: Remove unrelated change
```suggestion
```
created_at: 2025-03-17T17:24:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1973,6 +1973,21 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+		case IP_MULTICAST_LOOP:
+			if (IS_ENABLED(CONFIG_NET_IPV4)) {
+				if (*optlen != sizeof(int)) {
+					errno = EINVAL;
+					return -1;
+				}
+		
+				*(int *)optval = ctx->ipv4_mcast_loop;
+				*optlen = sizeof(int);
+				
+				return 0;
+			}
+				
+			errno = ENOPROTOOPT;
+			return -1;
path: subsys/net/lib/sockets/sockets_inet.c
user: login: pdgendt
body: The `ipv4_mcast_loop` member is compiled out if `CONFIG_NET_IPV4` isn't enabled.

```suggestion
#if defined(CONFIG_NET_IPV4)
		case IP_MULTICAST_LOOP:
			if (*optlen != sizeof(int)) {
				errno = EINVAL;
				return -1;
			}
		
			*(int *)optval = ctx->ipv4_mcast_loop;
			*optlen = sizeof(int);
				
			return 0;
#endif
```
created_at: 2025-03-17T17:27:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2591,6 +2606,18 @@ int zsock_setsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+		case IP_MULTICAST_LOOP:
+			if (IS_ENABLED(CONFIG_NET_IPV4)) {
+				if (optlen != sizeof(int)) {
+					errno = EINVAL;
+					return -1;
+				}
+
+				ctx->ipv4_mcast_loop = *(const int *)optval;
+				return 0;
+			}
+			errno = ENOPROTOOPT;
+			return -1;
path: subsys/net/lib/sockets/sockets_inet.c
user: login: pdgendt
body: Same here

```suggestion
#if defined(CONFIG_NET_IPV4)
		case IP_MULTICAST_LOOP:
			if (optlen != sizeof(int)) {
				errno = EINVAL;
				return -1;
			}

			ctx->ipv4_mcast_loop = *(const int *)optval;
			return 0;
#endif
```
created_at: 2025-03-17T17:27:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -418,7 +418,11 @@ __net_socket struct net_context {
 			uint8_t ipv4_mcast_ttl; /**< IPv4 multicast TTL */
 		};
 	};
-
+	
+#if defined(CONFIG_NET_IPV4)
+    	bool ipv4_mcast_loop;
path: include/zephyr/net/net_context.h
user: login: jukkar
body: Please add doxygen comment for this newly added value.
created_at: 2025-03-17T17:47:00Z
author_association: MEMBER
diff_hunk: @@ -411,6 +411,32 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
path: subsys/net/ip/net_core.c
user: login: Abd002
body: ok
created_at: 2025-03-17T18:27:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,32 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
path: subsys/net/ip/net_core.c
user: login: Abd002
body: thanks for that
created_at: 2025-03-17T20:25:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,31 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: As @jukkar mentioned in some other comment, the packet needs to be cloned before sending it.

Also the formatting for this function is still off.
created_at: 2025-03-18T08:22:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1973,6 +1973,19 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			if (*optlen != sizeof(int)) {
+				errno = EINVAL;
+				return -1;
+			}
+
+			*(int *)optval = ctx->ipv4_mcast_loop;
+			*optlen = sizeof(int);
+
+			return 0;
+#endif
+
path: subsys/net/lib/sockets/sockets_inet.c
user: login: pdgendt
body: No need for an additional newline

```suggestion
```
created_at: 2025-03-18T08:22:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,31 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} else {
+				NET_ERR("Failed to clone IPv4 multicast packet");
+			}
+		}
+	}
+#endif
path: subsys/net/ip/net_core.c
user: login: sayoojkkarun
body: Hi @Abd002 Could you please let me know how your test setup is?
The packet has to be cloned before `net_if_try_send_data` because the packet is unreferenced after queueing it for tx in `net_if_try_queue_tx`.
created_at: 2025-03-18T09:12:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,31 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
path: subsys/net/ip/net_core.c
user: login: Abd002
body: in formatting, u mean the condition before this ?
created_at: 2025-03-18T15:48:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,31 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: Yes, it is fixed now.
created_at: 2025-03-18T16:04:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,31 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} else {
+				NET_ERR("Failed to clone IPv4 multicast packet");
+			}
+		}
+	}
+#endif
path: subsys/net/ip/net_core.c
user: login: Abd002
body: thank you very much, still working on it.
created_at: 2025-03-18T21:14:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,31 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} else {
+				NET_ERR("Failed to clone IPv4 multicast packet");
+			}
+		}
+	}
+#endif
path: subsys/net/ip/net_core.c
user: login: sayoojkkarun
body: I would suggest you to get a testbed up first, verify it and then push the code.
created_at: 2025-03-19T06:36:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,6 +406,30 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	    net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: ```suggestion
			if (clone != NULL) {
```
created_at: 2025-03-19T09:27:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,6 +406,30 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	    net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: ```suggestion
		if (ctx != NULL && ctx->ipv4_mcast_loop) {
```
created_at: 2025-03-19T09:28:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1973,6 +1973,18 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			if (*optlen != sizeof(int)) {
+				errno = EINVAL;
+				return -1;
+			}
+
+			*(int *)optval = ctx->ipv4_mcast_loop;
path: subsys/net/lib/sockets/sockets_inet.c
user: login: pdgendt
body: ```suggestion
			*(int *)optval = ctx->ipv4_mcast_loop ? 1 : 0;
```
created_at: 2025-03-19T09:28:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2591,6 +2603,16 @@ int zsock_setsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			if (optlen != sizeof(int)) {
+				errno = EINVAL;
+				return -1;
+			}
+
+			ctx->ipv4_mcast_loop = *(const int *)optval;
path: subsys/net/lib/sockets/sockets_inet.c
user: login: pdgendt
body: ```suggestion
			ctx->ipv4_mcast_loop = *(const int *)optval != 0;
```
created_at: 2025-03-19T09:29:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -419,6 +419,11 @@ __net_socket struct net_context {
 		};
 	};
 
+#if defined(CONFIG_NET_IPV4)
+	/* Controls whether IPv4 multicast packets are looped back to the sender. */
+	bool ipv4_mcast_loop;
path: include/zephyr/net/net_context.h
user: login: jukkar
body: As I mentioned in IPv6 PR for this feature, we can share this variable between IPv6 and IPv4. So whichever PR is merged 2nd, it needs to change to use union here.
created_at: 2025-03-19T11:56:26Z
author_association: MEMBER
diff_hunk: @@ -565,6 +565,9 @@ int net_context_get(sa_family_t family, enum net_sock_type type, uint16_t proto,
 #if defined(CONFIG_NET_IPV4_MAPPING_TO_IPV6)
 		/* By default IPv4 and IPv6 are in different port spaces */
 		contexts[i].options.ipv6_v6only = true;
+#endif
+#if defined(CONFIG_NET_IPV4)
+		contexts[i].ipv4_mcast_loop = true;
path: subsys/net/ip/net_context.c
user: login: jukkar
body: Please take a look of IPv6 PR #87338 and have a Kconfig option to set the initial value.
created_at: 2025-03-19T11:57:42Z
author_association: MEMBER
diff_hunk: @@ -406,6 +406,30 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	    net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
+				}
+			} else {
+				NET_ERR("Failed to clone IPv4 multicast packet");
path: subsys/net/ip/net_core.c
user: login: jukkar
body: Please change this to `NET_DBG`
created_at: 2025-03-19T11:58:14Z
author_association: MEMBER
diff_hunk: @@ -406,6 +406,30 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	    net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
+					net_pkt_unref(clone);
path: subsys/net/ip/net_core.c
user: login: jukkar
body: Please update the statistics
```
					if (IS_ENABLED(CONFIG_NET_STATISTICS)) {
						net_stats_update_ipv4_drop(net_pkt_iface(pkt));
					}
```
created_at: 2025-03-19T11:59:16Z
author_association: MEMBER
diff_hunk: @@ -406,6 +406,30 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
+	if (net_pkt_family(pkt) == AF_INET &&
+	    net_ipv4_is_addr_mcast((const struct in_addr *)&NET_IPV4_HDR(pkt)->dst)) {
+		struct net_context *ctx = net_pkt_context(pkt);
+
+		if (ctx && ctx->ipv4_mcast_loop) {
+			struct net_pkt *clone = net_pkt_clone(pkt, K_NO_WAIT);
+
+			if (clone) {
+				NET_DBG("Looping back IPv4 multicast packet %p", clone);
+
+				net_pkt_set_iface(clone, net_pkt_iface(pkt));
+
+				if (net_recv_data(net_pkt_iface(clone), clone) < 0) {
+					NET_ERR("Failed to process cloned IPv4 packet");
path: subsys/net/ip/net_core.c
user: login: jukkar
body: This error print is probably not needed, please remove it.
created_at: 2025-03-19T11:59:53Z
author_association: MEMBER
diff_hunk: @@ -1973,6 +1973,18 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			if (*optlen != sizeof(int)) {
+				errno = EINVAL;
+				return -1;
+			}
+
+			*(int *)optval = ctx->options.ipv4_mcast_loop ? 1 : 0;
+			*optlen = sizeof(int);
+
+			return 0;
path: subsys/net/lib/sockets/sockets_inet.c
user: login: pdgendt
body: Similar to #87338 I'd prefer to add a `NET_OPT_IPV4_MCAST_LOOP` to `enum net_context_option` and work with getter/setters here.
created_at: 2025-03-20T07:32:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -565,6 +565,10 @@ int net_context_get(sa_family_t family, enum net_sock_type type, uint16_t proto,
 #if defined(CONFIG_NET_IPV4_MAPPING_TO_IPV6)
 		/* By default IPv4 and IPv6 are in different port spaces */
 		contexts[i].options.ipv6_v6only = true;
+#endif
+#if defined(CONFIG_NET_IPV4)
+		contexts[i].options.ipv4_mcast_loop = 
path: subsys/net/ip/net_context.c
user: login: pdgendt
body: No trailing spaces

```suggestion
		contexts[i].options.ipv4_mcast_loop =
```
created_at: 2025-03-21T09:25:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -565,6 +565,10 @@ int net_context_get(sa_family_t family, enum net_sock_type type, uint16_t proto,
 #if defined(CONFIG_NET_IPV4_MAPPING_TO_IPV6)
 		/* By default IPv4 and IPv6 are in different port spaces */
 		contexts[i].options.ipv6_v6only = true;
+#endif
+#if defined(CONFIG_NET_IPV4)
+		contexts[i].options.ipv4_mcast_loop = 
path: subsys/net/ip/net_context.c
user: login: Abd002
body: done.
created_at: 2025-03-21T09:33:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1973,6 +1973,19 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			ret = net_context_get_option(ctx,
+						     NET_OPT_IPV4_MCAST_LOOP,
+						     optval, optlen);
+
path: subsys/net/lib/sockets/sockets_inet.c
user: login: jukkar
body: Extra empty line could be removed
created_at: 2025-03-21T10:49:59Z
author_association: MEMBER
diff_hunk: @@ -2591,6 +2604,19 @@ int zsock_setsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			ret = net_context_set_option(ctx,
+						     NET_OPT_IPV4_MCAST_LOOP,
+						     optval, optlen);
+
path: subsys/net/lib/sockets/sockets_inet.c
user: login: jukkar
body: same here
created_at: 2025-03-21T10:50:07Z
author_association: MEMBER
diff_hunk: @@ -1973,6 +1973,19 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			ret = net_context_get_option(ctx,
+						     NET_OPT_IPV4_MCAST_LOOP,
+						     optval, optlen);
+
path: subsys/net/lib/sockets/sockets_inet.c
user: login: Abd002
body: done.
created_at: 2025-03-21T11:22:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2591,6 +2604,19 @@ int zsock_setsockopt_ctx(struct net_context *ctx, int level, int optname,
 			}
 
 			break;
+#if defined(CONFIG_NET_IPV4)
+		case IP_MULTICAST_LOOP:
+			ret = net_context_set_option(ctx,
+						     NET_OPT_IPV4_MCAST_LOOP,
+						     optval, optlen);
+
path: subsys/net/lib/sockets/sockets_inet.c
user: login: Abd002
body: done.
created_at: 2025-03-21T11:23:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3685,9 +3716,12 @@ int net_context_set_option(struct net_context *context,
 	case NET_OPT_LOCAL_PORT_RANGE:
 		ret = set_context_local_port_range(context, value, len);
 		break;
+	case NET_OPT_IPV4_MCAST_LOOP:
+		ret = set_context_ipv4_mcast_loop(context, value, len);
+    break;
 	case NET_OPT_IPV6_MCAST_LOOP:
-		ret = set_context_ipv6_mcast_loop(context, value, len);
-		break;
+    ret = set_context_ipv6_mcast_loop(context, value, len);
+    break;
path: subsys/net/ip/net_context.c
user: login: jukkar
body: Something went wrong with the indentation. You probably do not need to touch the IPv6 variant here.
created_at: 2025-03-21T13:51:22Z
author_association: MEMBER
diff_hunk: @@ -3685,9 +3716,12 @@ int net_context_set_option(struct net_context *context,
 	case NET_OPT_LOCAL_PORT_RANGE:
 		ret = set_context_local_port_range(context, value, len);
 		break;
+	case NET_OPT_IPV4_MCAST_LOOP:
+		ret = set_context_ipv4_mcast_loop(context, value, len);
+    break;
path: subsys/net/ip/net_context.c
user: login: jukkar
body: Indentation wrong
created_at: 2025-03-21T13:51:31Z
author_association: MEMBER
diff_hunk: @@ -3773,6 +3807,9 @@ int net_context_get_option(struct net_context *context,
 	case NET_OPT_LOCAL_PORT_RANGE:
 		ret = get_context_local_port_range(context, value, len);
 		break;
+	case NET_OPT_IPV4_MCAST_LOOP:
+		ret = get_context_ipv4_mcast_loop(context, value, len);
+    break;
path: subsys/net/ip/net_context.c
user: login: jukkar
body: ditto
created_at: 2025-03-21T13:51:41Z
author_association: MEMBER
diff_hunk: @@ -410,6 +410,28 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: This block is now almost identical to the ipv6, wondering if there'd be a smart way to merge these?
created_at: 2025-03-21T14:50:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -410,6 +410,28 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
path: subsys/net/ip/net_core.c
user: login: Abd002
body: I can make macro and pass function names to it, or I should make something simpler ?
created_at: 2025-03-21T15:08:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -410,6 +410,28 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: Perhaps a helper function you pass the `pkt` that returns a boolean if the packet should be looped, that way we can split the check logic from the cloning.
created_at: 2025-03-21T15:12:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -410,6 +410,28 @@ int net_try_send_data(struct net_pkt *pkt, k_timeout_t timeout)
 		goto err;
 	}
 
+#if defined(CONFIG_NET_IPV4)
path: subsys/net/ip/net_core.c
user: login: Abd002
body: sure, I will do it.
created_at: 2025-03-21T15:25:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -366,6 +366,35 @@ static inline int check_ip(struct net_pkt *pkt)
 	return ret;
 }
 
+#if defined(CONFIG_NET_IPV4) || defined(CONFIG_NET_IPV6)
+static inline bool process_multicast(struct net_pkt *pkt)
+{
+	struct net_context *ctx = net_pkt_context(pkt);
+	sa_family_t family = net_pkt_family(pkt);
+	if (!ctx) {
+		return false;
+	}
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: ```suggestion
	struct net_context *ctx = net_pkt_context(pkt);
	sa_family_t family = net_pkt_family(pkt);
	
	if (ctx == NULL) {
		return false;
	}
```
created_at: 2025-03-22T14:44:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -366,6 +366,35 @@ static inline int check_ip(struct net_pkt *pkt)
 	return ret;
 }
 
+#if defined(CONFIG_NET_IPV4) || defined(CONFIG_NET_IPV6)
+static inline bool process_multicast(struct net_pkt *pkt)
+{
+	struct net_context *ctx = net_pkt_context(pkt);
+	sa_family_t family = net_pkt_family(pkt);
+	if (!ctx) {
+		return false;
+	}
+
+#if defined(CONFIG_NET_IPV4)
+	if (family == AF_INET) {
+		const struct in_addr *dst = (const struct in_addr *)&NET_IPV4_HDR(pkt)->dst;
+		if (net_ipv4_is_addr_mcast(dst) && net_context_get_ipv4_mcast_loop(ctx)) {
+			return true;
+		}
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: ```suggestion
		const struct in_addr *dst = (const struct in_addr *)&NET_IPV4_HDR(pkt)->dst;

		return net_ipv4_is_addr_mcast(dst) && net_context_get_ipv4_mcast_loop(ctx);
```
created_at: 2025-03-22T14:44:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -366,6 +366,35 @@ static inline int check_ip(struct net_pkt *pkt)
 	return ret;
 }
 
+#if defined(CONFIG_NET_IPV4) || defined(CONFIG_NET_IPV6)
+static inline bool process_multicast(struct net_pkt *pkt)
+{
+	struct net_context *ctx = net_pkt_context(pkt);
+	sa_family_t family = net_pkt_family(pkt);
+	if (!ctx) {
+		return false;
+	}
+
+#if defined(CONFIG_NET_IPV4)
+	if (family == AF_INET) {
+		const struct in_addr *dst = (const struct in_addr *)&NET_IPV4_HDR(pkt)->dst;
+		if (net_ipv4_is_addr_mcast(dst) && net_context_get_ipv4_mcast_loop(ctx)) {
+			return true;
+		}
+	}
+#endif
+#if defined(CONFIG_NET_IPV6)
+	if (family == AF_INET6) {
+		const struct in6_addr *dst = (const struct in6_addr *)&NET_IPV6_HDR(pkt)->dst;
+		if (net_ipv6_is_addr_mcast(dst) && net_context_get_ipv6_mcast_loop(ctx)) {
+			return true;
+		}
path: subsys/net/ip/net_core.c
user: login: pdgendt
body: ```suggestion
		const struct in6_addr *dst = (const struct in6_addr *)&NET_IPV6_HDR(pkt)->dst;

		return net_ipv6_is_addr_mcast(dst) && net_context_get_ipv6_mcast_loop(ctx);
```
created_at: 2025-03-22T14:45:58Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-16T13:04:56Z
body: Hello @Abd002, and thank you very much for your first pull request to the Zephyr project!
Our Continuous Integration pipeline will execute a series of checks on your Pull Request commit messages and code, and you are expected to address any failures by updating the PR. Please take a look at [our commit message guidelines](https://docs.zephyrproject.org/latest/contribute/guidelines.html#commit-message-guidelines) to find out how to format your commit messages, and at [our contribution workflow](https://docs.zephyrproject.org/latest/contribute/guidelines.html#contribution-workflow) to understand how to update your Pull Request. If you haven't already, please make sure to review the project's [Contributor Expectations](https://docs.zephyrproject.org/latest/contribute/contributor_expectations.html) and update (by amending and force-pushing the commits) your pull request if necessary.
If you are stuck or need help please join us on [Discord](https://chat.zephyrproject.org/) and ask your question there. Additionally, you can [escalate the review](https://docs.zephyrproject.org/latest/contribute/contributor_expectations.html#pr-review-escalation) when applicable. 😊
author_association: NONE
user: login: carlescufi
created_at: 2025-03-16T18:01:02Z
body: Thanks for the PR! Please take a look at [our commit message guidelines](https://docs.zephyrproject.org/latest/contribute/guidelines.html#commit-message-guidelines) to find out how to format your commit messages, and at [our contribution workflow](https://docs.zephyrproject.org/latest/contribute/guidelines.html#contribution-workflow) to understand how to update your Pull Request.
author_association: MEMBER
user: login: carlescufi
created_at: 2025-03-16T18:02:10Z
body: @Abd002 you need to rebase your commits to squash them into a single one
author_association: MEMBER
user: login: pdgendt
created_at: 2025-03-21T13:21:03Z
body: It looks like https://github.com/zephyrproject-rtos/zephyr/pull/87338 got in first, will need to rebase this and merge ipv4 with ipv6.
author_association: CONTRIBUTOR
user: login: jukkar
created_at: 2025-03-21T13:52:44Z
body: Also please rebase the PR and squash the merge commit
author_association: MEMBER
user: login: Abd002
created_at: 2025-03-21T14:30:06Z
body: shouldn't i merge that to solve this conflict or what  ?
author_association: CONTRIBUTOR
user: login: sayoojkkarun
created_at: 2025-03-21T14:33:36Z
body: > shouldn't i merge that to solve this conflict or what  ?

Pull the current zephyr main branch to your fork.
It will show merge conflicts. We will have to solve them.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-24T08:54:40Z
body: Hi @Abd002!
Congratulations on getting your very first Zephyr pull request merged 🎉🥳. This is a fantastic achievement, and we're thrilled to have you as part of our community!

To celebrate this milestone and showcase your contribution, we'd love to award you the Zephyr Technical Contributor badge. If you're interested, please claim your badge by filling out this form: [Claim Your Zephyr Badge](https://forms.gle/oCw9iAPLhUsHTapc8).

Thank you for your valuable input, and we look forward to seeing more of your contributions in the future! 🪁
author_association: NONE