From 4abab85588771dd735f1046292e15d328fc01ded Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 22 Apr 2026 12:08:10 +0200 Subject: [PATCH 1/6] Add multicast UDP sockets --- Makefile | 38 +- README.md | 30 +- docs/API.md | 2 +- src/test/test_multicast_interop.c | 219 +++++++++++ src/test/unit/unit.c | 8 + src/test/unit/unit_tests_multicast.c | 255 +++++++++++++ src/wolfip.c | 520 ++++++++++++++++++++++++++- wolfip.h | 53 +++ 8 files changed, 1118 insertions(+), 7 deletions(-) create mode 100644 src/test/test_multicast_interop.c create mode 100644 src/test/unit/unit_tests_multicast.c diff --git a/Makefile b/Makefile index dba9ac8b..e2760440 100644 --- a/Makefile +++ b/Makefile @@ -242,6 +242,16 @@ build/test-evloop-tun: $(OBJ) build/test/test_eventloop_tun.o build/port/posix/l @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP) +build/test-multicast-interop: CFLAGS+=-DIP_MULTICAST +build/test-multicast-interop: build/multicast/wolfip.o build/test/test_multicast_interop.o build/port/posix/tap_linux.o + @echo "[LD] $@" + @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP) + +build/multicast/wolfip.o: src/wolfip.c + @mkdir -p `dirname $@` || true + @echo "[CC] $< (multicast)" + @$(CC) $(CFLAGS) -DIP_MULTICAST -c $< -o $@ + build/test-dns: $(OBJ) build/test/test_dhcp_dns.o @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP) @@ -374,7 +384,8 @@ UNIT_TEST_SRCS:=src/test/unit/unit.c \ src/test/unit/unit_tests_dns_dhcp.c \ src/test/unit/unit_tests_tcp_ack.c \ src/test/unit/unit_tests_tcp_flow.c \ - src/test/unit/unit_tests_proto.c + src/test/unit/unit_tests_proto.c \ + src/test/unit/unit_tests_multicast.c unit: build/test/unit @@ -385,6 +396,9 @@ build/test/unit: $(UNIT_TEST_SRCS) @echo "[LD] $@" @$(CC) build/test/unit.o -o build/test/unit $(UNIT_LDFLAGS) $(LDFLAGS) +unit-multicast: CFLAGS+=-DIP_MULTICAST +unit-multicast: clean-unit unit + ESP_UNIT_CHECK_CFLAGS := $(CHECK_PKG_CFLAGS) ifeq ($(UNAME_S),Darwin) ifneq ($(CHECK_PREFIX),) @@ -445,6 +459,8 @@ unit-leaksan: clean-unit build/test/unit COV_DIR:=build/coverage COV_UNIT:=$(COV_DIR)/unit COV_UNIT_O:=$(COV_DIR)/unit.o +COV_MCAST_UNIT:=$(COV_DIR)/unit-multicast +COV_MCAST_UNIT_O:=$(COV_DIR)/unit-multicast.o $(COV_UNIT_O): $(UNIT_TEST_SRCS) @mkdir -p $(COV_DIR) @@ -456,6 +472,16 @@ $(COV_UNIT): $(COV_UNIT_O) @echo "[LD] $@" @$(CC) $(COV_UNIT_O) -o $(COV_UNIT) $(UNIT_LDFLAGS) $(LDFLAGS) +$(COV_MCAST_UNIT_O): $(UNIT_TEST_SRCS) + @mkdir -p $(COV_DIR) + @echo "[CC] unit.c (multicast coverage)" + @$(CC) $(UNIT_CFLAGS) $(CFLAGS) -DIP_MULTICAST --coverage -c src/test/unit/unit.c -o $(COV_MCAST_UNIT_O) + +$(COV_MCAST_UNIT): LDFLAGS+=--coverage $(UNIT_LIBS) +$(COV_MCAST_UNIT): $(COV_MCAST_UNIT_O) + @echo "[LD] $@" + @$(CC) $(COV_MCAST_UNIT_O) -o $(COV_MCAST_UNIT) $(UNIT_LDFLAGS) $(LDFLAGS) + cov: unit $(COV_UNIT) @echo "[RUN] unit (coverage)" @rm -f $(COV_DIR)/*.gcda @@ -473,6 +499,14 @@ autocov: unit $(COV_UNIT) @mkdir -p build/coverage @gcovr -r . --exclude "src/test/unit/.*" --html-details -o build/coverage/index.html +autocov-multicast: unit-multicast $(COV_MCAST_UNIT) + @echo "[RUN] unit multicast (coverage)" + @rm -f $(COV_DIR)/*.gcda + @$(COV_MCAST_UNIT) + @echo "[COV] gcovr multicast html" + @mkdir -p build/coverage + @gcovr -r . --exclude "src/test/unit/.*" --html-details -o build/coverage/multicast.html + # Install dynamic library to re-link linux applications # install: @@ -571,7 +605,7 @@ build/test/test-wolfguard-interop: src/test/test_wolfguard_interop.c src/port/po clean-test-wolfguard-interop: @rm -f build/test/test-wolfguard-interop build/test/test_wolfguard_interop.o build/test/linux_tun.o -.PHONY: clean all static cppcheck cov autocov unit-asan unit-ubsan unit-leaksan clean-unit \ +.PHONY: clean all static cppcheck cov autocov autocov-multicast unit-multicast unit-asan unit-ubsan unit-leaksan clean-unit \ unit-esp-asan unit-esp-ubsan unit-esp-leaksan clean-unit-esp \ unit-wolfguard unit-wolfguard-asan unit-wolfguard-ubsan clean-unit-wolfguard \ test-wolfguard-loopback test-wolfguard-loopback-asan test-wolfguard-loopback-ubsan \ diff --git a/README.md b/README.md index 2a2d2413..cebbf6e9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ configured to forward traffic between multiple network interfaces. - Pre-allocated buffers for packet processing in static memory - Multi-interface support - Optional IPv4-forwarding +- Optional IPv4 UDP multicast with IGMPv3 ASM membership reports ## Supported socket types @@ -39,8 +40,9 @@ wolfIP exposes a BSD-like `socket(2)` API for IPv4 sockets: | **Network** | IPv4 | Datagram delivery, TTL handling | [RFC 791](https://datatracker.ietf.org/doc/html/rfc791) | | **Network** | IPv4 Forwarding | Multi-interface routing (optional) | [RFC 1812](https://datatracker.ietf.org/doc/html/rfc1812) | | **Network** | ICMP | Echo request/reply, TTL exceeded | [RFC 792](https://datatracker.ietf.org/doc/html/rfc792) | +| **Network** | IGMPv3 | ASM membership reports for IPv4 multicast (optional) | [RFC 3376](https://datatracker.ietf.org/doc/html/rfc3376) | | **Network** | IPsec | ESP Transport mode | [RFC 4303](https://datatracker.ietf.org/doc/html/rfc4303) | -| **Transport** | UDP | Unicast datagrams, checksum | [RFC 768](https://datatracker.ietf.org/doc/html/rfc768) | +| **Transport** | UDP | Unicast datagrams, checksum, optional IPv4 multicast | [RFC 768](https://datatracker.ietf.org/doc/html/rfc768) | | **Transport** | TCP | Connection management, reliable delivery | [RFC 793](https://datatracker.ietf.org/doc/html/rfc793), [RFC 9293](https://datatracker.ietf.org/doc/html/rfc9293) | | **Transport** | TCP | Maximum Segment Size negotiation | [RFC 793](https://datatracker.ietf.org/doc/html/rfc793) | | **Transport** | TCP | TCP Timestamps, RTT measurement, PAWS, Window Scaling | [RFC 7323](https://datatracker.ietf.org/doc/html/rfc7323) | @@ -139,6 +141,32 @@ The `-I wtcp0` flag pins the test to the injected interface and `-c5` generates five echo requests. Successful replies confirm the ICMP datagram socket support end-to-end through the tap device. +## Optional UDP Multicast + +IPv4 UDP multicast is compiled out by default. Define `IP_MULTICAST` to enable +BSD-style multicast socket options and IGMPv3 ASM membership reports: + +- `WOLFIP_IP_ADD_MEMBERSHIP` +- `WOLFIP_IP_DROP_MEMBERSHIP` +- `WOLFIP_IP_MULTICAST_IF` +- `WOLFIP_IP_MULTICAST_TTL` +- `WOLFIP_IP_MULTICAST_LOOP` + +The implementation supports any-source multicast joins and leaves. Source +filter APIs such as `MCAST_JOIN_SOURCE_GROUP` are not implemented. + +```sh +make unit-multicast +./build/test/unit + +make build/test-multicast-interop +sudo ./build/test-multicast-interop +``` + +The multicast interop test creates a Linux TAP interface (`wmcast0`) and +validates both directions: Linux sending to a wolfIP multicast receiver, and +wolfIP sending to a Linux multicast receiver. + ## FreeRTOS Port wolfIP now includes a dedicated FreeRTOS wrapper port at: diff --git a/docs/API.md b/docs/API.md index 52f47d30..ad3bf14a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -16,7 +16,7 @@ wolfIP is a minimal TCP/IP stack designed for resource-constrained embedded syst - ICMP (RFC 792) - ping replies only - DHCP (RFC 2131) - client only - DNS (RFC 1035) - client only - - UDP (RFC 768) - unicast only + - UDP (RFC 768) - unicast, optional IPv4 multicast with `IP_MULTICAST` - TCP (RFC 793) with options (Timestamps, MSS) ## Core Data Structures diff --git a/src/test/test_multicast_interop.c b/src/test/test_multicast_interop.c new file mode 100644 index 00000000..ec871e0d --- /dev/null +++ b/src/test/test_multicast_interop.c @@ -0,0 +1,219 @@ +/* test_multicast_interop.c + * + * Linux TAP interop smoke tests for wolfIP IPv4 UDP multicast. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "wolfip.h" + +#ifndef IP_MULTICAST +#error "test_multicast_interop requires IP_MULTICAST" +#endif + +#define MCAST_GROUP "239.1.2.9" +#define MCAST_PORT 19009 +#define WOLFIP_MCAST_PORT 19010 + +extern int tap_init(struct wolfIP_ll_dev *dev, const char *name, uint32_t host_ip); + +static uint64_t now_ms(void) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + return (uint64_t)tv.tv_sec * 1000U + (uint64_t)tv.tv_usec / 1000U; +} + +static void poll_stack_for(struct wolfIP *s, unsigned int ms) +{ + uint64_t end = now_ms() + ms; + + while (now_ms() < end) { + (void)wolfIP_poll(s, now_ms()); + usleep(1000); + } +} + +static int host_udp_socket(void) +{ + int fd = socket(AF_INET, SOCK_DGRAM, 0); + int one = 1; + + if (fd < 0) { + perror("socket"); + return -1; + } + (void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + return fd; +} + +static int test_host_to_wolfip(struct wolfIP *s, uint32_t host_ip) +{ + int host_fd; + int wolf_fd; + struct sockaddr_in host_if; + struct sockaddr_in dst; + struct wolfIP_sockaddr_in bind_addr; + struct wolfIP_ip_mreq mreq; + char rx[32]; + const char payload[] = "linux-to-wolfip"; + unsigned int i; + + wolf_fd = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_DGRAM, 17); + if (wolf_fd < 0) + return -1; + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = htons(MCAST_PORT); + bind_addr.sin_addr.s_addr = 0; + if (wolfIP_sock_bind(s, wolf_fd, (struct wolfIP_sockaddr *)&bind_addr, + sizeof(bind_addr)) < 0) + return -1; + memset(&mreq, 0, sizeof(mreq)); + inet_pton(AF_INET, MCAST_GROUP, &mreq.imr_multiaddr.s_addr); + mreq.imr_interface.s_addr = htonl(INADDR_ANY); + if (wolfIP_sock_setsockopt(s, wolf_fd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) + return -1; + + host_fd = host_udp_socket(); + if (host_fd < 0) + return -1; + memset(&host_if, 0, sizeof(host_if)); + host_if.sin_addr.s_addr = host_ip; + if (setsockopt(host_fd, IPPROTO_IP, IP_MULTICAST_IF, + &host_if.sin_addr, sizeof(host_if.sin_addr)) < 0) { + perror("setsockopt IP_MULTICAST_IF"); + close(host_fd); + return -1; + } + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = htons(MCAST_PORT); + inet_pton(AF_INET, MCAST_GROUP, &dst.sin_addr); + + if (sendto(host_fd, payload, sizeof(payload), 0, + (struct sockaddr *)&dst, sizeof(dst)) != (ssize_t)sizeof(payload)) { + perror("sendto host multicast"); + close(host_fd); + return -1; + } + for (i = 0; i < 1000; i++) { + int ret; + + (void)wolfIP_poll(s, now_ms()); + ret = wolfIP_sock_recvfrom(s, wolf_fd, rx, sizeof(rx), 0, NULL, NULL); + if (ret == (int)sizeof(payload) && memcmp(rx, payload, sizeof(payload)) == 0) { + close(host_fd); + return 0; + } + usleep(1000); + } + close(host_fd); + fprintf(stderr, "wolfIP did not receive Linux multicast payload\n"); + return -1; +} + +static int test_wolfip_to_host(struct wolfIP *s, uint32_t host_ip) +{ + int host_fd; + int wolf_fd; + int ttl = 3; + struct sockaddr_in bind_addr; + struct ip_mreq host_mreq; + struct wolfIP_sockaddr_in dst; + fd_set rfds; + struct timeval tv; + char rx[32]; + const char payload[] = "wolfip-to-linux"; + + host_fd = host_udp_socket(); + if (host_fd < 0) + return -1; + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); + bind_addr.sin_port = htons(WOLFIP_MCAST_PORT); + if (bind(host_fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) { + perror("bind host multicast"); + close(host_fd); + return -1; + } + memset(&host_mreq, 0, sizeof(host_mreq)); + inet_pton(AF_INET, MCAST_GROUP, &host_mreq.imr_multiaddr); + host_mreq.imr_interface.s_addr = host_ip; + if (setsockopt(host_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + &host_mreq, sizeof(host_mreq)) < 0) { + perror("host IP_ADD_MEMBERSHIP"); + close(host_fd); + return -1; + } + + wolf_fd = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_DGRAM, 17); + if (wolf_fd < 0) + return -1; + if (wolfIP_sock_setsockopt(s, wolf_fd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0) + return -1; + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = htons(WOLFIP_MCAST_PORT); + inet_pton(AF_INET, MCAST_GROUP, &dst.sin_addr.s_addr); + if (wolfIP_sock_sendto(s, wolf_fd, payload, sizeof(payload), 0, + (struct wolfIP_sockaddr *)&dst, sizeof(dst)) != (int)sizeof(payload)) + return -1; + poll_stack_for(s, 50); + + FD_ZERO(&rfds); + FD_SET(host_fd, &rfds); + tv.tv_sec = 2; + tv.tv_usec = 0; + if (select(host_fd + 1, &rfds, NULL, NULL, &tv) <= 0) { + fprintf(stderr, "Linux did not receive wolfIP multicast payload\n"); + close(host_fd); + return -1; + } + if (recv(host_fd, rx, sizeof(rx), 0) != (ssize_t)sizeof(payload) || + memcmp(rx, payload, sizeof(payload)) != 0) { + fprintf(stderr, "Linux received unexpected multicast payload\n"); + close(host_fd); + return -1; + } + close(host_fd); + return 0; +} + +int main(void) +{ + struct wolfIP *s; + struct wolfIP_ll_dev *tapdev; + struct in_addr host; + + wolfIP_init_static(&s); + tapdev = wolfIP_getdev(s); + if (!tapdev) + return 1; + inet_aton(HOST_STACK_IP, &host); + if (tap_init(tapdev, "wmcast0", host.s_addr) < 0) + return 2; + wolfIP_ipconfig_set(s, atoip4(WOLFIP_IP), atoip4("255.255.255.0"), + atoip4(HOST_STACK_IP)); + poll_stack_for(s, 50); + + if (test_host_to_wolfip(s, host.s_addr) < 0) + return 3; + if (test_wolfip_to_host(s, host.s_addr) < 0) + return 4; + printf("multicast interop ok\n"); + return 0; +} diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 10f2f824..3bcf35f3 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -5,6 +5,7 @@ #include "unit_tests_tcp_ack.c" #include "unit_tests_tcp_flow.c" #include "unit_tests_proto.c" +#include "unit_tests_multicast.c" Suite *wolf_suite(void) { @@ -227,6 +228,13 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_udp_no_icmp_unreachable_for_multicast_src); tcase_add_test(tc_utils, test_udp_no_icmp_unreachable_for_broadcast_dst); tcase_add_test(tc_utils, test_udp_no_icmp_unreachable_for_multicast_dst); +#ifdef IP_MULTICAST + tcase_add_test(tc_utils, test_multicast_join_and_drop_reports); + tcase_add_test(tc_utils, test_multicast_join_validation_and_shared_refs); + tcase_add_test(tc_utils, test_multicast_udp_receive_requires_join); + tcase_add_test(tc_utils, test_multicast_udp_send_mac_ttl_loop_and_options); + tcase_add_test(tc_utils, test_multicast_igmp_query_refreshes_report); +#endif tcase_add_test(tc_utils, test_tcp_no_rst_for_broadcast_dst); tcase_add_test(tc_utils, test_tcp_no_rst_for_multicast_dst); tcase_add_test(tc_utils, test_dhcp_renewing_transitions_to_rebinding); diff --git a/src/test/unit/unit_tests_multicast.c b/src/test/unit/unit_tests_multicast.c new file mode 100644 index 00000000..1a9902a9 --- /dev/null +++ b/src/test/unit/unit_tests_multicast.c @@ -0,0 +1,255 @@ +#ifdef IP_MULTICAST + +static void multicast_mreq(struct wolfIP_ip_mreq *mreq, ip4 group, ip4 if_addr) +{ + memset(mreq, 0, sizeof(*mreq)); + mreq->imr_multiaddr.s_addr = ee32(group); + mreq->imr_interface.s_addr = ee32(if_addr); +} + +static uint8_t *last_igmp_payload(void) +{ + return last_frame_sent + ETH_HEADER_LEN + IP_HEADER_LEN + IP_OPTION_ROUTER_ALERT_LEN; +} + +static void build_multicast_udp(uint8_t *buf, struct wolfIP *s, ip4 src, ip4 dst, + uint16_t sport, uint16_t dport, + const void *payload, uint16_t payload_len) +{ + struct wolfIP_udp_datagram *udp = (struct wolfIP_udp_datagram *)buf; + uint8_t mac[6]; + + memset(buf, 0, sizeof(struct wolfIP_udp_datagram) + payload_len); + mcast_ip_to_eth(dst, mac); + memcpy(udp->ip.eth.dst, mac, sizeof(mac)); + memcpy(udp->ip.eth.src, "\x02\x00\x00\x00\x00\x01", 6); + udp->ip.eth.type = ee16(ETH_TYPE_IP); + udp->ip.ver_ihl = 0x45; + udp->ip.ttl = 64; + udp->ip.proto = WI_IPPROTO_UDP; + udp->ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + payload_len); + udp->ip.src = ee32(src); + udp->ip.dst = ee32(dst); + udp->src_port = ee16(sport); + udp->dst_port = ee16(dport); + udp->len = ee16(UDP_HEADER_LEN + payload_len); + memcpy(udp->data, payload, payload_len); + (void)s; + fix_udp_checksums(udp); +} + +START_TEST(test_multicast_join_and_drop_reports) +{ + struct wolfIP s; + int sd; + struct wolfIP_ip_mreq mreq; + ip4 group = 0xE9010203U; + uint8_t *igmp; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + + multicast_mreq(&mreq, group, IPADDR_ANY); + last_frame_sent_size = 0; + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + ck_assert_uint_eq(s.mcast[0].group, group); + ck_assert_uint_eq(s.mcast[0].refs, 1); + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_mem_eq(last_frame_sent, "\x01\x00\x5e\x00\x00\x16", 6); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + 9], WI_IPPROTO_IGMP); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + 8], 1); + igmp = last_igmp_payload(); + ck_assert_uint_eq(igmp[0], IGMP_TYPE_V3_MEMBERSHIP_REPORT); + ck_assert_uint_eq(igmp[8], IGMPV3_REC_MODE_IS_EXCLUDE); + ck_assert_uint_eq(get_be32(igmp + 12), group); + + last_frame_sent_size = 0; + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + ck_assert_uint_eq(s.mcast[0].refs, 0); + ck_assert_uint_gt(last_frame_sent_size, 0); + igmp = last_igmp_payload(); + ck_assert_uint_eq(igmp[8], IGMPV3_REC_CHANGE_TO_INCLUDE); +} +END_TEST + +START_TEST(test_multicast_join_validation_and_shared_refs) +{ + struct wolfIP s; + int sd1; + int sd2; + struct wolfIP_ip_mreq mreq; + struct wolfIP_ip_mreq bad; + ip4 group = 0xE9010204U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd1 = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + sd2 = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd1, 0); + ck_assert_int_gt(sd2, 0); + + multicast_mreq(&bad, 0x0A000001U, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd1, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &bad, sizeof(bad)), -WOLFIP_EINVAL); + multicast_mreq(&mreq, group, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd1, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd1, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), -WOLFIP_EINVAL); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd2, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + ck_assert_uint_eq(s.mcast[0].refs, 2); + + last_frame_sent_size = 0; + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd1, WOLFIP_SOL_IP, + WOLFIP_IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + ck_assert_uint_eq(s.mcast[0].refs, 1); + ck_assert_uint_eq(last_frame_sent_size, 0); + ck_assert_int_eq(wolfIP_sock_close(&s, sd2), 0); + ck_assert_uint_eq(s.mcast[0].refs, 0); +} +END_TEST + +START_TEST(test_multicast_udp_receive_requires_join) +{ + struct wolfIP s; + int sd; + struct wolfIP_sockaddr_in sin; + struct wolfIP_sockaddr_in from; + socklen_t fromlen = sizeof(from); + struct wolfIP_ip_mreq mreq; + uint8_t out[8]; + const char payload[] = "hello"; + uint8_t frame[sizeof(struct wolfIP_udp_datagram) + sizeof(payload)]; + ip4 group = 0xE9010205U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5000); + ck_assert_int_eq(wolfIP_sock_bind(&s, sd, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + + build_multicast_udp(frame, &s, 0x0A000001U, group, 4000, 5000, + payload, sizeof(payload)); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); + ck_assert_int_eq(wolfIP_sock_recvfrom(&s, sd, out, sizeof(out), 0, + (struct wolfIP_sockaddr *)&from, &fromlen), -WOLFIP_EAGAIN); + + multicast_mreq(&mreq, group, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); + ck_assert_int_eq(wolfIP_sock_recvfrom(&s, sd, out, sizeof(out), 0, + (struct wolfIP_sockaddr *)&from, &fromlen), (int)sizeof(payload)); + ck_assert_mem_eq(out, payload, sizeof(payload)); +} +END_TEST + +START_TEST(test_multicast_udp_send_mac_ttl_loop_and_options) +{ + struct wolfIP s; + int sd; + struct wolfIP_sockaddr_in bind_addr; + struct wolfIP_sockaddr_in dst; + struct wolfIP_ip_mreq mreq; + struct wolfIP_udp_datagram *udp; + uint8_t out[8]; + int ttl = 7; + int loop = 1; + int got = 0; + socklen_t gotlen = sizeof(got); + ip4 group = 0xE9010206U; + const char payload[] = "mc"; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = ee16(5001); + ck_assert_int_eq(wolfIP_sock_bind(&s, sd, (struct wolfIP_sockaddr *)&bind_addr, + sizeof(bind_addr)), 0); + multicast_mreq(&mreq, group, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_TTL, &ttl, sizeof(ttl)), 0); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_LOOP, &loop, sizeof(loop)), 0); + ck_assert_int_eq(wolfIP_sock_getsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_TTL, &got, &gotlen), 0); + ck_assert_int_eq(got, ttl); + + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = ee16(5001); + dst.sin_addr.s_addr = ee32(group); + last_frame_sent_size = 0; + ck_assert_int_eq(wolfIP_sock_sendto(&s, sd, payload, sizeof(payload), 0, + (struct wolfIP_sockaddr *)&dst, sizeof(dst)), (int)sizeof(payload)); + ck_assert_int_eq(wolfIP_poll(&s, 1), 0); + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_mem_eq(last_frame_sent, "\x01\x00\x5e\x01\x02\x06", 6); + udp = (struct wolfIP_udp_datagram *)last_frame_sent; + ck_assert_uint_eq(udp->ip.ttl, ttl); + ck_assert_int_eq(wolfIP_sock_recvfrom(&s, sd, out, sizeof(out), 0, NULL, NULL), + (int)sizeof(payload)); + ck_assert_mem_eq(out, payload, sizeof(payload)); +} +END_TEST + +START_TEST(test_multicast_igmp_query_refreshes_report) +{ + struct wolfIP s; + int sd; + struct wolfIP_ip_mreq mreq; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + IGMPV3_QUERY_MIN_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + uint8_t *igmp = frame + ETH_HEADER_LEN + IP_HEADER_LEN; + ip4 group = 0xE9010207U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + multicast_mreq(&mreq, group, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, "\x01\x00\x5e\x00\x00\x01", 6); + memcpy(ip->eth.src, "\x02\x00\x00\x00\x00\x01", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 1; + ip->proto = WI_IPPROTO_IGMP; + ip->len = ee16(IP_HEADER_LEN + IGMPV3_QUERY_MIN_LEN); + ip->src = ee32(0x0A000001U); + ip->dst = ee32(IGMP_ALL_HOSTS); + igmp[0] = IGMP_TYPE_MEMBERSHIP_QUERY; + put_be32(igmp + 4, group); + put_be16(igmp + 2, ip_checksum_buf(igmp, IGMPV3_QUERY_MIN_LEN)); + fix_ip_checksum(ip); + + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_uint_eq(last_igmp_payload()[8], IGMPV3_REC_MODE_IS_EXCLUDE); +} +END_TEST + +#endif /* IP_MULTICAST */ diff --git a/src/wolfip.c b/src/wolfip.c index 99ec1fa3..a1cef85d 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -81,6 +81,7 @@ struct wolfIP_icmp_packet; #define ICMP_FRAG_NEEDED 4 #define WI_IPPROTO_ICMP 0x01 +#define WI_IPPROTO_IGMP 0x02 #define WI_IPPROTO_TCP 0x06 #define WI_IPPROTO_UDP 0x11 #define IPADDR_ANY 0x00000000 @@ -104,6 +105,17 @@ struct wolfIP_icmp_packet; #define IP_HEADER_LEN 20 #define UDP_HEADER_LEN 8 #define ICMP_HEADER_LEN 8 +#define IGMPV3_REPORT_DST 0xE0000016U +#define IGMP_ALL_HOSTS 0xE0000001U +#define IGMP_TYPE_MEMBERSHIP_QUERY 0x11 +#define IGMP_TYPE_V3_MEMBERSHIP_REPORT 0x22 +#define IGMPV3_REC_MODE_IS_EXCLUDE 2 +#define IGMPV3_REC_CHANGE_TO_INCLUDE 3 +#define IGMP_HEADER_LEN 8 +#define IGMPV3_QUERY_MIN_LEN 12 +#define IGMPV3_REPORT_HEADER_LEN 8 +#define IGMPV3_GROUP_RECORD_BASE_LEN 8 +#define IP_OPTION_ROUTER_ALERT_LEN 4 #define ARP_HEADER_LEN 28 #ifdef ETHERNET @@ -127,6 +139,13 @@ struct wolfIP_icmp_packet; #define TCP_DEFAULT_MSS 536U #define TCP_CTRL_RTO_MAXRTX 6U #define TCP_RTO_MAX_BACKOFF 15U /* Max retries before closing; also clamps shift */ + +#ifdef IP_MULTICAST +#ifndef WOLFIP_UDP_MCAST_MEMBERSHIPS +#define WOLFIP_UDP_MCAST_MEMBERSHIPS 4 +#endif +#define WOLFIP_MCAST_MEMBERSHIPS (MAX_UDPSOCKETS * WOLFIP_UDP_MCAST_MEMBERSHIPS) +#endif #define TCP_RTO_MIN_MS 1000U #define TCP_RTO_MAX_MS 60000U #define TCP_RTO_G_MS 1U @@ -758,6 +777,19 @@ struct PACKED wolfIP_icmp_dest_unreachable_packet { uint8_t orig_packet[TTL_EXCEEDED_ORIG_PACKET_SIZE_MAX]; }; +#ifdef IP_MULTICAST +struct udp_mcast_join { + ip4 group; + uint8_t if_idx; +}; + +struct wolfIP_mcast_membership { + ip4 group; + uint8_t if_idx; + uint8_t refs; +}; +#endif + static uint16_t icmp_echo_id(const struct wolfIP_icmp_packet *icmp) { uint16_t net = 0; @@ -1104,6 +1136,13 @@ struct tcpsocket { /* UDP socket */ struct udpsocket { struct fifo rxbuf, txbuf; +#ifdef IP_MULTICAST + struct udp_mcast_join mcast[WOLFIP_UDP_MCAST_MEMBERSHIPS]; + uint8_t mcast_ttl; + uint8_t mcast_loop; + uint8_t mcast_if_set; + uint8_t mcast_if_idx; +#endif }; struct tsocket { @@ -1176,6 +1215,10 @@ struct packetsocket { }; #endif #endif + +#ifdef IP_MULTICAST +static void udp_mcast_drop_all(struct tsocket *ts); +#endif static inline uint32_t tcp_seq_inc(uint32_t seq, uint32_t n); static inline int tcp_seq_leq(uint32_t a, uint32_t b); static inline int tcp_seq_lt(uint32_t a, uint32_t b); @@ -1305,6 +1348,9 @@ struct wolfIP { #if WOLFIP_PACKET_SOCKETS struct packetsocket packetsockets[WOLFIP_MAX_PACKETSOCKETS]; #endif +#endif +#ifdef IP_MULTICAST + struct wolfIP_mcast_membership mcast[WOLFIP_MCAST_MEMBERSHIPS]; #endif uint16_t ipcounter; uint64_t last_tick; @@ -1588,6 +1634,96 @@ static inline int wolfIP_ip_is_multicast(ip4 addr) return ((addr & 0xF0000000U) == 0xE0000000U); } +#ifdef IP_MULTICAST +static uint16_t ip_checksum_buf(const void *buf, uint16_t len) +{ + const uint8_t *p = (const uint8_t *)buf; + uint32_t sum = 0; + + while (len > 1) { + sum += ((uint16_t)p[0] << 8) | p[1]; + p += 2; + len -= 2; + } + if (len) + sum += (uint16_t)p[0] << 8; + while (sum >> 16) + sum = (sum & 0xffffU) + (sum >> 16); + return (uint16_t)~sum; +} + +static void put_be16(uint8_t *p, uint16_t v) +{ + uint16_t be = ee16(v); + memcpy(p, &be, sizeof(be)); +} + +static void put_be32(uint8_t *p, uint32_t v) +{ + uint32_t be = ee32(v); + memcpy(p, &be, sizeof(be)); +} + +static uint32_t get_be32(const uint8_t *p) +{ + uint32_t be; + + memcpy(&be, p, sizeof(be)); + return ee32(be); +} + +static void mcast_ip_to_eth(ip4 group, uint8_t mac[6]) +{ + mac[0] = 0x01; + mac[1] = 0x00; + mac[2] = 0x5e; + mac[3] = (uint8_t)((group >> 16) & 0x7fU); + mac[4] = (uint8_t)((group >> 8) & 0xffU); + mac[5] = (uint8_t)(group & 0xffU); +} + +static int eth_is_ipv4_multicast_mac(const uint8_t mac[6]) +{ + return mac[0] == 0x01 && mac[1] == 0x00 && mac[2] == 0x5e && + (mac[3] & 0x80U) == 0; +} + +static struct wolfIP_mcast_membership *mcast_membership_find(struct wolfIP *s, + unsigned int if_idx, + ip4 group) +{ + unsigned int i; + + if (!s) + return NULL; + for (i = 0; i < WOLFIP_MCAST_MEMBERSHIPS; i++) { + if (s->mcast[i].group == group && s->mcast[i].if_idx == if_idx && + s->mcast[i].refs != 0) + return &s->mcast[i]; + } + return NULL; +} + +static int mcast_is_joined(struct wolfIP *s, unsigned int if_idx, ip4 group) +{ + return mcast_membership_find(s, if_idx, group) != NULL; +} + +static int udp_socket_has_mcast(const struct tsocket *t, unsigned int if_idx, ip4 group) +{ + unsigned int i; + + if (!t) + return 0; + for (i = 0; i < WOLFIP_UDP_MCAST_MEMBERSHIPS; i++) { + if (t->sock.udp.mcast[i].group == group && + t->sock.udp.mcast[i].if_idx == if_idx) + return 1; + } + return 0; +} +#endif + static int wolfIP_ip_is_broadcast(const struct wolfIP *s, ip4 addr) { unsigned int i; @@ -2076,6 +2212,10 @@ static struct tsocket *udp_new_socket(struct wolfIP *s) t->if_idx = 0; fifo_init(&t->sock.udp.rxbuf, t->rxmem, RXBUF_SIZE); fifo_init(&t->sock.udp.txbuf, t->txmem, TXBUF_SIZE); +#ifdef IP_MULTICAST + t->sock.udp.mcast_ttl = 1; + t->sock.udp.mcast_loop = 1; +#endif if (tx_has_writable_space(t)) t->events |= CB_EVENT_WRITABLE; return t; @@ -2128,9 +2268,19 @@ static void udp_try_recv(struct wolfIP *s, unsigned int if_idx, for (i = 0; i < MAX_UDPSOCKETS; i++) { struct tsocket *t = &s->udpsockets[i]; uint32_t expected_len; - if (t->src_port == ee16(udp->dst_port) && (t->dst_port == 0 || t->dst_port == ee16(udp->src_port)) && + int addr_match = (((t->local_ip == 0) && DHCP_IS_RUNNING(s)) || - (t->local_ip == dst_ip && (t->remote_ip == 0 || t->remote_ip == src_ip))) ) { + (t->local_ip == dst_ip && (t->remote_ip == 0 || t->remote_ip == src_ip))); +#ifdef IP_MULTICAST + if (wolfIP_ip_is_multicast(dst_ip)) { + addr_match = udp_socket_has_mcast(t, if_idx, dst_ip) && + (t->remote_ip == 0 || t->remote_ip == src_ip || + t->remote_ip == dst_ip); + } +#endif + if (t->src_port == ee16(udp->dst_port) && + (t->dst_port == 0 || t->dst_port == ee16(udp->src_port)) && + addr_match) { if (t->local_ip == 0) t->if_idx = (uint8_t)if_idx; @@ -3611,6 +3761,123 @@ static int eth_output_add_header(struct wolfIP *S, unsigned int if_idx, } #endif +#ifdef IP_MULTICAST +static int igmp_send_report(struct wolfIP *s, unsigned int if_idx, ip4 group, + uint8_t record_type) +{ + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + IP_OPTION_ROUTER_ALERT_LEN + + IGMPV3_REPORT_HEADER_LEN + IGMPV3_GROUP_RECORD_BASE_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + uint8_t *iph = frame + ETH_HEADER_LEN; + uint8_t *igmp; + struct ipconf *conf; + uint16_t ip_hlen = IP_HEADER_LEN + IP_OPTION_ROUTER_ALERT_LEN; + uint16_t igmp_len = IGMPV3_REPORT_HEADER_LEN + IGMPV3_GROUP_RECORD_BASE_LEN; + uint16_t ip_len = ip_hlen + igmp_len; + uint16_t id; +#ifdef ETHERNET + uint8_t mac[6]; +#endif + + if (!s || !wolfIP_ip_is_multicast(group)) + return -WOLFIP_EINVAL; + conf = wolfIP_ipconf_at(s, if_idx); + if (!conf || conf->ip == IPADDR_ANY) + return -WOLFIP_EINVAL; + memset(frame, 0, sizeof(frame)); + + iph[0] = 0x46; /* IPv4, 24-byte header with Router Alert option. */ + iph[1] = 0; + put_be16(iph + 2, ip_len); + id = ipcounter_next(s); + memcpy(iph + 4, &id, sizeof(id)); + put_be16(iph + 6, 0); + iph[8] = 1; + iph[9] = WI_IPPROTO_IGMP; + put_be32(iph + 12, conf->ip); + put_be32(iph + 16, IGMPV3_REPORT_DST); + iph[20] = 0x94; + iph[21] = IP_OPTION_ROUTER_ALERT_LEN; + iph[22] = 0; + iph[23] = 0; + put_be16(iph + 10, ip_checksum_buf(iph, ip_hlen)); + + igmp = iph + ip_hlen; + igmp[0] = IGMP_TYPE_V3_MEMBERSHIP_REPORT; + igmp[1] = 0; + igmp[4] = 0; + igmp[5] = 0; + igmp[6] = 0; + igmp[7] = 1; + igmp[8] = record_type; + igmp[9] = 0; + igmp[10] = 0; + igmp[11] = 0; + put_be32(igmp + 12, group); + put_be16(igmp + 2, ip_checksum_buf(igmp, igmp_len)); + +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(s, if_idx)) { + mcast_ip_to_eth(IGMPV3_REPORT_DST, mac); + eth_output_add_header(s, if_idx, mac, &ip->eth, ETH_TYPE_IP); + } +#endif +#ifdef WOLFIP_ESP + /* Mirror the ESP-encap pattern used elsewhere (icmp_input, + * wolfIP_send_ttl_exceeded, wolfIP_poll): if an outbound SA matches the + * report destination, esp_send wraps and transmits; otherwise it returns + * 1 and we fall through to the plaintext send. In the common case no SA + * is configured for 224.0.0.22 so the normal path is unchanged. */ + if (!wolfIP_ll_is_non_ethernet(s, if_idx)) { + struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, if_idx); + if (esp_send(ll, ip, ip_len) == 1) + return wolfIP_ll_send_frame(s, if_idx, frame, sizeof(frame)); + return 0; + } +#endif + return wolfIP_ll_send_frame(s, if_idx, frame, sizeof(frame)); +} + +static void igmp_input(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip_packet *ip, uint32_t frame_len) +{ + uint16_t ip_len; + uint16_t igmp_len; + uint8_t *igmp; + ip4 group = IPADDR_ANY; + unsigned int i; + + if (!s || frame_len < ETH_HEADER_LEN + IP_HEADER_LEN + IGMP_HEADER_LEN) + return; + ip_len = ee16(ip->len); + if (ip_len < IP_HEADER_LEN + IGMP_HEADER_LEN || + frame_len < (uint32_t)(ETH_HEADER_LEN + ip_len)) + return; + igmp_len = ip_len - IP_HEADER_LEN; + igmp = ((uint8_t *)ip) + ETH_HEADER_LEN + IP_HEADER_LEN; + if (ip_checksum_buf(igmp, igmp_len) != 0) + return; + if (igmp[0] != IGMP_TYPE_MEMBERSHIP_QUERY) + return; + /* RFC 2236 §2 (IGMPv2) and RFC 3376 §4.1 (IGMPv3) both place the Group + * Address at offset 4 within the message. Read unconditionally so that + * IGMPv1/v2 group-specific queries (8-byte messages) are not silently + * treated as general queries. */ + group = get_be32(igmp + 4); + if (group != IPADDR_ANY && !wolfIP_ip_is_multicast(group)) + return; + + for (i = 0; i < WOLFIP_MCAST_MEMBERSHIPS; i++) { + if (s->mcast[i].refs == 0 || s->mcast[i].if_idx != if_idx) + continue; + if (group != IPADDR_ANY && group != s->mcast[i].group) + continue; + (void)igmp_send_report(s, if_idx, s->mcast[i].group, + IGMPV3_REC_MODE_IS_EXCLUDE); + } +} +#endif + #ifdef WOLFIP_ESP #include "src/wolfesp.c" #endif /* WOLFIP_ESP */ @@ -3721,6 +3988,10 @@ static int ip_output_add_header(struct tsocket *t, struct wolfIP_ip_packet *ip, ip->len = ee16(len); ip->flags_fo = (proto == WI_IPPROTO_TCP) ? ee16(0x4000U) : 0; ip->ttl = 64; +#ifdef IP_MULTICAST + if (proto == WI_IPPROTO_UDP && wolfIP_ip_is_multicast(t->remote_ip)) + ip->ttl = t->sock.udp.mcast_ttl; +#endif ip->proto = proto; ip->id = ee16(t->S->ipcounter); t->S->ipcounter = (uint16_t)(t->S->ipcounter + 1); @@ -4908,6 +5179,10 @@ static void close_socket(struct tsocket *ts) ts->sock.tcp.tmr_rto = NO_TIMER; } } +#ifdef IP_MULTICAST + if (ts->proto == WI_IPPROTO_UDP) + udp_mcast_drop_all(ts); +#endif memset(ts, 0, sizeof(struct tsocket)); } @@ -5414,6 +5689,10 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len ts->src_port += 1024; } if_idx = wolfIP_route_for_ip(s, ts->remote_ip); +#ifdef IP_MULTICAST + if (wolfIP_ip_is_multicast(ts->remote_ip) && ts->sock.udp.mcast_if_set) + if_idx = ts->sock.udp.mcast_if_idx; +#endif conf = wolfIP_ipconf_at(s, if_idx); ts->if_idx = (uint8_t)if_idx; if (ts->local_ip == 0) { @@ -5890,6 +6169,110 @@ int wolfIP_sock_read(struct wolfIP *s, int sockfd, void *buf, size_t len) return wolfIP_sock_recvfrom(s, sockfd, buf, len, 0, NULL, 0); } +#ifdef IP_MULTICAST +static int mcast_if_from_addr(struct wolfIP *s, ip4 if_addr, ip4 group, + unsigned int *if_idx) +{ + int found = 0; + + if (!s || !if_idx || !wolfIP_ip_is_multicast(group)) + return -WOLFIP_EINVAL; + if (if_addr == IPADDR_ANY) { + *if_idx = wolfIP_route_for_ip(s, group); + if (wolfIP_ipconf_at(s, *if_idx)) + return 0; + return -WOLFIP_EINVAL; + } + *if_idx = wolfIP_if_for_local_ip(s, if_addr, &found); + return found ? 0 : -WOLFIP_EINVAL; +} + +static int udp_mcast_join(struct wolfIP *s, struct tsocket *ts, ip4 group, + unsigned int if_idx) +{ + unsigned int i; + unsigned int j; + struct wolfIP_mcast_membership *m = NULL; + + if (!s || !ts || !wolfIP_ip_is_multicast(group) || if_idx >= s->if_count) + return -WOLFIP_EINVAL; + if (udp_socket_has_mcast(ts, if_idx, group)) + return -WOLFIP_EINVAL; + for (i = 0; i < WOLFIP_UDP_MCAST_MEMBERSHIPS; i++) { + if (ts->sock.udp.mcast[i].group == IPADDR_ANY) + break; + } + if (i == WOLFIP_UDP_MCAST_MEMBERSHIPS) + return -WOLFIP_ENOMEM; + + m = mcast_membership_find(s, if_idx, group); + if (!m) { + for (j = 0; j < WOLFIP_MCAST_MEMBERSHIPS; j++) { + if (s->mcast[j].refs == 0) { + m = &s->mcast[j]; + m->group = group; + m->if_idx = (uint8_t)if_idx; + break; + } + } + } + if (!m) + return -WOLFIP_ENOMEM; + + ts->sock.udp.mcast[i].group = group; + ts->sock.udp.mcast[i].if_idx = (uint8_t)if_idx; + if (m->refs == 0) + (void)igmp_send_report(s, if_idx, group, IGMPV3_REC_MODE_IS_EXCLUDE); + if (m->refs != 0xff) + m->refs++; + return 0; +} + +static int udp_mcast_drop(struct wolfIP *s, struct tsocket *ts, ip4 group, + unsigned int if_idx) +{ + unsigned int i; + struct wolfIP_mcast_membership *m; + + if (!s || !ts || !wolfIP_ip_is_multicast(group) || if_idx >= s->if_count) + return -WOLFIP_EINVAL; + for (i = 0; i < WOLFIP_UDP_MCAST_MEMBERSHIPS; i++) { + if (ts->sock.udp.mcast[i].group == group && + ts->sock.udp.mcast[i].if_idx == if_idx) + break; + } + if (i == WOLFIP_UDP_MCAST_MEMBERSHIPS) + return -WOLFIP_EINVAL; + ts->sock.udp.mcast[i].group = IPADDR_ANY; + ts->sock.udp.mcast[i].if_idx = 0; + + m = mcast_membership_find(s, if_idx, group); + if (m && m->refs > 0) { + m->refs--; + if (m->refs == 0) { + (void)igmp_send_report(s, if_idx, group, + IGMPV3_REC_CHANGE_TO_INCLUDE); + memset(m, 0, sizeof(*m)); + } + } + return 0; +} + +static void udp_mcast_drop_all(struct tsocket *ts) +{ + unsigned int i; + + if (!ts || !ts->S) + return; + for (i = 0; i < WOLFIP_UDP_MCAST_MEMBERSHIPS; i++) { + if (ts->sock.udp.mcast[i].group != IPADDR_ANY) { + (void)udp_mcast_drop(ts->S, ts, ts->sock.udp.mcast[i].group, + ts->sock.udp.mcast[i].if_idx); + } + } +} +#endif + int wolfIP_sock_setsockopt(struct wolfIP *s, int sockfd, int level, int optname, const void *optval, socklen_t optlen) { @@ -5937,6 +6320,80 @@ int wolfIP_sock_setsockopt(struct wolfIP *s, int sockfd, int level, int optname, ts->recv_ttl = enable ? 1 : 0; return 0; } +#ifdef IP_MULTICAST + if (level == WOLFIP_SOL_IP && IS_SOCKET_UDP(sockfd)) { + if (optname == WOLFIP_IP_ADD_MEMBERSHIP || + optname == WOLFIP_IP_DROP_MEMBERSHIP) { + const struct wolfIP_ip_mreq *mreq = + (const struct wolfIP_ip_mreq *)optval; + unsigned int if_idx; + ip4 group; + ip4 if_addr; + int ret; + + if (!mreq || optlen < (socklen_t)sizeof(*mreq)) + return -WOLFIP_EINVAL; + group = ee32(mreq->imr_multiaddr.s_addr); + if_addr = ee32(mreq->imr_interface.s_addr); + ret = mcast_if_from_addr(s, if_addr, group, &if_idx); + if (ret < 0) + return ret; + if (optname == WOLFIP_IP_ADD_MEMBERSHIP) + return udp_mcast_join(s, ts, group, if_idx); + return udp_mcast_drop(s, ts, group, if_idx); + } + if (optname == WOLFIP_IP_MULTICAST_IF) { + const struct wolfIP_mreq_addr *addr = + (const struct wolfIP_mreq_addr *)optval; + unsigned int if_idx; + ip4 if_addr; + int ret; + + if (!addr || optlen < (socklen_t)sizeof(*addr)) + return -WOLFIP_EINVAL; + if_addr = ee32(addr->s_addr); + ret = mcast_if_from_addr(s, if_addr, IGMP_ALL_HOSTS, &if_idx); + if (ret < 0) + return ret; + ts->sock.udp.mcast_if_idx = (uint8_t)if_idx; + ts->sock.udp.mcast_if_set = 1; + return 0; + } + if (optname == WOLFIP_IP_MULTICAST_TTL) { + uint8_t ttl8; + int ttl; + + if (!optval || optlen == 0) + return -WOLFIP_EINVAL; + if (optlen >= (socklen_t)sizeof(int)) { + memcpy(&ttl, optval, sizeof(ttl)); + if (ttl < 0 || ttl > 255) + return -WOLFIP_EINVAL; + ttl8 = (uint8_t)ttl; + } else { + memcpy(&ttl8, optval, sizeof(ttl8)); + } + ts->sock.udp.mcast_ttl = ttl8; + return 0; + } + if (optname == WOLFIP_IP_MULTICAST_LOOP) { + uint8_t loop8; + int loop; + + if (!optval || optlen == 0) + return -WOLFIP_EINVAL; + if (optlen >= (socklen_t)sizeof(int)) { + memcpy(&loop, optval, sizeof(loop)); + loop8 = loop ? 1U : 0U; + } else { + memcpy(&loop8, optval, sizeof(loop8)); + loop8 = loop8 ? 1U : 0U; + } + ts->sock.udp.mcast_loop = loop8; + return 0; + } + } +#endif return 0; } @@ -6024,6 +6481,37 @@ int wolfIP_sock_getsockopt(struct wolfIP *s, int sockfd, int level, int optname, } return 0; } +#ifdef IP_MULTICAST + if (level == WOLFIP_SOL_IP && IS_SOCKET_UDP(sockfd)) { + if (optname == WOLFIP_IP_MULTICAST_TTL || + optname == WOLFIP_IP_MULTICAST_LOOP) { + int value; + + if (!optval || !optlen || *optlen < (socklen_t)sizeof(int)) + return -WOLFIP_EINVAL; + value = (optname == WOLFIP_IP_MULTICAST_TTL) ? + ts->sock.udp.mcast_ttl : ts->sock.udp.mcast_loop; + memcpy(optval, &value, sizeof(value)); + *optlen = sizeof(value); + return 0; + } + if (optname == WOLFIP_IP_MULTICAST_IF) { + struct wolfIP_mreq_addr addr; + struct ipconf *conf; + unsigned int if_idx; + + if (!optval || !optlen || *optlen < (socklen_t)sizeof(addr)) + return -WOLFIP_EINVAL; + if_idx = ts->sock.udp.mcast_if_set ? + ts->sock.udp.mcast_if_idx : wolfIP_socket_if_idx(ts); + conf = wolfIP_ipconf_at(s, if_idx); + addr.s_addr = ee32(conf ? conf->ip : IPADDR_ANY); + memcpy(optval, &addr, sizeof(addr)); + *optlen = sizeof(addr); + return 0; + } + } +#endif return 0; } int wolfIP_sock_close(struct wolfIP *s, int sockfd) @@ -7876,6 +8364,11 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, else if (dispatch_ip->proto == 0x01) { icmp_input(s, if_idx, dispatch_ip, dispatch_len); } +#ifdef IP_MULTICAST + else if (dispatch_ip->proto == WI_IPPROTO_IGMP) { + igmp_input(s, if_idx, dispatch_ip, dispatch_len); + } +#endif #ifdef DEBUG_IP else { LOG("info: dropping ip packet: 0x%02x\n", dispatch_ip->proto); @@ -7915,8 +8408,19 @@ static void wolfIP_recv_on(struct wolfIP *s, unsigned int if_idx, void *buf, uin #endif if (eth->type == ee16(ETH_TYPE_IP)) { struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)eth; - if ((memcmp(eth->dst, ll->mac, 6) != 0) && (memcmp(eth->dst, "\xff\xff\xff\xff\xff\xff", 6) != 0)) { + if ((memcmp(eth->dst, ll->mac, 6) != 0) && + (memcmp(eth->dst, "\xff\xff\xff\xff\xff\xff", 6) != 0)) { +#ifdef IP_MULTICAST + ip4 dst_ip = ee32(ip->dst); + if (!eth_is_ipv4_multicast_mac(eth->dst) || + !wolfIP_ip_is_multicast(dst_ip) || + (!mcast_is_joined(s, if_idx, dst_ip) && + dst_ip != IGMPV3_REPORT_DST && dst_ip != IGMP_ALL_HOSTS)) { + return; /* Not for us */ + } +#else return; /* Not for us */ +#endif } ip_recv(s, if_idx, ip, len); } else if (eth->type == ee16(ETH_TYPE_ARP)) { @@ -8702,6 +9206,11 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) if (loop) memcpy(t->nexthop_mac, loop->mac, 6); } else if (!wolfIP_ll_is_non_ethernet(s, tx_if)) { +#ifdef IP_MULTICAST + if (wolfIP_ip_is_multicast(t->remote_ip)) { + mcast_ip_to_eth(t->remote_ip, t->nexthop_mac); + } else +#endif if ((!wolfIP_ip_is_broadcast(s, nexthop) && (arp_lookup(s, tx_if, nexthop, t->nexthop_mac) < 0))) { /* Send ARP request */ @@ -8714,6 +9223,11 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) #endif len = desc->len - ETH_HEADER_LEN; ip_output_add_header(t, (struct wolfIP_ip_packet *)udp, WI_IPPROTO_UDP, len); +#ifdef IP_MULTICAST + if (wolfIP_ip_is_multicast(t->remote_ip) && t->sock.udp.mcast_loop) { + udp_try_recv(s, tx_if, udp, desc->len); + } +#endif if (wolfIP_filter_notify_udp(WOLFIP_FILT_SENDING, t->S, tx_if, udp, desc->len) != 0) break; if (wolfIP_filter_notify_ip(WOLFIP_FILT_SENDING, t->S, tx_if, &udp->ip, desc->len) != 0) diff --git a/wolfip.h b/wolfip.h index 2128b376..03b24eba 100644 --- a/wolfip.h +++ b/wolfip.h @@ -72,6 +72,48 @@ typedef unsigned long size_t; #endif #endif +#ifdef IP_MULTICAST +#ifndef WOLFIP_IP_ADD_MEMBERSHIP +#ifdef IP_ADD_MEMBERSHIP +#define WOLFIP_IP_ADD_MEMBERSHIP IP_ADD_MEMBERSHIP +#else +#define WOLFIP_IP_ADD_MEMBERSHIP 35 +#endif +#endif + +#ifndef WOLFIP_IP_DROP_MEMBERSHIP +#ifdef IP_DROP_MEMBERSHIP +#define WOLFIP_IP_DROP_MEMBERSHIP IP_DROP_MEMBERSHIP +#else +#define WOLFIP_IP_DROP_MEMBERSHIP 36 +#endif +#endif + +#ifndef WOLFIP_IP_MULTICAST_IF +#ifdef IP_MULTICAST_IF +#define WOLFIP_IP_MULTICAST_IF IP_MULTICAST_IF +#else +#define WOLFIP_IP_MULTICAST_IF 32 +#endif +#endif + +#ifndef WOLFIP_IP_MULTICAST_TTL +#ifdef IP_MULTICAST_TTL +#define WOLFIP_IP_MULTICAST_TTL IP_MULTICAST_TTL +#else +#define WOLFIP_IP_MULTICAST_TTL 33 +#endif +#endif + +#ifndef WOLFIP_IP_MULTICAST_LOOP +#ifdef IP_MULTICAST_LOOP +#define WOLFIP_IP_MULTICAST_LOOP IP_MULTICAST_LOOP +#else +#define WOLFIP_IP_MULTICAST_LOOP 34 +#endif +#endif +#endif /* IP_MULTICAST */ + /* Types */ struct wolfIP; typedef uint32_t ip4; @@ -145,6 +187,17 @@ struct ipconf { ip4 gw; }; +#ifdef IP_MULTICAST +struct wolfIP_mreq_addr { + uint32_t s_addr; +}; + +struct wolfIP_ip_mreq { + struct wolfIP_mreq_addr imr_multiaddr; + struct wolfIP_mreq_addr imr_interface; +}; +#endif + /* Socket interface */ #define MARK_TCP_SOCKET 0x100 /* Mark a socket as TCP */ #define MARK_UDP_SOCKET 0x200 /* Mark a socket as UDP */ From b8e9fcdb2fcfbefeaa07f214e90bf60c86f9e9d0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 22 Apr 2026 12:46:36 +0200 Subject: [PATCH 2/6] Addressed copilot's comment --- src/test/unit/unit.c | 2 + src/test/unit/unit_tests_multicast.c | 126 +++++++++++++++++++++++++++ src/wolfip.c | 14 ++- 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 3bcf35f3..79108b6d 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -234,6 +234,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_multicast_udp_receive_requires_join); tcase_add_test(tc_utils, test_multicast_udp_send_mac_ttl_loop_and_options); tcase_add_test(tc_utils, test_multicast_igmp_query_refreshes_report); + tcase_add_test(tc_utils, test_multicast_join_requires_configured_ip); + tcase_add_test(tc_utils, test_multicast_if_pins_egress_interface); #endif tcase_add_test(tc_utils, test_tcp_no_rst_for_broadcast_dst); tcase_add_test(tc_utils, test_tcp_no_rst_for_multicast_dst); diff --git a/src/test/unit/unit_tests_multicast.c b/src/test/unit/unit_tests_multicast.c index 1a9902a9..0c14fa4a 100644 --- a/src/test/unit/unit_tests_multicast.c +++ b/src/test/unit/unit_tests_multicast.c @@ -252,4 +252,130 @@ START_TEST(test_multicast_igmp_query_refreshes_report) } END_TEST +START_TEST(test_multicast_join_requires_configured_ip) +{ + struct wolfIP s; + int sd; + struct wolfIP_ip_mreq mreq; + ip4 group = 0xE9020101U; + + /* No wolfIP_ipconfig_set on primary: interface has no source IP. */ + wolfIP_init(&s); + mock_link_init(&s); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + + /* Join via IPADDR_ANY must fail when the route-selected interface has no + * configured source IP: otherwise the join would be recorded but the + * IGMP report could never be built, announcing membership only locally. */ + multicast_mreq(&mreq, group, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), -WOLFIP_EINVAL); + ck_assert_uint_eq(s.mcast[0].refs, 0); + + /* Once the interface has a source IP, the same join succeeds and a + * report is emitted. */ + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + last_frame_sent_size = 0; + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + ck_assert_uint_eq(s.mcast[0].refs, 1); + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + 9], WI_IPPROTO_IGMP); +} +END_TEST + +START_TEST(test_multicast_if_pins_egress_interface) +{ + struct wolfIP s; + int sd; + struct tsocket *ts; + struct wolfIP_mreq_addr addr; + struct wolfIP_mreq_addr got; + socklen_t gotlen = sizeof(got); + struct wolfIP_sockaddr_in bind_addr; + struct wolfIP_sockaddr_in dst; + uint8_t primary_mac[6]; + uint8_t secondary_mac[6]; + struct wolfIP_ll_dev *ll_primary; + struct wolfIP_ll_dev *ll_secondary; + ip4 primary_ip = 0x0A000002U; /* 10.0.0.2/24 */ + ip4 secondary_ip = 0x0A000102U; /* 10.0.1.2/24 */ + ip4 group = 0xEF010203U; /* 239.1.2.3 */ + const char payload[] = "if"; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + ll_primary = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ll_secondary = wolfIP_getdev_ex(&s, TEST_SECOND_IF); + ck_assert_ptr_nonnull(ll_primary); + ck_assert_ptr_nonnull(ll_secondary); + memcpy(primary_mac, ll_primary->mac, 6); + memcpy(secondary_mac, ll_secondary->mac, 6); + + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + ts = &s.udpsockets[SOCKET_UNMARK(sd)]; + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = ee16(5002); + ck_assert_int_eq(wolfIP_sock_bind(&s, sd, + (struct wolfIP_sockaddr *)&bind_addr, sizeof(bind_addr)), 0); + + /* Pin egress to the secondary interface. */ + memset(&addr, 0, sizeof(addr)); + addr.s_addr = ee32(secondary_ip); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_IF, &addr, sizeof(addr)), 0); + ck_assert_uint_eq(ts->sock.udp.mcast_if_set, 1); + ck_assert_uint_eq(ts->sock.udp.mcast_if_idx, TEST_SECOND_IF); + + /* getsockopt reports the address of the pinned interface. */ + memset(&got, 0, sizeof(got)); + ck_assert_int_eq(wolfIP_sock_getsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_IF, &got, &gotlen), 0); + ck_assert_uint_eq(ee32(got.s_addr), secondary_ip); + + /* A multicast sendto must egress on the secondary interface — verify via + * the source MAC of the transmitted frame (mock_send is shared across + * interfaces but eth_output_add_header uses the egress dev's MAC). */ + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = ee16(5002); + dst.sin_addr.s_addr = ee32(group); + last_frame_sent_size = 0; + ck_assert_int_eq(wolfIP_sock_sendto(&s, sd, payload, sizeof(payload), 0, + (struct wolfIP_sockaddr *)&dst, sizeof(dst)), + (int)sizeof(payload)); + ck_assert_int_eq(wolfIP_poll(&s, 1), 0); + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_mem_eq(last_frame_sent + 6, secondary_mac, 6); + + /* Clearing with INADDR_ANY reverts to per-destination routing (Linux + * IP_MULTICAST_IF semantics). */ + memset(&addr, 0, sizeof(addr)); + addr.s_addr = ee32(IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_IF, &addr, sizeof(addr)), 0); + ck_assert_uint_eq(ts->sock.udp.mcast_if_set, 0); + ck_assert_uint_eq(ts->sock.udp.mcast_if_idx, 0); + + /* Next multicast sendto goes via the default route — primary interface. */ + last_frame_sent_size = 0; + ck_assert_int_eq(wolfIP_sock_sendto(&s, sd, payload, sizeof(payload), 0, + (struct wolfIP_sockaddr *)&dst, sizeof(dst)), + (int)sizeof(payload)); + ck_assert_int_eq(wolfIP_poll(&s, 1), 0); + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_mem_eq(last_frame_sent + 6, primary_mac, 6); + + /* After clearing, getsockopt falls back to the socket's current interface + * (which is the primary route for the previous sendto). */ + gotlen = sizeof(got); + memset(&got, 0, sizeof(got)); + ck_assert_int_eq(wolfIP_sock_getsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_IF, &got, &gotlen), 0); + ck_assert_uint_eq(ee32(got.s_addr), primary_ip); +} +END_TEST + #endif /* IP_MULTICAST */ diff --git a/src/wolfip.c b/src/wolfip.c index a1cef85d..911de5e2 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -6173,13 +6173,18 @@ int wolfIP_sock_read(struct wolfIP *s, int sockfd, void *buf, size_t len) static int mcast_if_from_addr(struct wolfIP *s, ip4 if_addr, ip4 group, unsigned int *if_idx) { + struct ipconf *conf; int found = 0; if (!s || !if_idx || !wolfIP_ip_is_multicast(group)) return -WOLFIP_EINVAL; if (if_addr == IPADDR_ANY) { *if_idx = wolfIP_route_for_ip(s, group); - if (wolfIP_ipconf_at(s, *if_idx)) + conf = wolfIP_ipconf_at(s, *if_idx); + /* Require a configured source IP so igmp_send_report can build a + * valid report; otherwise the join would succeed locally but never + * be announced on the wire. */ + if (conf && conf->ip != IPADDR_ANY) return 0; return -WOLFIP_EINVAL; } @@ -6352,6 +6357,13 @@ int wolfIP_sock_setsockopt(struct wolfIP *s, int sockfd, int level, int optname, if (!addr || optlen < (socklen_t)sizeof(*addr)) return -WOLFIP_EINVAL; if_addr = ee32(addr->s_addr); + /* Linux IP_MULTICAST_IF with INADDR_ANY clears the pinned + * interface and reverts to per-destination routing. */ + if (if_addr == IPADDR_ANY) { + ts->sock.udp.mcast_if_set = 0; + ts->sock.udp.mcast_if_idx = 0; + return 0; + } ret = mcast_if_from_addr(s, if_addr, IGMP_ALL_HOSTS, &if_idx); if (ret < 0) return ret; From 2311a05a9c9ebdf72b225aa15cd17400067f9401 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 22 Apr 2026 12:49:28 +0200 Subject: [PATCH 3/6] Added multicast interop tests --- .github/workflows/multicast-interop.yml | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/multicast-interop.yml diff --git a/.github/workflows/multicast-interop.yml b/.github/workflows/multicast-interop.yml new file mode 100644 index 00000000..4e3ee4cd --- /dev/null +++ b/.github/workflows/multicast-interop.yml @@ -0,0 +1,59 @@ +name: Multicast interop tests + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +jobs: + multicast: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential check gcovr libwolfssl-dev pkg-config + sudo modprobe tun + + - name: Build and run default unit tests + run: | + set -euo pipefail + make clean-unit unit + timeout --preserve-status 5m ./build/test/unit + + - name: Build and run multicast unit tests + run: | + set -euo pipefail + make unit-multicast + timeout --preserve-status 5m ./build/test/unit + + - name: Build multicast TAP interop + run: | + set -euo pipefail + make build/test-multicast-interop + + - name: Run multicast TAP interop + timeout-minutes: 3 + run: | + set -euo pipefail + timeout --preserve-status 3m sudo ./build/test-multicast-interop + + - name: Multicast coverage + run: | + set -euo pipefail + make clean-unit autocov-multicast + gcovr -r . --exclude "src/test/unit/.*" --json -o build/coverage/multicast.json + gcovr -r . --exclude "src/test/unit/.*" --txt-metric branch + + - name: Cleanup TAP state + if: always() + run: | + sudo ip link del wmcast0 2>/dev/null || true + sudo killall tcpdump 2>/dev/null || true From 39877a0fb3b775360017688003e17ce88a3bd230 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 22 Apr 2026 12:54:29 +0200 Subject: [PATCH 4/6] slim down multicast tests in github action --- .github/workflows/multicast-interop.yml | 28 +++++-------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/.github/workflows/multicast-interop.yml b/.github/workflows/multicast-interop.yml index 4e3ee4cd..ee2a196e 100644 --- a/.github/workflows/multicast-interop.yml +++ b/.github/workflows/multicast-interop.yml @@ -19,41 +19,23 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y build-essential check gcovr libwolfssl-dev pkg-config + sudo apt-get install -y build-essential check libwolfssl-dev pkg-config sudo modprobe tun - - name: Build and run default unit tests - run: | - set -euo pipefail - make clean-unit unit - timeout --preserve-status 5m ./build/test/unit - - name: Build and run multicast unit tests + timeout-minutes: 5 run: | set -euo pipefail make unit-multicast timeout --preserve-status 5m ./build/test/unit - - name: Build multicast TAP interop - run: | - set -euo pipefail - make build/test-multicast-interop - - - name: Run multicast TAP interop + - name: Build and run multicast TAP interop timeout-minutes: 3 run: | set -euo pipefail + make build/test-multicast-interop timeout --preserve-status 3m sudo ./build/test-multicast-interop - - name: Multicast coverage - run: | - set -euo pipefail - make clean-unit autocov-multicast - gcovr -r . --exclude "src/test/unit/.*" --json -o build/coverage/multicast.json - gcovr -r . --exclude "src/test/unit/.*" --txt-metric branch - - name: Cleanup TAP state if: always() - run: | - sudo ip link del wmcast0 2>/dev/null || true - sudo killall tcpdump 2>/dev/null || true + run: sudo ip link del wmcast0 2>/dev/null || true From 6792420de0a1deaec5d632c3ff418b9a0680abbe Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 22 Apr 2026 13:11:44 +0200 Subject: [PATCH 5/6] Addressed two fenrir comments --- src/test/unit/unit.c | 2 + src/test/unit/unit_tests_multicast.c | 93 ++++++++++++++++++++++++++++ src/wolfip.c | 22 +++++-- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 79108b6d..e6106456 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -236,6 +236,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_multicast_igmp_query_refreshes_report); tcase_add_test(tc_utils, test_multicast_join_requires_configured_ip); tcase_add_test(tc_utils, test_multicast_if_pins_egress_interface); + tcase_add_test(tc_utils, test_multicast_loop_does_not_fire_on_blocked_send); + tcase_add_test(tc_utils, test_multicast_recv_rejects_short_frame); #endif tcase_add_test(tc_utils, test_tcp_no_rst_for_broadcast_dst); tcase_add_test(tc_utils, test_tcp_no_rst_for_multicast_dst); diff --git a/src/test/unit/unit_tests_multicast.c b/src/test/unit/unit_tests_multicast.c index 0c14fa4a..f068c1ab 100644 --- a/src/test/unit/unit_tests_multicast.c +++ b/src/test/unit/unit_tests_multicast.c @@ -378,4 +378,97 @@ START_TEST(test_multicast_if_pins_egress_interface) } END_TEST +START_TEST(test_multicast_loop_does_not_fire_on_blocked_send) +{ + struct wolfIP s; + int sd; + struct wolfIP_sockaddr_in bind_addr; + struct wolfIP_sockaddr_in dst; + struct wolfIP_ip_mreq mreq; + uint8_t out[8]; + int loop = 1; + ip4 group = 0xE9010208U; + const char payload[] = "xy"; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = ee16(5003); + ck_assert_int_eq(wolfIP_sock_bind(&s, sd, + (struct wolfIP_sockaddr *)&bind_addr, sizeof(bind_addr)), 0); + + multicast_mreq(&mreq, group, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_LOOP, &loop, sizeof(loop)), 0); + + /* Block the egress UDP path. With the pre-fix code the mcast_loop + * udp_try_recv() ran before the filter, so the local RX queue got one + * copy per poll even though the frame was never transmitted; with the + * fix, a blocked send must not deliver a loopback copy. */ + filter_block_reason = WOLFIP_FILT_SENDING; + wolfIP_filter_set_callback(test_filter_cb_block, NULL); + wolfIP_filter_set_udp_mask(WOLFIP_FILT_MASK(WOLFIP_FILT_SENDING)); + + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = ee16(5003); + dst.sin_addr.s_addr = ee32(group); + last_frame_sent_size = 0; + ck_assert_int_eq(wolfIP_sock_sendto(&s, sd, payload, sizeof(payload), 0, + (struct wolfIP_sockaddr *)&dst, sizeof(dst)), + (int)sizeof(payload)); + + /* Poll twice: with the pre-fix code the descriptor sticks in the txbuf + * and each poll re-loops the datagram, so recvfrom would return it. */ + (void)wolfIP_poll(&s, 1); + (void)wolfIP_poll(&s, 2); + ck_assert_uint_eq(last_frame_sent_size, 0U); + ck_assert_int_eq(wolfIP_sock_recvfrom(&s, sd, out, sizeof(out), 0, NULL, + NULL), -WOLFIP_EAGAIN); + + /* Clearing the filter lets the next poll send and loop exactly once. */ + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_udp_mask(0); + (void)wolfIP_poll(&s, 3); + ck_assert_uint_gt(last_frame_sent_size, 0U); + ck_assert_int_eq(wolfIP_sock_recvfrom(&s, sd, out, sizeof(out), 0, NULL, + NULL), (int)sizeof(payload)); + /* Only one loopback copy — no leftover. */ + ck_assert_int_eq(wolfIP_sock_recvfrom(&s, sd, out, sizeof(out), 0, NULL, + NULL), -WOLFIP_EAGAIN); +} +END_TEST + +START_TEST(test_multicast_recv_rejects_short_frame) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN]; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + + /* Build a 14-byte frame (Ethernet header only) with a multicast-looking + * destination MAC and ETH_TYPE_IP. Without the length guard, + * wolfIP_recv_on would read ip->dst at offsets 30-33 — well past the + * end of the buffer. Under ASAN this is a heap-buffer-overflow. */ + memset(frame, 0, sizeof(frame)); + memcpy(frame + 0, "\x01\x00\x5e\x01\x02\x08", 6); + memcpy(frame + 6, "\x02\x00\x00\x00\x00\x01", 6); + frame[12] = (ETH_TYPE_IP >> 8) & 0xff; + frame[13] = ETH_TYPE_IP & 0xff; + + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); + /* Silently dropped, no response sent. */ + ck_assert_uint_eq(last_frame_sent_size, 0U); +} +END_TEST + #endif /* IP_MULTICAST */ diff --git a/src/wolfip.c b/src/wolfip.c index 911de5e2..4b1792df 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -8423,7 +8423,12 @@ static void wolfIP_recv_on(struct wolfIP *s, unsigned int if_idx, void *buf, uin if ((memcmp(eth->dst, ll->mac, 6) != 0) && (memcmp(eth->dst, "\xff\xff\xff\xff\xff\xff", 6) != 0)) { #ifdef IP_MULTICAST - ip4 dst_ip = ee32(ip->dst); + ip4 dst_ip; + /* Guard the read of ip->dst (bytes 30-33) against short frames + * from drivers that don't pad to 60 bytes. */ + if (len < (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN)) + return; + dst_ip = ee32(ip->dst); if (!eth_is_ipv4_multicast_mac(eth->dst) || !wolfIP_ip_is_multicast(dst_ip) || (!mcast_is_joined(s, if_idx, dst_ip) && @@ -9235,11 +9240,6 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) #endif len = desc->len - ETH_HEADER_LEN; ip_output_add_header(t, (struct wolfIP_ip_packet *)udp, WI_IPPROTO_UDP, len); -#ifdef IP_MULTICAST - if (wolfIP_ip_is_multicast(t->remote_ip) && t->sock.udp.mcast_loop) { - udp_try_recv(s, tx_if, udp, desc->len); - } -#endif if (wolfIP_filter_notify_udp(WOLFIP_FILT_SENDING, t->S, tx_if, udp, desc->len) != 0) break; if (wolfIP_filter_notify_ip(WOLFIP_FILT_SENDING, t->S, tx_if, &udp->ip, desc->len) != 0) @@ -9273,6 +9273,16 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) break; if (send_ret < 0) break; +#ifdef IP_MULTICAST + /* Loopback only after a successful wire send. Running udp_try_recv + * before the filter/send path caused repeated local deliveries + * when a SENDING filter blocked the frame or the driver returned + * -EAGAIN: the descriptor stays in the txbuf and every subsequent + * wolfIP_poll() re-enters the loop and re-loops the datagram. */ + if (wolfIP_ip_is_multicast(t->remote_ip) && t->sock.udp.mcast_loop) { + udp_try_recv(s, tx_if, udp, desc->len); + } +#endif fifo_pop(&t->sock.udp.txbuf); desc = fifo_peek(&t->sock.udp.txbuf); } From f9982fd6bad25df11457bcb2b47dd979bbff7f4b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 22 Apr 2026 14:53:19 +0200 Subject: [PATCH 6/6] Addressed two more copilot's comments. --- src/test/unit/unit.c | 2 + src/test/unit/unit_tests_multicast.c | 100 +++++++++++++++++++++++++++ src/wolfip.c | 37 ++++++---- 3 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index e6106456..36452666 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -238,6 +238,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_multicast_if_pins_egress_interface); tcase_add_test(tc_utils, test_multicast_loop_does_not_fire_on_blocked_send); tcase_add_test(tc_utils, test_multicast_recv_rejects_short_frame); + tcase_add_test(tc_utils, test_multicast_setsockopt_accepts_unaligned_mreq); + tcase_add_test(tc_utils, test_multicast_getsockopt_ttl_loop_accepts_uint8); #endif tcase_add_test(tc_utils, test_tcp_no_rst_for_broadcast_dst); tcase_add_test(tc_utils, test_tcp_no_rst_for_multicast_dst); diff --git a/src/test/unit/unit_tests_multicast.c b/src/test/unit/unit_tests_multicast.c index f068c1ab..ab96baa3 100644 --- a/src/test/unit/unit_tests_multicast.c +++ b/src/test/unit/unit_tests_multicast.c @@ -471,4 +471,104 @@ START_TEST(test_multicast_recv_rejects_short_frame) } END_TEST +START_TEST(test_multicast_setsockopt_accepts_unaligned_mreq) +{ + struct wolfIP s; + int sd; + /* 1 byte of padding before the mreq so the struct lands at odd alignment + * even on strict-alignment toolchains. A direct (const struct *)cast on + * this pointer would be an unaligned uint32_t load; UBSAN + * -fsanitize=alignment flags that as undefined behaviour. */ + uint8_t raw[1 + sizeof(struct wolfIP_ip_mreq)]; + struct wolfIP_ip_mreq mreq_native; + uint8_t raw_if[1 + sizeof(struct wolfIP_mreq_addr)]; + struct wolfIP_mreq_addr if_native; + ip4 group = 0xE9010209U; + ip4 iface_ip = 0x0A000002U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, iface_ip, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + + /* IP_ADD_MEMBERSHIP via a misaligned mreq. */ + multicast_mreq(&mreq_native, group, IPADDR_ANY); + memset(raw, 0, sizeof(raw)); + memcpy(raw + 1, &mreq_native, sizeof(mreq_native)); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, raw + 1, + (socklen_t)sizeof(mreq_native)), 0); + ck_assert_uint_eq(s.mcast[0].refs, 1); + + /* IP_DROP_MEMBERSHIP via a misaligned mreq. */ + memset(raw, 0, sizeof(raw)); + memcpy(raw + 1, &mreq_native, sizeof(mreq_native)); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_DROP_MEMBERSHIP, raw + 1, + (socklen_t)sizeof(mreq_native)), 0); + ck_assert_uint_eq(s.mcast[0].refs, 0); + + /* IP_MULTICAST_IF via a misaligned wolfIP_mreq_addr. */ + if_native.s_addr = ee32(iface_ip); + memset(raw_if, 0, sizeof(raw_if)); + memcpy(raw_if + 1, &if_native, sizeof(if_native)); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_IF, raw_if + 1, + (socklen_t)sizeof(if_native)), 0); + ck_assert_uint_eq(s.udpsockets[SOCKET_UNMARK(sd)].sock.udp.mcast_if_set, 1); +} +END_TEST + +START_TEST(test_multicast_getsockopt_ttl_loop_accepts_uint8) +{ + struct wolfIP s; + int sd; + int ttl_in = 7; + int loop_in = 0; + uint8_t byte_buf; + int int_buf; + socklen_t len; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_TTL, &ttl_in, sizeof(ttl_in)), 0); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_LOOP, &loop_in, sizeof(loop_in)), 0); + + /* getsockopt with a uint8_t buffer must succeed and write one byte, + * mirroring setsockopt which already accepts either size. */ + byte_buf = 0xff; + len = sizeof(byte_buf); + ck_assert_int_eq(wolfIP_sock_getsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_TTL, &byte_buf, &len), 0); + ck_assert_uint_eq(len, sizeof(byte_buf)); + ck_assert_uint_eq(byte_buf, 7U); + + byte_buf = 0xff; + len = sizeof(byte_buf); + ck_assert_int_eq(wolfIP_sock_getsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_LOOP, &byte_buf, &len), 0); + ck_assert_uint_eq(len, sizeof(byte_buf)); + ck_assert_uint_eq(byte_buf, 0U); + + /* The larger int path still works and returns sizeof(int). */ + int_buf = -1; + len = sizeof(int_buf); + ck_assert_int_eq(wolfIP_sock_getsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_TTL, &int_buf, &len), 0); + ck_assert_uint_eq(len, sizeof(int_buf)); + ck_assert_int_eq(int_buf, 7); + + /* Zero-length buffer is still rejected. */ + len = 0; + ck_assert_int_eq(wolfIP_sock_getsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_MULTICAST_TTL, &byte_buf, &len), -WOLFIP_EINVAL); +} +END_TEST + #endif /* IP_MULTICAST */ diff --git a/src/wolfip.c b/src/wolfip.c index 4b1792df..b7771d7b 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -6329,17 +6329,20 @@ int wolfIP_sock_setsockopt(struct wolfIP *s, int sockfd, int level, int optname, if (level == WOLFIP_SOL_IP && IS_SOCKET_UDP(sockfd)) { if (optname == WOLFIP_IP_ADD_MEMBERSHIP || optname == WOLFIP_IP_DROP_MEMBERSHIP) { - const struct wolfIP_ip_mreq *mreq = - (const struct wolfIP_ip_mreq *)optval; + struct wolfIP_ip_mreq mreq; unsigned int if_idx; ip4 group; ip4 if_addr; int ret; - if (!mreq || optlen < (socklen_t)sizeof(*mreq)) + /* Copy into an aligned local to avoid unaligned 32-bit loads on + * strict-alignment targets when the caller's optval buffer is + * not naturally aligned. */ + if (!optval || optlen < (socklen_t)sizeof(mreq)) return -WOLFIP_EINVAL; - group = ee32(mreq->imr_multiaddr.s_addr); - if_addr = ee32(mreq->imr_interface.s_addr); + memcpy(&mreq, optval, sizeof(mreq)); + group = ee32(mreq.imr_multiaddr.s_addr); + if_addr = ee32(mreq.imr_interface.s_addr); ret = mcast_if_from_addr(s, if_addr, group, &if_idx); if (ret < 0) return ret; @@ -6348,15 +6351,15 @@ int wolfIP_sock_setsockopt(struct wolfIP *s, int sockfd, int level, int optname, return udp_mcast_drop(s, ts, group, if_idx); } if (optname == WOLFIP_IP_MULTICAST_IF) { - const struct wolfIP_mreq_addr *addr = - (const struct wolfIP_mreq_addr *)optval; + struct wolfIP_mreq_addr addr; unsigned int if_idx; ip4 if_addr; int ret; - if (!addr || optlen < (socklen_t)sizeof(*addr)) + if (!optval || optlen < (socklen_t)sizeof(addr)) return -WOLFIP_EINVAL; - if_addr = ee32(addr->s_addr); + memcpy(&addr, optval, sizeof(addr)); + if_addr = ee32(addr.s_addr); /* Linux IP_MULTICAST_IF with INADDR_ANY clears the pinned * interface and reverts to per-destination routing. */ if (if_addr == IPADDR_ANY) { @@ -6499,12 +6502,22 @@ int wolfIP_sock_getsockopt(struct wolfIP *s, int sockfd, int level, int optname, optname == WOLFIP_IP_MULTICAST_LOOP) { int value; - if (!optval || !optlen || *optlen < (socklen_t)sizeof(int)) + if (!optval || !optlen || *optlen < (socklen_t)sizeof(uint8_t)) return -WOLFIP_EINVAL; value = (optname == WOLFIP_IP_MULTICAST_TTL) ? ts->sock.udp.mcast_ttl : ts->sock.udp.mcast_loop; - memcpy(optval, &value, sizeof(value)); - *optlen = sizeof(value); + /* Linux get_ip_sockopt writes IP_MULTICAST_TTL/LOOP as int when + * the caller provided room for one and as a single byte when the + * buffer is exactly sizeof(uint8_t) — keeps get/set symmetric + * with setsockopt, which accepts either width. */ + if (*optlen >= (socklen_t)sizeof(value)) { + memcpy(optval, &value, sizeof(value)); + *optlen = sizeof(value); + } else { + uint8_t value8 = (uint8_t)value; + memcpy(optval, &value8, sizeof(value8)); + *optlen = sizeof(value8); + } return 0; } if (optname == WOLFIP_IP_MULTICAST_IF) {