@@ -252,4 +252,130 @@ START_TEST(test_multicast_igmp_query_refreshes_report)
252252}
253253END_TEST
254254
255+ START_TEST (test_multicast_join_requires_configured_ip )
256+ {
257+ struct wolfIP s ;
258+ int sd ;
259+ struct wolfIP_ip_mreq mreq ;
260+ ip4 group = 0xE9020101U ;
261+
262+ /* No wolfIP_ipconfig_set on primary: interface has no source IP. */
263+ wolfIP_init (& s );
264+ mock_link_init (& s );
265+ sd = wolfIP_sock_socket (& s , AF_INET , IPSTACK_SOCK_DGRAM , WI_IPPROTO_UDP );
266+ ck_assert_int_gt (sd , 0 );
267+
268+ /* Join via IPADDR_ANY must fail when the route-selected interface has no
269+ * configured source IP: otherwise the join would be recorded but the
270+ * IGMP report could never be built, announcing membership only locally. */
271+ multicast_mreq (& mreq , group , IPADDR_ANY );
272+ ck_assert_int_eq (wolfIP_sock_setsockopt (& s , sd , WOLFIP_SOL_IP ,
273+ WOLFIP_IP_ADD_MEMBERSHIP , & mreq , sizeof (mreq )), - WOLFIP_EINVAL );
274+ ck_assert_uint_eq (s .mcast [0 ].refs , 0 );
275+
276+ /* Once the interface has a source IP, the same join succeeds and a
277+ * report is emitted. */
278+ wolfIP_ipconfig_set (& s , 0x0A000002U , 0xFFFFFF00U , 0 );
279+ last_frame_sent_size = 0 ;
280+ ck_assert_int_eq (wolfIP_sock_setsockopt (& s , sd , WOLFIP_SOL_IP ,
281+ WOLFIP_IP_ADD_MEMBERSHIP , & mreq , sizeof (mreq )), 0 );
282+ ck_assert_uint_eq (s .mcast [0 ].refs , 1 );
283+ ck_assert_uint_gt (last_frame_sent_size , 0 );
284+ ck_assert_uint_eq (last_frame_sent [ETH_HEADER_LEN + 9 ], WI_IPPROTO_IGMP );
285+ }
286+ END_TEST
287+
288+ START_TEST (test_multicast_if_pins_egress_interface )
289+ {
290+ struct wolfIP s ;
291+ int sd ;
292+ struct tsocket * ts ;
293+ struct wolfIP_mreq_addr addr ;
294+ struct wolfIP_mreq_addr got ;
295+ socklen_t gotlen = sizeof (got );
296+ struct wolfIP_sockaddr_in bind_addr ;
297+ struct wolfIP_sockaddr_in dst ;
298+ uint8_t primary_mac [6 ];
299+ uint8_t secondary_mac [6 ];
300+ struct wolfIP_ll_dev * ll_primary ;
301+ struct wolfIP_ll_dev * ll_secondary ;
302+ ip4 primary_ip = 0x0A000002U ; /* 10.0.0.2/24 */
303+ ip4 secondary_ip = 0x0A000102U ; /* 10.0.1.2/24 */
304+ ip4 group = 0xEF010203U ; /* 239.1.2.3 */
305+ const char payload [] = "if" ;
306+
307+ setup_stack_with_two_ifaces (& s , primary_ip , secondary_ip );
308+ ll_primary = wolfIP_getdev_ex (& s , TEST_PRIMARY_IF );
309+ ll_secondary = wolfIP_getdev_ex (& s , TEST_SECOND_IF );
310+ ck_assert_ptr_nonnull (ll_primary );
311+ ck_assert_ptr_nonnull (ll_secondary );
312+ memcpy (primary_mac , ll_primary -> mac , 6 );
313+ memcpy (secondary_mac , ll_secondary -> mac , 6 );
314+
315+ sd = wolfIP_sock_socket (& s , AF_INET , IPSTACK_SOCK_DGRAM , WI_IPPROTO_UDP );
316+ ck_assert_int_gt (sd , 0 );
317+ ts = & s .udpsockets [SOCKET_UNMARK (sd )];
318+ memset (& bind_addr , 0 , sizeof (bind_addr ));
319+ bind_addr .sin_family = AF_INET ;
320+ bind_addr .sin_port = ee16 (5002 );
321+ ck_assert_int_eq (wolfIP_sock_bind (& s , sd ,
322+ (struct wolfIP_sockaddr * )& bind_addr , sizeof (bind_addr )), 0 );
323+
324+ /* Pin egress to the secondary interface. */
325+ memset (& addr , 0 , sizeof (addr ));
326+ addr .s_addr = ee32 (secondary_ip );
327+ ck_assert_int_eq (wolfIP_sock_setsockopt (& s , sd , WOLFIP_SOL_IP ,
328+ WOLFIP_IP_MULTICAST_IF , & addr , sizeof (addr )), 0 );
329+ ck_assert_uint_eq (ts -> sock .udp .mcast_if_set , 1 );
330+ ck_assert_uint_eq (ts -> sock .udp .mcast_if_idx , TEST_SECOND_IF );
331+
332+ /* getsockopt reports the address of the pinned interface. */
333+ memset (& got , 0 , sizeof (got ));
334+ ck_assert_int_eq (wolfIP_sock_getsockopt (& s , sd , WOLFIP_SOL_IP ,
335+ WOLFIP_IP_MULTICAST_IF , & got , & gotlen ), 0 );
336+ ck_assert_uint_eq (ee32 (got .s_addr ), secondary_ip );
337+
338+ /* A multicast sendto must egress on the secondary interface — verify via
339+ * the source MAC of the transmitted frame (mock_send is shared across
340+ * interfaces but eth_output_add_header uses the egress dev's MAC). */
341+ memset (& dst , 0 , sizeof (dst ));
342+ dst .sin_family = AF_INET ;
343+ dst .sin_port = ee16 (5002 );
344+ dst .sin_addr .s_addr = ee32 (group );
345+ last_frame_sent_size = 0 ;
346+ ck_assert_int_eq (wolfIP_sock_sendto (& s , sd , payload , sizeof (payload ), 0 ,
347+ (struct wolfIP_sockaddr * )& dst , sizeof (dst )),
348+ (int )sizeof (payload ));
349+ ck_assert_int_eq (wolfIP_poll (& s , 1 ), 0 );
350+ ck_assert_uint_gt (last_frame_sent_size , 0 );
351+ ck_assert_mem_eq (last_frame_sent + 6 , secondary_mac , 6 );
352+
353+ /* Clearing with INADDR_ANY reverts to per-destination routing (Linux
354+ * IP_MULTICAST_IF semantics). */
355+ memset (& addr , 0 , sizeof (addr ));
356+ addr .s_addr = ee32 (IPADDR_ANY );
357+ ck_assert_int_eq (wolfIP_sock_setsockopt (& s , sd , WOLFIP_SOL_IP ,
358+ WOLFIP_IP_MULTICAST_IF , & addr , sizeof (addr )), 0 );
359+ ck_assert_uint_eq (ts -> sock .udp .mcast_if_set , 0 );
360+ ck_assert_uint_eq (ts -> sock .udp .mcast_if_idx , 0 );
361+
362+ /* Next multicast sendto goes via the default route — primary interface. */
363+ last_frame_sent_size = 0 ;
364+ ck_assert_int_eq (wolfIP_sock_sendto (& s , sd , payload , sizeof (payload ), 0 ,
365+ (struct wolfIP_sockaddr * )& dst , sizeof (dst )),
366+ (int )sizeof (payload ));
367+ ck_assert_int_eq (wolfIP_poll (& s , 1 ), 0 );
368+ ck_assert_uint_gt (last_frame_sent_size , 0 );
369+ ck_assert_mem_eq (last_frame_sent + 6 , primary_mac , 6 );
370+
371+ /* After clearing, getsockopt falls back to the socket's current interface
372+ * (which is the primary route for the previous sendto). */
373+ gotlen = sizeof (got );
374+ memset (& got , 0 , sizeof (got ));
375+ ck_assert_int_eq (wolfIP_sock_getsockopt (& s , sd , WOLFIP_SOL_IP ,
376+ WOLFIP_IP_MULTICAST_IF , & got , & gotlen ), 0 );
377+ ck_assert_uint_eq (ee32 (got .s_addr ), primary_ip );
378+ }
379+ END_TEST
380+
255381#endif /* IP_MULTICAST */
0 commit comments