diff --git a/spec/std/socket/udp_socket_spec.cr b/spec/std/socket/udp_socket_spec.cr index 3c071f9bacbe..8b57f0c5320c 100644 --- a/spec/std/socket/udp_socket_spec.cr +++ b/spec/std/socket/udp_socket_spec.cr @@ -1,8 +1,9 @@ require "./spec_helper" +require "../../support/errno" require "socket" describe UDPSocket do - each_ip_family do |family, address| + each_ip_family do |family, address, unspecified_address| it "#bind" do port = unused_local_port socket = UDPSocket.new(family) @@ -52,6 +53,63 @@ describe UDPSocket do client.close server.close end + + if {{ flag?(:darwin) }} && family == Socket::Family::INET6 + # Darwin is failing to join IPv6 multicast groups on older versions. + # However this is known to work on macOS Mojave with Darwin 18.2.0. + # Darwin also has a bug that prevents selecting the "default" interface. + # https://lists.apple.com/archives/darwin-kernel/2014/Mar/msg00012.html + pending "joins and transmits to multicast groups" + else + it "joins and transmits to multicast groups" do + udp = UDPSocket.new(family) + udp.bind(unspecified_address, 2000) + + udp.multicast_loopback = false + udp.multicast_loopback?.should eq(false) + + udp.multicast_hops = 4 + udp.multicast_hops.should eq(4) + udp.multicast_hops = 0 + udp.multicast_hops.should eq(0) + + addr = case family + when Socket::Family::INET + expect_raises(Socket::Error, "Unsupported IP address family: INET. For use with IPv6 only") do + udp.multicast_interface 0 + end + udp.multicast_interface Socket::IPAddress.new(unspecified_address, 0) + + Socket::IPAddress.new("224.0.0.254", 2000) + when Socket::Family::INET6 + expect_raises(Socket::Error, "Unsupported IP address family: INET6. For use with IPv4 only") do + udp.multicast_interface(Socket::IPAddress.new(unspecified_address, 0)) + end + udp.multicast_interface(0) + + Socket::IPAddress.new("ff02::102", 2000) + else + raise "Unsupported IP address family: #{family}" + end + + udp.join_group(addr) + udp.multicast_loopback = true + udp.multicast_loopback?.should eq(true) + + udp.send("testing", addr) + udp.receive[0].should eq("testing") + + udp.leave_group(addr) + udp.send("testing", addr) + + # Test that nothing was received after leaving the multicast group + spawn do + sleep 100.milliseconds + udp.close + end + expect_raises_errno(Errno::EBADF, "Error receiving datagram: Bad file descriptor") { udp.receive } + end + end end {% if flag?(:linux) %} diff --git a/src/lib_c/aarch64-linux-gnu/c/netinet/in.cr b/src/lib_c/aarch64-linux-gnu/c/netinet/in.cr index 466c5dab53e7..b934d735f80b 100644 --- a/src/lib_c/aarch64-linux-gnu/c/netinet/in.cr +++ b/src/lib_c/aarch64-linux-gnu/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -39,4 +40,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 32 + IPV6_MULTICAST_IF = 17 + + IP_MULTICAST_TTL = 33 + IPV6_MULTICAST_HOPS = 18 + + IP_MULTICAST_LOOP = 34 + IPV6_MULTICAST_LOOP = 19 + + IP_ADD_MEMBERSHIP = 35 + IPV6_JOIN_GROUP = 20 + + IP_DROP_MEMBERSHIP = 36 + IPV6_LEAVE_GROUP = 21 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/aarch64-linux-musl/c/netinet/in.cr b/src/lib_c/aarch64-linux-musl/c/netinet/in.cr index 464da8a3fe4d..c497075effaf 100644 --- a/src/lib_c/aarch64-linux-musl/c/netinet/in.cr +++ b/src/lib_c/aarch64-linux-musl/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -39,4 +40,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 32 + IPV6_MULTICAST_IF = 17 + + IP_MULTICAST_TTL = 33 + IPV6_MULTICAST_HOPS = 18 + + IP_MULTICAST_LOOP = 34 + IPV6_MULTICAST_LOOP = 19 + + IP_ADD_MEMBERSHIP = 35 + IPV6_JOIN_GROUP = 20 + + IP_DROP_MEMBERSHIP = 36 + IPV6_LEAVE_GROUP = 21 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/arm-linux-gnueabihf/c/netinet/in.cr b/src/lib_c/arm-linux-gnueabihf/c/netinet/in.cr index 466c5dab53e7..b934d735f80b 100644 --- a/src/lib_c/arm-linux-gnueabihf/c/netinet/in.cr +++ b/src/lib_c/arm-linux-gnueabihf/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -39,4 +40,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 32 + IPV6_MULTICAST_IF = 17 + + IP_MULTICAST_TTL = 33 + IPV6_MULTICAST_HOPS = 18 + + IP_MULTICAST_LOOP = 34 + IPV6_MULTICAST_LOOP = 19 + + IP_ADD_MEMBERSHIP = 35 + IPV6_JOIN_GROUP = 20 + + IP_DROP_MEMBERSHIP = 36 + IPV6_LEAVE_GROUP = 21 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/i386-linux-gnu/c/netinet/in.cr b/src/lib_c/i386-linux-gnu/c/netinet/in.cr index 466c5dab53e7..b934d735f80b 100644 --- a/src/lib_c/i386-linux-gnu/c/netinet/in.cr +++ b/src/lib_c/i386-linux-gnu/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -39,4 +40,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 32 + IPV6_MULTICAST_IF = 17 + + IP_MULTICAST_TTL = 33 + IPV6_MULTICAST_HOPS = 18 + + IP_MULTICAST_LOOP = 34 + IPV6_MULTICAST_LOOP = 19 + + IP_ADD_MEMBERSHIP = 35 + IPV6_JOIN_GROUP = 20 + + IP_DROP_MEMBERSHIP = 36 + IPV6_LEAVE_GROUP = 21 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/i386-linux-musl/c/netinet/in.cr b/src/lib_c/i386-linux-musl/c/netinet/in.cr index 464da8a3fe4d..c497075effaf 100644 --- a/src/lib_c/i386-linux-musl/c/netinet/in.cr +++ b/src/lib_c/i386-linux-musl/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -39,4 +40,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 32 + IPV6_MULTICAST_IF = 17 + + IP_MULTICAST_TTL = 33 + IPV6_MULTICAST_HOPS = 18 + + IP_MULTICAST_LOOP = 34 + IPV6_MULTICAST_LOOP = 19 + + IP_ADD_MEMBERSHIP = 35 + IPV6_JOIN_GROUP = 20 + + IP_DROP_MEMBERSHIP = 36 + IPV6_LEAVE_GROUP = 21 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/x86_64-darwin/c/netinet/in.cr b/src/lib_c/x86_64-darwin/c/netinet/in.cr index 88d64a2b23cc..02cb5721dc21 100644 --- a/src/lib_c/x86_64-darwin/c/netinet/in.cr +++ b/src/lib_c/x86_64-darwin/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -41,4 +42,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt end + + IP_MULTICAST_IF = 9 + IPV6_MULTICAST_IF = 9 + + IP_MULTICAST_TTL = 10 + IPV6_MULTICAST_HOPS = 10 + + IP_MULTICAST_LOOP = 11 + IPV6_MULTICAST_LOOP = 11 + + IP_ADD_MEMBERSHIP = 12 + IPV6_JOIN_GROUP = 12 + + IP_DROP_MEMBERSHIP = 13 + IPV6_LEAVE_GROUP = 13 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/x86_64-freebsd/c/netinet/in.cr b/src/lib_c/x86_64-freebsd/c/netinet/in.cr index 698b00a5bede..0f09b0e39ca5 100644 --- a/src/lib_c/x86_64-freebsd/c/netinet/in.cr +++ b/src/lib_c/x86_64-freebsd/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -41,4 +42,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 9 + IPV6_MULTICAST_IF = 9 + + IP_MULTICAST_TTL = 10 + IPV6_MULTICAST_HOPS = 10 + + IP_MULTICAST_LOOP = 11 + IPV6_MULTICAST_LOOP = 11 + + IP_ADD_MEMBERSHIP = 12 + IPV6_JOIN_GROUP = 12 + + IP_DROP_MEMBERSHIP = 13 + IPV6_LEAVE_GROUP = 13 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/x86_64-linux-gnu/c/netinet/in.cr b/src/lib_c/x86_64-linux-gnu/c/netinet/in.cr index 466c5dab53e7..b934d735f80b 100644 --- a/src/lib_c/x86_64-linux-gnu/c/netinet/in.cr +++ b/src/lib_c/x86_64-linux-gnu/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -39,4 +40,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 32 + IPV6_MULTICAST_IF = 17 + + IP_MULTICAST_TTL = 33 + IPV6_MULTICAST_HOPS = 18 + + IP_MULTICAST_LOOP = 34 + IPV6_MULTICAST_LOOP = 19 + + IP_ADD_MEMBERSHIP = 35 + IPV6_JOIN_GROUP = 20 + + IP_DROP_MEMBERSHIP = 36 + IPV6_LEAVE_GROUP = 21 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/x86_64-linux-musl/c/netinet/in.cr b/src/lib_c/x86_64-linux-musl/c/netinet/in.cr index 464da8a3fe4d..c497075effaf 100644 --- a/src/lib_c/x86_64-linux-musl/c/netinet/in.cr +++ b/src/lib_c/x86_64-linux-musl/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -39,4 +40,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 32 + IPV6_MULTICAST_IF = 17 + + IP_MULTICAST_TTL = 33 + IPV6_MULTICAST_HOPS = 18 + + IP_MULTICAST_LOOP = 34 + IPV6_MULTICAST_LOOP = 19 + + IP_ADD_MEMBERSHIP = 35 + IPV6_JOIN_GROUP = 20 + + IP_DROP_MEMBERSHIP = 36 + IPV6_LEAVE_GROUP = 21 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/lib_c/x86_64-openbsd/c/netinet/in.cr b/src/lib_c/x86_64-openbsd/c/netinet/in.cr index 9e5ab45a2318..f5b968cae094 100644 --- a/src/lib_c/x86_64-openbsd/c/netinet/in.cr +++ b/src/lib_c/x86_64-openbsd/c/netinet/in.cr @@ -3,6 +3,7 @@ require "../stdint" lib LibC IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 IPPROTO_ICMP = 1 IPPROTO_RAW = 255 IPPROTO_TCP = 6 @@ -41,4 +42,29 @@ lib LibC sin6_addr : In6Addr sin6_scope_id : UInt32T end + + IP_MULTICAST_IF = 9 + IPV6_MULTICAST_IF = 9 + + IP_MULTICAST_TTL = 10 + IPV6_MULTICAST_HOPS = 10 + + IP_MULTICAST_LOOP = 11 + IPV6_MULTICAST_LOOP = 11 + + IP_ADD_MEMBERSHIP = 12 + IPV6_JOIN_GROUP = 12 + + IP_DROP_MEMBERSHIP = 13 + IPV6_LEAVE_GROUP = 13 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end end diff --git a/src/socket/udp_socket.cr b/src/socket/udp_socket.cr index 503588cdb940..712d02f2df7f 100644 --- a/src/socket/udp_socket.cr +++ b/src/socket/udp_socket.cr @@ -92,4 +92,148 @@ class UDPSocket < IPSocket bytes_read, sockaddr, addrlen = recvfrom(message) {bytes_read, IPAddress.from(sockaddr, addrlen)} end + + # Reports whether transmitted multicast packets should be copied and sent + # back to the originator. + def multicast_loopback? + case @family + when Family::INET + getsockopt_bool LibC::IP_MULTICAST_LOOP, LibC::IPPROTO_IP + when Family::INET6 + getsockopt_bool LibC::IPV6_MULTICAST_LOOP, LibC::IPPROTO_IPV6 + else + raise Socket::Error.new "Unsupported IP address family: #{@family}" + end + end + + # Sets whether transmitted multicast packets should be copied and sent back + # to the originator, if the host has joined the multicast group. + def multicast_loopback=(val : Bool) + case @family + when Family::INET + setsockopt_bool LibC::IP_MULTICAST_LOOP, val, LibC::IPPROTO_IP + when Family::INET6 + setsockopt_bool LibC::IPV6_MULTICAST_LOOP, val, LibC::IPPROTO_IPV6 + else + raise Socket::Error.new "Unsupported IP address family: #{@family}" + end + end + + # Returns the current value of the `hoplimit` field on uni-cast packets. + # Datagrams with a `hoplimit` of `1` are not forwarded beyond the local network. + # Multicast datagrams with a `hoplimit` of `0` will not be transmitted on any + # network, but may be delivered locally if the sending host belongs to the + # destination group and multicast loopback is enabled. + def multicast_hops + case @family + when Family::INET + getsockopt LibC::IP_MULTICAST_TTL, 0, LibC::IPPROTO_IP + when Family::INET6 + getsockopt LibC::IPV6_MULTICAST_HOPS, 0, LibC::IPPROTO_IPV6 + else + raise Socket::Error.new "Unsupported IP address family: #{@family}" + end + end + + # The multicast hops option controls the `hoplimit` field on uni-cast packets. + # If `-1` is specified, the kernel will use a default value. + # If a value of `0` to `255` is specified, the packet will have the specified + # value as `hoplimit`. Other values are considered invalid and `Errno` will be raised. + # Datagrams with a `hoplimit` of `1` are not forwarded beyond the local network. + # Multicast datagrams with a `hoplimit` of `0` will not be transmitted on any + # network, but may be delivered locally if the sending host belongs to the + # destination group and multicast loopback is enabled. + def multicast_hops=(val : Int) + case @family + when Family::INET + setsockopt LibC::IP_MULTICAST_TTL, val, LibC::IPPROTO_IP + when Family::INET6 + setsockopt LibC::IPV6_MULTICAST_HOPS, val, LibC::IPPROTO_IPV6 + else + raise Socket::Error.new "Unsupported IP address family: #{@family}" + end + val + end + + # For hosts with multiple interfaces, each multicast transmission is sent + # from the primary network interface. This function overrides the default + # IPv4 interface address for subsequent transmissions. Setting the interface + # to `0.0.0.0` will select the default interface. + # Raises `Socket::Error` unless the socket is IPv4 and an IPv4 address is provided. + def multicast_interface(address : IPAddress) + if @family == Family::INET + if addr = address.@addr4 + setsockopt LibC::IP_MULTICAST_IF, addr, LibC::IPPROTO_IP + else + raise Socket::Error.new "Expecting an IPv4 interface address. Address provided: #{address.address}" + end + else + raise Socket::Error.new "Unsupported IP address family: #{@family}. For use with IPv4 only" + end + end + + # For hosts with multiple interfaces, each multicast transmission is sent + # from the primary network interface. This function overrides the default + # IPv6 interface for subsequent transmissions. Setting the interface to + # index `0` will select the default interface. + def multicast_interface(index : UInt32) + if @family == Family::INET6 + setsockopt LibC::IPV6_MULTICAST_IF, index, LibC::IPPROTO_IPV6 + else + raise Socket::Error.new "Unsupported IP address family: #{@family}. For use with IPv6 only" + end + end + + # A host must become a member of a multicast group before it can receive + # datagrams sent to the group. + # Raises `Socket::Error` if an incompatible address is provided. + def join_group(address : IPAddress) + case @family + when Family::INET + group_modify(address, LibC::IP_ADD_MEMBERSHIP) + when Family::INET6 + group_modify(address, LibC::IPV6_JOIN_GROUP) + else + raise Socket::Error.new "Unsupported IP address family: #{@family}" + end + end + + # Drops membership to the specified group. Memberships are automatically + # dropped when the socket is closed or the process exits. + # Raises `Socket::Error` if an incompatible address is provided. + def leave_group(address : IPAddress) + case @family + when Family::INET + group_modify(address, LibC::IP_DROP_MEMBERSHIP) + when Family::INET6 + group_modify(address, LibC::IPV6_LEAVE_GROUP) + else + raise Socket::Error.new "Unsupported IP address family: #{@family}" + end + end + + private def group_modify(ip, operation) + case @family + when Family::INET + if ip_addr = ip.@addr4 + req = LibC::IpMreq.new + req.imr_multiaddr = ip_addr + + setsockopt operation, req, LibC::IPPROTO_IP + else + raise Socket::Error.new "Expecting an IPv4 multicast address. Address provided: #{ip.address}" + end + when Family::INET6 + if ip_addr = ip.@addr6 + req = LibC::Ipv6Mreq.new + req.ipv6mr_multiaddr = ip_addr + + setsockopt operation, req, LibC::IPPROTO_IPV6 + else + raise Socket::Error.new "Expecting an IPv6 multicast address. Address provided: #{ip.address}" + end + else + raise Socket::Error.new "Unsupported IP address family: #{@family}" + end + end end