From 619a45308d8b662b619cdbcac66aab5545096a56 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 21 Apr 2026 06:16:23 +0300 Subject: [PATCH] net: add udp multicast support (fixes #25450) --- vlib/net/README.md | 11 +++ vlib/net/udp.c.v | 207 +++++++++++++++++++++++++++++++++++++++++++- vlib/net/udp_test.v | 42 +++++++++ 3 files changed, 257 insertions(+), 3 deletions(-) diff --git a/vlib/net/README.md b/vlib/net/README.md index 1ac0eb9e0..1d93819ea 100644 --- a/vlib/net/README.md +++ b/vlib/net/README.md @@ -3,3 +3,14 @@ `net` provides networking functions. It is mostly a wrapper to BSD sockets, so you can listen on a port, connect to remote TCP/UDP services, and communicate with them. + +UDP multicast example: + +```v +import net + +mut socket := net.listen_udp('0.0.0.0:9999')! +socket.join_multicast_group('224.0.0.1', '0.0.0.0')! +socket.set_multicast_ttl(2)! +socket.set_multicast_loop(true)! +``` diff --git a/vlib/net/udp.c.v b/vlib/net/udp.c.v index b8a67a1fc..de5e8db6d 100644 --- a/vlib/net/udp.c.v +++ b/vlib/net/udp.c.v @@ -26,6 +26,18 @@ mut: is_blocking bool = true } +@[_pack: '1'] +struct Ipv4MulticastRequest { + multiaddr [4]u8 + interface_ip [4]u8 +} + +@[_pack: '1'] +struct Ipv6MulticastRequest { + multiaddr [16]u8 + interface_index u32 +} + pub fn dial_udp(raddr string) !&UdpConn { addrs := resolve_addrs_fuzzy(raddr, .udp)! @@ -196,6 +208,115 @@ pub fn (mut c UdpConn) close() ! { return c.sock.close() } +// join_multicast_group joins the UDP socket to an IPv4 or IPv6 multicast group. +// For IPv4 sockets, `iface_addr` must be an IPv4 address or `''` to use the default interface. +// For IPv6 sockets, `iface_addr` must be a numeric interface index or `''` to use the default interface. +pub fn (mut c UdpConn) join_multicast_group(multicast_addr string, iface_addr string) ! { + family := c.multicast_family()! + match family { + .ip { + mreq := Ipv4MulticastRequest{ + multiaddr: parse_ipv4_multicast_addr(multicast_addr)! + interface_ip: parse_ipv4_interface_addr(iface_addr)! + } + c.sock.set_option_raw(C.IPPROTO_IP, C.IP_ADD_MEMBERSHIP, &mreq, + sizeof(Ipv4MulticastRequest))! + } + .ip6 { + mreq := Ipv6MulticastRequest{ + multiaddr: parse_ipv6_multicast_addr(multicast_addr)! + interface_index: parse_ipv6_interface_index(iface_addr)! + } + c.sock.set_option_raw(C.IPPROTO_IPV6, ipv6_membership_socket_option(true), &mreq, + sizeof(Ipv6MulticastRequest))! + } + else { + return error('net: udp multicast is only supported on ip and ip6 sockets') + } + } +} + +// leave_multicast_group leaves an IPv4 or IPv6 multicast group previously joined by the socket. +// `iface_addr` follows the same rules as `join_multicast_group`. +pub fn (mut c UdpConn) leave_multicast_group(multicast_addr string, iface_addr string) ! { + family := c.multicast_family()! + match family { + .ip { + mreq := Ipv4MulticastRequest{ + multiaddr: parse_ipv4_multicast_addr(multicast_addr)! + interface_ip: parse_ipv4_interface_addr(iface_addr)! + } + c.sock.set_option_raw(C.IPPROTO_IP, C.IP_DROP_MEMBERSHIP, &mreq, + sizeof(Ipv4MulticastRequest))! + } + .ip6 { + mreq := Ipv6MulticastRequest{ + multiaddr: parse_ipv6_multicast_addr(multicast_addr)! + interface_index: parse_ipv6_interface_index(iface_addr)! + } + c.sock.set_option_raw(C.IPPROTO_IPV6, ipv6_membership_socket_option(false), &mreq, + sizeof(Ipv6MulticastRequest))! + } + else { + return error('net: udp multicast is only supported on ip and ip6 sockets') + } + } +} + +// set_multicast_ttl sets the IPv4 TTL or IPv6 hop limit used for outgoing multicast traffic. +// Valid values are between 0 and 255 inclusive. +pub fn (mut c UdpConn) set_multicast_ttl(ttl int) ! { + if ttl < 0 || ttl > 255 { + return error('net: multicast ttl must be between 0 and 255') + } + match c.multicast_family()! { + .ip { + c.sock.set_option_int(C.IPPROTO_IP, C.IP_MULTICAST_TTL, ttl)! + } + .ip6 { + c.sock.set_option_int(C.IPPROTO_IPV6, C.IPV6_MULTICAST_HOPS, ttl)! + } + else { + return error('net: udp multicast is only supported on ip and ip6 sockets') + } + } +} + +// set_multicast_loop enables or disables local loopback for outgoing multicast packets. +pub fn (mut c UdpConn) set_multicast_loop(enable bool) ! { + value := int(enable) + match c.multicast_family()! { + .ip { + c.sock.set_option_int(C.IPPROTO_IP, C.IP_MULTICAST_LOOP, value)! + } + .ip6 { + c.sock.set_option_int(C.IPPROTO_IPV6, C.IPV6_MULTICAST_LOOP, value)! + } + else { + return error('net: udp multicast is only supported on ip and ip6 sockets') + } + } +} + +// set_multicast_interface sets the outgoing multicast interface for the UDP socket. +// For IPv4 sockets, `iface_addr` must be an IPv4 address or `''` to clear it to the default interface. +// For IPv6 sockets, `iface_addr` must be a numeric interface index or `''` to use the default interface. +pub fn (mut c UdpConn) set_multicast_interface(iface_addr string) ! { + match c.multicast_family()! { + .ip { + addr := parse_ipv4_interface_addr(iface_addr)! + c.sock.set_option_raw(C.IPPROTO_IP, C.IP_MULTICAST_IF, &addr[0], sizeof([4]u8))! + } + .ip6 { + index := parse_ipv6_interface_index(iface_addr)! + c.sock.set_option_int(C.IPPROTO_IPV6, C.IPV6_MULTICAST_IF, int(index))! + } + else { + return error('net: udp multicast is only supported on ip and ip6 sockets') + } + } +} + pub fn listen_udp(laddr string) !&UdpConn { addrs := resolve_addrs_fuzzy(laddr, .udp)! // TODO(emily): @@ -274,6 +395,86 @@ fn new_udp_socket_for_remote(raddr Addr) !&UdpSocket { return sock } +fn (c &UdpConn) multicast_family() !AddrFamily { + family := c.sock.l.family() + if family !in [.ip, .ip6] { + return error('net: udp multicast is only supported on ip and ip6 sockets') + } + return family +} + +fn normalize_ip_literal(address string) string { + if address.len >= 2 && address[0] == `[` && address[address.len - 1] == `]` { + return address[1..address.len - 1] + } + return address +} + +fn parse_ipv4_interface_addr(iface_addr string) ![4]u8 { + if iface_addr.len == 0 { + return [4]u8{} + } + address := normalize_ip_literal(iface_addr) + mut parsed := [4]u8{} + if C.inet_pton(.ip, &char(address.str), &parsed[0]) != 1 { + return error('net: ipv4 multicast interface must be an ipv4 address') + } + return parsed +} + +fn parse_ipv4_multicast_addr(multicast_addr string) ![4]u8 { + address := normalize_ip_literal(multicast_addr) + mut parsed := [4]u8{} + if C.inet_pton(.ip, &char(address.str), &parsed[0]) != 1 { + return error('net: invalid ipv4 multicast address `${multicast_addr}`') + } + if parsed[0] < 224 || parsed[0] > 239 { + return error('net: `${multicast_addr}` is not an ipv4 multicast address') + } + return parsed +} + +fn parse_ipv6_multicast_addr(multicast_addr string) ![16]u8 { + address := normalize_ip_literal(multicast_addr) + mut parsed := [16]u8{} + if C.inet_pton(.ip6, &char(address.str), &parsed[0]) != 1 { + return error('net: invalid ipv6 multicast address `${multicast_addr}`') + } + if parsed[0] != 0xff { + return error('net: `${multicast_addr}` is not an ipv6 multicast address') + } + return parsed +} + +fn parse_ipv6_interface_index(iface_addr string) !u32 { + if iface_addr.len == 0 { + return 0 + } + for ch in iface_addr { + if ch < `0` || ch > `9` { + return error('net: ipv6 multicast interface must be a numeric interface index') + } + } + return iface_addr.u32() +} + +fn ipv6_membership_socket_option(join bool) int { + $if windows { + return if join { C.IPV6_ADD_MEMBERSHIP } else { C.IPV6_DROP_MEMBERSHIP } + } $else { + return if join { C.IPV6_JOIN_GROUP } else { C.IPV6_LEAVE_GROUP } + } +} + +fn (mut s UdpSocket) set_option_raw(level int, opt int, value voidptr, value_len u32) ! { + socket_error(C.setsockopt(s.handle, level, opt, value, value_len))! +} + +fn (mut s UdpSocket) set_option_int(level int, opt int, value int) ! { + s.set_option_raw(level, opt, &value, sizeof(int))! +} + +// set_option_bool sets a boolean socket option on the UDP socket. pub fn (mut s UdpSocket) set_option_bool(opt SocketOption, value bool) ! { // TODO: reenable when this `in` operation works again // if opt !in opts_can_set { @@ -283,13 +484,13 @@ pub fn (mut s UdpSocket) set_option_bool(opt SocketOption, value bool) ! { // return err_option_wrong_type // } x := int(value) - socket_error(C.setsockopt(s.handle, C.SOL_SOCKET, int(opt), &x, sizeof(int)))! + s.set_option_int(C.SOL_SOCKET, int(opt), x)! } +// set_dualstack enables or disables dual-stack behavior for IPv6 UDP sockets. pub fn (mut s UdpSocket) set_dualstack(on bool) ! { x := int(!on) - socket_error(C.setsockopt(s.handle, C.IPPROTO_IPV6, int(SocketOption.ipv6_only), &x, - sizeof(int)))! + s.set_option_int(C.IPPROTO_IPV6, int(SocketOption.ipv6_only), x)! } // close shuts down and closes the socket for communication. diff --git a/vlib/net/udp_test.v b/vlib/net/udp_test.v index 5188894f9..a3837061a 100644 --- a/vlib/net/udp_test.v +++ b/vlib/net/udp_test.v @@ -97,3 +97,45 @@ fn test_udp_read_timeout_is_honored_for_blocking_reads() ! { assert read == payload.len assert buf[..read].bytestr() == payload } + +fn test_udp_multicast_ipv4_socket_options() ! { + mut listener := net.listen_udp('0.0.0.0:0')! + defer { + listener.close() or {} + } + + listener.join_multicast_group('224.0.0.1', '0.0.0.0')! + listener.leave_multicast_group('224.0.0.1', '0.0.0.0')! + listener.set_multicast_ttl(2)! + listener.set_multicast_loop(true)! + listener.set_multicast_loop(false)! + listener.set_multicast_interface('0.0.0.0')! +} + +fn test_udp_multicast_validates_inputs() ! { + mut listener := net.listen_udp('0.0.0.0:0')! + defer { + listener.close() or {} + } + + mut ttl_failed := false + listener.set_multicast_ttl(-1) or { + ttl_failed = true + assert err.msg().contains('multicast ttl') + } + assert ttl_failed + + mut non_multicast_failed := false + listener.join_multicast_group('127.0.0.1', '') or { + non_multicast_failed = true + assert err.msg().contains('not an ipv4 multicast address') + } + assert non_multicast_failed + + mut iface_failed := false + listener.set_multicast_interface('not-an-ip') or { + iface_failed = true + assert err.msg().contains('ipv4 multicast interface') + } + assert iface_failed +} -- 2.39.5