diff options
author | Neil Fuller <nfuller@google.com> | 2014-01-30 14:17:54 +0000 |
---|---|---|
committer | Neil Fuller <nfuller@google.com> | 2014-02-28 16:02:08 +0000 |
commit | df29508a7aa622f265aaebdc472eb7d679185ebb (patch) | |
tree | e2ac108fcb0b350c8fe41033d26a4d272f926584 /luni | |
parent | dc22c51199c848e9eaca7081165d6d9b94cf3389 (diff) | |
download | libcore-df29508a7aa622f265aaebdc472eb7d679185ebb.zip libcore-df29508a7aa622f265aaebdc472eb7d679185ebb.tar.gz libcore-df29508a7aa622f265aaebdc472eb7d679185ebb.tar.bz2 |
Implementation of MulticastChannel.
Note: ProtocolFamily / StandardProtocolFamily /
DatagramChannel.open(ProtocolFamily) have not been
implemented.
There is a related change to libnativehelper that
must be merged at the same time to avoid
build breakage. See
https://android-review.googlesource.com/#/c/81371/
Bug: 12464155
Change-Id: I07fc049b429a2d373e9bd7b07149632f5cd69f9d
Diffstat (limited to 'luni')
22 files changed, 2356 insertions, 15 deletions
diff --git a/luni/src/main/java/java/net/MulticastSocket.java b/luni/src/main/java/java/net/MulticastSocket.java index 970468a..24e66c5 100644 --- a/luni/src/main/java/java/net/MulticastSocket.java +++ b/luni/src/main/java/java/net/MulticastSocket.java @@ -229,6 +229,9 @@ public class MulticastSocket extends DatagramSocket { private void checkJoinOrLeave(InetAddress groupAddr) throws IOException { checkOpen(); + if (groupAddr == null) { + throw new IllegalArgumentException("groupAddress == null"); + } if (!groupAddr.isMulticastAddress()) { throw new IOException("Not a multicast group: " + groupAddr); } diff --git a/luni/src/main/java/java/nio/DatagramChannelImpl.java b/luni/src/main/java/java/nio/DatagramChannelImpl.java index cb940db..883ffea 100644 --- a/luni/src/main/java/java/nio/DatagramChannelImpl.java +++ b/luni/src/main/java/java/nio/DatagramChannelImpl.java @@ -27,6 +27,7 @@ import java.net.DatagramSocketImpl; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.NetworkInterface; import java.net.PlainDatagramSocketImpl; import java.net.SocketAddress; import java.net.SocketException; @@ -37,7 +38,9 @@ import java.nio.channels.AlreadyConnectedException; import java.nio.channels.ClosedChannelException; import java.nio.channels.DatagramChannel; import java.nio.channels.IllegalBlockingModeException; +import java.nio.channels.MembershipKey; import java.nio.channels.NotYetConnectedException; +import java.nio.channels.UnresolvedAddressException; import java.nio.channels.UnsupportedAddressTypeException; import java.nio.channels.spi.SelectorProvider; import java.util.Arrays; @@ -77,6 +80,9 @@ class DatagramChannelImpl extends DatagramChannel implements FileDescriptorChann private final Object readLock = new Object(); private final Object writeLock = new Object(); + // A helper to manage multicast group membership. Created as required. + private MulticastMembershipHandler multicastMembershipHandler; + /* * Constructor */ @@ -122,6 +128,9 @@ class DatagramChannelImpl extends DatagramChannel implements FileDescriptorChann } InetSocketAddress localAddress = (InetSocketAddress) local; + if (localAddress.isUnresolved()) { + throw new UnresolvedAddressException(); + } IoBridge.bind(fd, localAddress.getAddress(), localAddress.getPort()); onBind(true /* updateSocketState */); return this; @@ -516,6 +525,7 @@ class DatagramChannelImpl extends DatagramChannel implements FileDescriptorChann // A closed channel is not connected. onDisconnect(true /* updateSocketState */); IoBridge.closeSocket(fd); + multicastMembershipHandler = null; if (socket != null && !socket.isClosed()) { socket.onClose(); @@ -529,7 +539,7 @@ class DatagramChannelImpl extends DatagramChannel implements FileDescriptorChann /* * Status check, must be open. */ - private void checkOpen() throws IOException { + private void checkOpen() throws ClosedChannelException { if (!isOpen()) { throw new ClosedChannelException(); } @@ -561,6 +571,52 @@ class DatagramChannelImpl extends DatagramChannel implements FileDescriptorChann return fd; } + @Override + synchronized public MembershipKey join(InetAddress groupAddress, + NetworkInterface networkInterface) throws IOException { + + checkOpen(); + ensureMembershipHandlerExists(); + return multicastMembershipHandler.addAnySourceMembership(networkInterface, groupAddress); + } + + @Override + synchronized public MembershipKey join( + InetAddress groupAddress, NetworkInterface networkInterface, InetAddress sourceAddress) + throws IOException { + checkOpen(); + ensureMembershipHandlerExists(); + return multicastMembershipHandler.addSourceSpecificMembership( + networkInterface, groupAddress, sourceAddress); + } + + synchronized void multicastDrop(MembershipKeyImpl membershipKey) { + ensureMembershipHandlerExists(); + multicastMembershipHandler.dropMembership(membershipKey); + } + + synchronized void multicastBlock(MembershipKeyImpl membershipKey, InetAddress sourceAddress) + throws SocketException { + + ensureMembershipHandlerExists(); + multicastMembershipHandler.block(membershipKey, sourceAddress); + } + + synchronized void multicastUnblock(MembershipKeyImpl membershipKey, InetAddress sourceAddress) { + ensureMembershipHandlerExists(); + multicastMembershipHandler.unblock(membershipKey, sourceAddress); + } + + /** + * Creates the {@code multicastMembershipHandler} if one doesn't already exist. Callers must + * handle synchronization. + */ + private void ensureMembershipHandlerExists() { + if (multicastMembershipHandler == null) { + multicastMembershipHandler = new MulticastMembershipHandler(this); + } + } + /* * The adapter class of DatagramSocket */ diff --git a/luni/src/main/java/java/nio/MembershipKeyImpl.java b/luni/src/main/java/java/nio/MembershipKeyImpl.java new file mode 100644 index 0000000..3d7e957 --- /dev/null +++ b/luni/src/main/java/java/nio/MembershipKeyImpl.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package java.nio; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.nio.channels.MembershipKey; +import java.nio.channels.MulticastChannel; + +/** + * An implementation of {@link MembershipKey}. + * + * To keep this class simple and keep all mutation operations in one place and easily synchronized, + * most socket logic is held in {@link java.nio.DatagramChannelImpl}. + */ +final class MembershipKeyImpl extends MembershipKey { + + private final DatagramChannelImpl channel; + private final InetAddress groupAddress; + private final NetworkInterface networkInterface; + private final InetAddress sourceAddress; + private volatile boolean isValid; + + public MembershipKeyImpl(DatagramChannelImpl channel, NetworkInterface networkInterface, + InetAddress groupAddress, InetAddress sourceAddress) { + + this.channel = channel; + this.networkInterface = networkInterface; + this.groupAddress = groupAddress; + this.sourceAddress = sourceAddress; + this.isValid = true; + } + + @Override + public boolean isValid() { + // invalidate() is called if the key is dropped, but for simplicity it is not + // invalidated when the channel is closed. Therefore, the channel must also be checked to see + // if it is still open. + return isValid && channel.isOpen(); + } + + void invalidate() { + this.isValid = false; + } + + @Override + public void drop() { + channel.multicastDrop(this); + } + + @Override + public MembershipKey block(InetAddress source) throws IOException { + channel.multicastBlock(this, source); + return this; + } + + @Override + synchronized public MembershipKey unblock(InetAddress source) { + channel.multicastUnblock(this, source); + return this; + } + + @Override + public MulticastChannel channel() { + return channel; + } + + @Override + public InetAddress group() { + return groupAddress; + } + + @Override + public NetworkInterface networkInterface() { + return networkInterface; + } + + @Override + public InetAddress sourceAddress() { + return sourceAddress; + } + + @Override + public String toString() { + return "MembershipKeyImpl{" + + "groupAddress=" + groupAddress + + ", networkInterface=" + networkInterface + + ", sourceAddress=" + sourceAddress + + '}'; + } +} diff --git a/luni/src/main/java/java/nio/MulticastMembershipHandler.java b/luni/src/main/java/java/nio/MulticastMembershipHandler.java new file mode 100644 index 0000000..7564d4e --- /dev/null +++ b/luni/src/main/java/java/nio/MulticastMembershipHandler.java @@ -0,0 +1,496 @@ +package java.nio; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.channels.MembershipKey; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import libcore.io.IoBridge; +import libcore.io.StructGroupReq; +import libcore.io.StructGroupSourceReq; + +/** + * A helper class for {@link DatagramChannelImpl} that keeps track of multicast group + * memberships. This class is not threadsafe, and relies on the DatagramChannelImpl to synchronize. + * + * <p>See <a href="http://tools.ietf.org/html/rfc3678">RFC 3678</a> for context and terminology. + */ +final class MulticastMembershipHandler { + + private final DatagramChannelImpl channel; + private final Map<Id, Membership> memberships = new HashMap<Id, Membership>(); + + MulticastMembershipHandler(DatagramChannelImpl channel) { + this.channel = channel; + } + + /** + * The implementation for + * {@link java.nio.channels.MulticastChannel#join(InetAddress, NetworkInterface)}. + */ + public MembershipKeyImpl addAnySourceMembership( + NetworkInterface networkInterface, InetAddress groupAddress) throws SocketException { + + validateMulticastGroupArgs(groupAddress, networkInterface); + assertChannelOpen(); + + Id id = new Id(networkInterface, groupAddress); + Membership membership = memberships.get(id); + if (membership != null) { + return membership.getAnySourceMembershipKey(); + } + + // No existing membership found. Attempt to join. + StructGroupReq groupReq = makeGroupReq(groupAddress, networkInterface); + IoBridge.setSocketOption(channel.getFD(), IoBridge.JAVA_MCAST_JOIN_GROUP, groupReq); + + // Record the membership and return the key. + membership = Membership.createAnySource(channel, networkInterface, groupAddress); + memberships.put(id, membership); + return membership.getAnySourceMembershipKey(); + } + + /** + * The implementation for + * {@link java.nio.channels.MulticastChannel#join(InetAddress, NetworkInterface, InetAddress)}. + */ + public MembershipKeyImpl addSourceSpecificMembership( + NetworkInterface networkInterface, InetAddress groupAddress, InetAddress sourceAddress) + throws SocketException { + + validateMulticastGroupArgs(groupAddress, networkInterface); + validateSourceAddress(sourceAddress); + validateAddressProtocolTheSame(groupAddress, sourceAddress); + assertChannelOpen(); + + Id id = new Id(networkInterface, groupAddress); + Membership membership = memberships.get(id); + if (membership != null) { + MembershipKeyImpl existingMembershipKey = + membership.getSourceSpecificMembershipKey(sourceAddress); + if (existingMembershipKey != null) { + return existingMembershipKey; + } + } + + // No existing membership found. Attempt to join. + IoBridge.setSocketOption(channel.getFD(), IoBridge.JAVA_MCAST_JOIN_SOURCE_GROUP, + makeGroupSourceReq(groupAddress, networkInterface, sourceAddress)); + + if (membership == null) { + // Record the membership and return the key. + membership = Membership.createSourceSpecific( + channel, networkInterface, groupAddress, sourceAddress); + memberships.put(id, membership); + return membership.getSourceSpecificMembershipKey(sourceAddress); + } else { + // Add a new source to the existing membership. + return membership.addSource(sourceAddress); + } + } + + /** + * The implementation for {@link MembershipKey#drop()}. + */ + public void dropMembership(MembershipKeyImpl membershipKey) { + // For compatibility with the RI, this is one case where the membershipKey can no longer be + // valid. + if (!membershipKey.isValid()) { + return; + } + if (membershipKey.channel() != this.channel) { + throw new AssertionError("Bad membership key"); + } + assertChannelOpen(); + + Id id = createId(membershipKey); + Membership membership = memberships.get(id); + if (membership == null) { + throw new AssertionError("Bad membership key" + membershipKey); + } + + if (!membership.isSourceSpecific()) { + try { + StructGroupReq groupReq = + makeGroupReq(membershipKey.group(), membershipKey.networkInterface()); + IoBridge.setSocketOption(channel.getFD(), IoBridge.JAVA_MCAST_LEAVE_GROUP, groupReq); + } catch (SocketException e) { + // TODO: Obtain opinion on how to report this, throw this or if it is safe to ignore. + throw new IllegalStateException(e); + } + memberships.remove(id); + } else { + StructGroupSourceReq groupSourceReq = makeGroupSourceReq(membershipKey.group(), + membershipKey.networkInterface(), membershipKey.sourceAddress()); + + try { + IoBridge.setSocketOption( + channel.getFD(), IoBridge.JAVA_MCAST_LEAVE_SOURCE_GROUP, + groupSourceReq); + } catch (SocketException e) { + // TODO: Obtain opinion on how to report this, throw this or if it is safe to ignore. + throw new IllegalStateException(e); + } + + boolean isLast = membership.removeSource(membershipKey.sourceAddress()); + if (isLast) { + memberships.remove(id); + } + } + membershipKey.invalidate(); + } + + /** + * The implementation for {@link MembershipKey#block(java.net.InetAddress)}. + */ + public void block(MembershipKeyImpl membershipKey, InetAddress sourceAddress) + throws SocketException { + validateMembershipKey(membershipKey); + validateSourceAddress(sourceAddress); + validateAddressProtocolTheSame(membershipKey.group(), sourceAddress); + assertChannelOpen(); + + Membership membership = getMembershipForKey(membershipKey); + if (membership == null) { + throw new AssertionError("Bad membership key" + membershipKey); + } + + if (membership.isBlocked(sourceAddress)) { + return; + } + + IoBridge.setSocketOption(channel.getFD(), IoBridge.JAVA_MCAST_BLOCK_SOURCE, + makeGroupSourceReq( + membershipKey.group(), membershipKey.networkInterface(), sourceAddress)); + + membership.block(sourceAddress); + } + + /** + * The implementation for {@link MembershipKey#unblock(java.net.InetAddress)}. + */ + public void unblock(MembershipKeyImpl membershipKey, InetAddress sourceAddress) { + validateMembershipKey(membershipKey); + validateSourceAddress(sourceAddress); + validateAddressProtocolTheSame(membershipKey.group(), sourceAddress); + assertChannelOpen(); + + Membership membership = getMembershipForKey(membershipKey); + if (membership == null) { + throw new AssertionError("Bad membership key" + membershipKey); + } + + if (!membership.isBlocked(sourceAddress)) { + throw new IllegalStateException( + "sourceAddress " + sourceAddress + " is not blocked for " + membership.debugId()); + } + + try { + IoBridge.setSocketOption(channel.getFD(), IoBridge.JAVA_MCAST_UNBLOCK_SOURCE, + makeGroupSourceReq(membershipKey.group(), membershipKey.networkInterface(), + sourceAddress)); + } catch (SocketException e) { + throw new IllegalStateException(e); + } + + membership.unblock(sourceAddress); + } + + private Membership getMembershipForKey(MembershipKey membershipKey) { + Id id = createId(membershipKey); + Membership membership = memberships.get(id); + if (membership == null) { + throw new AssertionError("No membership found for id " + id); + } + return membership; + } + + private void assertChannelOpen() { + if (!channel.isOpen()) { + throw new AssertionError("Channel is closed"); + } + } + + private void validateMembershipKey(MembershipKeyImpl membershipKey) { + if (membershipKey.channel() != this.channel) { + throw new AssertionError("Invalid or bad membership key"); + } + if (!membershipKey.isValid()) { + throw new IllegalStateException("Membership key is no longer valid: " + membershipKey); + } + } + + private static Id createId(MembershipKey membershipKey) { + return new Id(membershipKey.networkInterface(), membershipKey.group()); + } + + private static void validateSourceAddress(InetAddress sourceAddress) { + if (sourceAddress.isAnyLocalAddress()) { + throw new IllegalArgumentException( + "sourceAddress must not be a wildcard address, is " + sourceAddress); + } + if (sourceAddress.isMulticastAddress()) { + throw new IllegalArgumentException( + "sourceAddress must be a unicast address, is " + sourceAddress); + } + } + + private static void validateMulticastGroupArgs( + InetAddress groupAddress, NetworkInterface networkInterface) throws SocketException { + + if (groupAddress == null) { + // RI throws NullPointerException. + throw new NullPointerException("groupAddress == null"); + } + if (networkInterface == null) { + // RI throws NullPointerException. + throw new NullPointerException("networkInterface == null"); + } + if (!networkInterface.isLoopback() && !networkInterface.supportsMulticast()) { + throw new IllegalArgumentException( + "networkInterface " + networkInterface + " does not support multicast"); + } + if (!groupAddress.isMulticastAddress()) { + throw new IllegalArgumentException("Not a multicast group: " + groupAddress); + } + } + + private static void validateAddressProtocolTheSame( + InetAddress groupAddress, InetAddress sourceAddress) { + + if (groupAddress.getClass() != sourceAddress.getClass()) { + throw new IllegalArgumentException("Mixed address types not permitted: groupAddress: " + + groupAddress + ", sourceAddress: " + sourceAddress); + } + } + + private static StructGroupSourceReq makeGroupSourceReq( + InetAddress gsr_group, NetworkInterface networkInterface, InetAddress gsr_source) { + int gsr_interface = (networkInterface != null) ? networkInterface.getIndex() : 0; + return new StructGroupSourceReq(gsr_interface, gsr_group, gsr_source); + } + + private static StructGroupReq makeGroupReq(InetAddress gr_group, + NetworkInterface networkInterface) { + int gr_interface = (networkInterface != null) ? networkInterface.getIndex() : 0; + return new StructGroupReq(gr_interface, gr_group); + } + + /** + * Membership information associated with an {@link Id}. A membership can be one of two types: + * "source-specific" and "any-source". The two types a mutually exclusive for a given Id. + */ + static final class Membership { + + private final DatagramChannelImpl channel; + private final InetAddress groupAddress; + private final NetworkInterface networkInterface; + + // Any-source membership key. Mutually exclusive with sourceSpecificMembershipKeys. + private final MembershipKeyImpl anySourceMembershipKey; + // Blocked source addresses for any-source memberships. Assigned when required. + private Set<InetAddress> blockedSourceAddresses; + + // Source-specific membership keys. Mutually exclusive with anySourceMembershipKey. + private final Map<InetAddress, MembershipKeyImpl> sourceSpecificMembershipKeys; + + /** Use {@link #createSourceSpecific} or {@link #createAnySource} to construct. */ + private Membership( + DatagramChannelImpl channel, + InetAddress groupAddress, + NetworkInterface networkInterface, + MembershipKeyImpl anySourceMembershipKey, + Map<InetAddress, MembershipKeyImpl> sourceSpecificMembershipKeys) { + + this.channel = channel; + this.groupAddress = groupAddress; + this.networkInterface = networkInterface; + this.anySourceMembershipKey = anySourceMembershipKey; + this.sourceSpecificMembershipKeys = sourceSpecificMembershipKeys; + } + + /** Creates an any-source membership. */ + public static Membership createAnySource(DatagramChannelImpl channel, + NetworkInterface networkInterface, InetAddress groupAddress) { + + MembershipKeyImpl withoutSourceAddressKey = + new MembershipKeyImpl(channel, networkInterface, groupAddress, null /* sourceAddress */); + return new Membership( + channel, groupAddress, networkInterface, withoutSourceAddressKey, + null /* sourceSpecificMembershipKeys */); + } + + /** + * Creates a source-specific membership. See {@link #addSource} to add additional source + * addresses. + */ + public static Membership createSourceSpecific(DatagramChannelImpl channel, + NetworkInterface networkInterface, InetAddress groupAddress, InetAddress sourceAddress) { + + Map<InetAddress, MembershipKeyImpl> withSourceKeys = + new HashMap<InetAddress, MembershipKeyImpl>(); + Membership membership = new Membership( + channel, groupAddress, networkInterface, null /* anySourceMembershipKey */, + withSourceKeys); + membership.addSource(sourceAddress); + return membership; + } + + /** + * Adds a new source address filter to an existing membership, returning the associated + * {@link MembershipKeyImpl}. Throws an {@code IllegalStateException} if this is an + * any-source membership. + */ + public MembershipKeyImpl addSource(InetAddress sourceAddress) { + if (sourceSpecificMembershipKeys == null) { + throw new IllegalStateException( + "Can only add sources to source-specific memberships: " + debugId()); + } + + MembershipKeyImpl membershipKey = + new MembershipKeyImpl(channel, networkInterface, groupAddress, sourceAddress); + sourceSpecificMembershipKeys.put(sourceAddress, membershipKey); + return membershipKey; + } + + /** + * Removes the specified {@code sourceAddress} from the set of filters. Returns {@code true} if + * the set of filters is now empty. Throws an {@code IllegalStateException} if this is an + * any-source membership. + */ + public boolean removeSource(InetAddress sourceAddress) { + if (sourceSpecificMembershipKeys == null) { + throw new IllegalStateException( + "Can only remove sources from source-specific memberships: " + debugId()); + } + sourceSpecificMembershipKeys.remove(sourceAddress); + return sourceSpecificMembershipKeys.isEmpty(); + } + + /** + * Returns {@code true} if the membership source-specific, false if it is any-source. + */ + public boolean isSourceSpecific() { + return sourceSpecificMembershipKeys != null; + } + + /** + * Returns the {@link MembershipKeyImpl} for this membership. Throws an + * {@code IllegalStateException} if this is not an any-source membership. + */ + public MembershipKeyImpl getAnySourceMembershipKey() { + if (sourceSpecificMembershipKeys != null) { + throw new IllegalStateException( + "There an existing source-specific membership for " + debugId()); + } + return anySourceMembershipKey; + } + + /** + * Returns the {@link MembershipKeyImpl} for the specified {@code sourceAddress}. Throws an + * {@code IllegalStateException} if this is not a source-specific membership. + */ + public MembershipKeyImpl getSourceSpecificMembershipKey(InetAddress sourceAddress) { + if (anySourceMembershipKey != null) { + throw new IllegalStateException("There an existing any-source membership for " + debugId()); + } + return sourceSpecificMembershipKeys.get(sourceAddress); + } + + /** + * Returns {@code true} if there is an existing block for the specified address. Throws an + * {@code IllegalStateException} if this is not an any-source membership. + */ + public boolean isBlocked(InetAddress sourceAddress) { + if (anySourceMembershipKey == null) { + throw new IllegalStateException( + "block()/unblock() are only applicable for any-source memberships: " + debugId()); + } + return blockedSourceAddresses != null && blockedSourceAddresses.contains(sourceAddress); + } + + /** + * Adds a blocked address to this membership. Throws an {@code IllegalStateException} if + * the address is already blocked. Throws an {@code IllegalStateException} if this is not an + * any-source membership. + */ + public void block(InetAddress sourceAddress) { + if (anySourceMembershipKey == null) { + throw new IllegalStateException( + "block() is not supported for source-specific group memberships: " + debugId()); + } + if (blockedSourceAddresses == null) { + blockedSourceAddresses = new HashSet<InetAddress>(); + } + if (!blockedSourceAddresses.add(sourceAddress)) { + throw new IllegalStateException( + "Could not block " + sourceAddress + ": it was already blocked for " + debugId()); + } + } + + /** + * Removes a blocked address from this membership. Throws an {@code IllegalStateException} if + * the address is not blocked. Throws an {@code IllegalStateException} if this is not an + * any-source membership. + */ + public void unblock(InetAddress sourceAddress) { + if (anySourceMembershipKey == null) { + throw new IllegalStateException( + "unblock() is not supported for source-specific group memberships: " + debugId()); + } + if (blockedSourceAddresses == null || !blockedSourceAddresses.remove(sourceAddress)) { + throw new IllegalStateException( + "Could not unblock " + sourceAddress + ": it was not blocked for " + debugId()); + } + } + + public String debugId() { + return "<" + networkInterface + ":" + groupAddress + ">"; + } + + } + + /** An identifier for a multicast group membership, independent of membership type. */ + private static final class Id { + + private final InetAddress groupAddress; + private final NetworkInterface networkInterface; + + public Id(NetworkInterface networkInterface, InetAddress groupAddress) { + this.groupAddress = groupAddress; + this.networkInterface = networkInterface; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Id)) { + return false; + } + + Id id = (Id) o; + + if (!groupAddress.equals(id.groupAddress)) { + return false; + } + if (!networkInterface.equals(id.networkInterface)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = groupAddress.hashCode(); + result = 31 * result + networkInterface.hashCode(); + return result; + } + } +} diff --git a/luni/src/main/java/java/nio/ServerSocketChannelImpl.java b/luni/src/main/java/java/nio/ServerSocketChannelImpl.java index 3d6cd22..5adea1a 100644 --- a/luni/src/main/java/java/nio/ServerSocketChannelImpl.java +++ b/luni/src/main/java/java/nio/ServerSocketChannelImpl.java @@ -32,6 +32,7 @@ import java.nio.channels.IllegalBlockingModeException; import java.nio.channels.NotYetBoundException; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +import java.nio.channels.UnresolvedAddressException; import java.nio.channels.UnsupportedAddressTypeException; import java.nio.channels.spi.SelectorProvider; import java.util.Set; @@ -67,8 +68,14 @@ final class ServerSocketChannelImpl extends ServerSocketChannel implements FileD if (socket.isBound()) { throw new AlreadyBoundException(); } - if (localAddr != null && !(localAddr instanceof InetSocketAddress)) { - throw new UnsupportedAddressTypeException(); + if (localAddr != null) { + if (!(localAddr instanceof InetSocketAddress)) { + throw new UnsupportedAddressTypeException(); + } + InetSocketAddress localInetAddress = (InetSocketAddress) localAddr; + if (localInetAddress.isUnresolved()) { + throw new UnresolvedAddressException(); + } } socket.bind(localAddr, backlog); diff --git a/luni/src/main/java/java/nio/SocketChannelImpl.java b/luni/src/main/java/java/nio/SocketChannelImpl.java index 5d3db43..d1e77d6 100644 --- a/luni/src/main/java/java/nio/SocketChannelImpl.java +++ b/luni/src/main/java/java/nio/SocketChannelImpl.java @@ -157,6 +157,9 @@ class SocketChannelImpl extends SocketChannel implements FileDescriptorChannel { } InetSocketAddress localAddress = (InetSocketAddress) local; + if (localAddress.isUnresolved()) { + throw new UnresolvedAddressException(); + } IoBridge.bind(fd, localAddress.getAddress(), localAddress.getPort()); onBind(true /* updateSocketState */); return this; diff --git a/luni/src/main/java/java/nio/channels/DatagramChannel.java b/luni/src/main/java/java/nio/channels/DatagramChannel.java index 2aa228d..3a5d1cc 100644 --- a/luni/src/main/java/java/nio/channels/DatagramChannel.java +++ b/luni/src/main/java/java/nio/channels/DatagramChannel.java @@ -19,6 +19,8 @@ package java.nio.channels; import java.io.IOException; import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.NetworkInterface; import java.net.SocketAddress; import java.net.SocketOption; import java.nio.ByteBuffer; @@ -42,7 +44,7 @@ import java.util.Set; * same time. */ public abstract class DatagramChannel extends AbstractSelectableChannel - implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel { + implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel { /** * Constructs a new {@code DatagramChannel}. @@ -130,6 +132,24 @@ public abstract class DatagramChannel extends AbstractSelectableChannel throw new UnsupportedOperationException("Subclasses must override this method"); } + /** @hide Until ready for a public API change */ + @Override + public MembershipKey join(InetAddress groupAddress, NetworkInterface networkInterface) + throws IOException { + // This method was added for interoperability with Java 7, where it is abstract. It is + // concrete here to avoid breaking existing Android applications that extend this class. + throw new UnsupportedOperationException("Subclasses must override this method"); + } + + /** @hide Until ready for a public API change */ + @Override + public MembershipKey join(InetAddress groupAddress, NetworkInterface networkInterface, + InetAddress sourceAddress) throws IOException { + // This method was added for interoperability with Java 7, where it is abstract. It is + // concrete here to avoid breaking existing Android applications that extend this class. + throw new UnsupportedOperationException("Subclasses must override this method"); + } + /** * Returns whether this channel's socket is connected or not. * diff --git a/luni/src/main/java/java/nio/channels/MembershipKey.java b/luni/src/main/java/java/nio/channels/MembershipKey.java new file mode 100644 index 0000000..18ff92d --- /dev/null +++ b/luni/src/main/java/java/nio/channels/MembershipKey.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package java.nio.channels; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; + +/** + * A token produced as the result of joining a multicast group with + * {@link DatagramChannel#join(java.net.InetAddress, java.net.NetworkInterface)} or + * {@link DatagramChannel#join(java.net.InetAddress, java.net.NetworkInterface, + * java.net.InetAddress)}. + * + * <p>A multicast group membership can be source-specific or any-source. Source-specific memberships + * only allow datagrams from a single source address to be received. Any-source memberships + * initially allow datagrams from any source address to be received, but may have individual unicast + * IP addresses blocked via {@link #block(java.net.InetAddress)}. Any-source membership keys return + * {@code null} from {@link #sourceAddress()}. + * + * <p>See <a href="http://tools.ietf.org/html/rfc3678">RFC 3678: Socket Interface Extensions for + * Multicast Source Filters</a> for concepts and terminology associated with multicast membership. + * + * @since 1.7 + * @hide Until ready for a public API change + */ +public abstract class MembershipKey { + + protected MembershipKey() {} + + /** + * Returns {@code true} until the membership is dropped with {@link #drop()} or the associated + * channel is closed. + */ + public abstract boolean isValid(); + + /** + * Drops this membership from the multicast group, invalidating this key. + */ + public abstract void drop(); + + /** + * Blocks datagrams from the specified source address; after this call any datagrams received from + * the address will be discarded. Blocking an already-blocked source address has no effect. A + * blocked address can be unblocked by calling {@link #unblock(java.net.InetAddress)}. + * + * <p>The block may not take effect instantaneously: datagrams that are already buffered by the + * underlying OS may still be delivered. + * + * <p>There is an OS-level limit on the number of source addresses that can be block for a given + * {@code groupAddress}, {@code networkInterface} pair. This is typically 10. Attempts to add + * more than this result in a {@code SocketException}. + * + * <p>If this membership key is source-specific an {@link IllegalStateException} is thrown. + * + * @throws IllegalStateException + * if this membership key is no longer valid or is of the wrong type + * @throws IllegalArgumentException + * if the source address is not unicast address of the same type as the multicast group + * address supplied when the group was joined + * @throws IOException + * if an I/O error occurs. + */ + public abstract MembershipKey block(InetAddress source) throws IOException; + + /** + * Unblocks datagrams from the specified source address that were previously blocked with a call + * to {@link #block(java.net.InetAddress)}; after this call any datagrams received from the + * address will be received. Unblocking an address that is not currently blocked throws an + * {@code IllegalStateException}. + * + * <p>If this membership key is source-specific an {@link IllegalStateException} is thrown. + * + * @throws IllegalStateException + * if this membership key is no longer valid or is of the wrong type, or the address is + * not currently blocked + * @throws IllegalArgumentException + * if the source address is not unicast address of the same type as the multicast group + * address supplied when the group was joined + */ + public abstract MembershipKey unblock(InetAddress source); + + /** + * Returns the {@code MulticastChannel} associated with this key. Continues returning the value + * even when the key has been invalidated. + */ + public abstract MulticastChannel channel(); + + /** + * Returns the multicast group address associated with this key. Continues returning the value + * even when the key has been invalidated. + */ + public abstract InetAddress group(); + + /** + * Returns the network interface associated with this key. Continues returning the value + * even when the key has been invalidated. + */ + public abstract NetworkInterface networkInterface(); + + /** + * Returns the source address associated with this key if the membership is source-specific. + * Returns {@code null} if the membership is any-source. Continues returning the value + * even when the key has been invalidated. + */ + public abstract InetAddress sourceAddress(); +} diff --git a/luni/src/main/java/java/nio/channels/MulticastChannel.java b/luni/src/main/java/java/nio/channels/MulticastChannel.java new file mode 100644 index 0000000..41ef501 --- /dev/null +++ b/luni/src/main/java/java/nio/channels/MulticastChannel.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package java.nio.channels; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; + +/** + * A type of {@link NetworkChannel} that supports IP multicasting. IP multicasting allows for + * efficient routing of an IP datagram to multiple hosts. Hosts wishing to receive multicast + * datagrams join a multicast group identified by a multicast IP address. + * + * <p>Any datagram socket can be used to <em>send</em> to a multicast group: senders <em>do not</em> + * have to be a member of the group. + * + * <p>See <a href="http://www.ietf.org/rfc/rfc2236.txt">RFC 2236: Internet Group Management + * Protocol, Version 2</a> and <a href="http://www.ietf.org/rfc/rfc3376.txt">RFC 3376: Internet + * Group Management Protocol, Version 3</a> for network-level information regarding IPv4 group + * membership. See <a href="http://www.ietf.org/rfc/rfc2710.txt">RFC 2710: Multicast Listener + * Discovery (MLD) for IPv6</a> and <a href="http://www.ietf.org/rfc/rfc3810.txt">RFC 3810: + * Multicast Listener Discovery Version 2 (MLDv2) for IPv6</a> for equivalent IPv6 information. + * + * <p>See <a href="http://tools.ietf.org/html/rfc3678">RFC 3678: Socket Interface Extensions for + * Multicast Source Filters</a> for concepts and terminology associated with multicast membership. + * + * <p>IP multicast requires support from network infrastructure; networks may not support + * all features of IP multicast. + * + * <p>A channel can be restricted to send multicast datagrams through a specific + * {@link NetworkInterface} by using {@link #setOption(java.net.SocketOption, Object)} with + * {@link java.net.StandardSocketOptions#IP_MULTICAST_IF}. + * + * <p>A channel may or may not receive multicast datagrams sent from this host, determined by the + * {@link java.net.StandardSocketOptions#IP_MULTICAST_LOOP} option. + * + * <p>The time-to-live for multicast datagrams can be set using the + * {@link java.net.StandardSocketOptions#IP_MULTICAST_TTL} option. + * + * <p>Usually multicast channels should have {@link java.net.StandardSocketOptions#SO_REUSEADDR} + * set to {@code true} before binding to enable multiple sockets on this host to be members of + * the same multicast group. + * + * <p>Typically multicast channels are {@link NetworkChannel#bind bound} to a wildcard address + * such as "0.0.0.0" (IPv4) or "::" (IPv6). They may also be bound to a multicast group address. + * Binding to a multicast group address restricts datagrams received to only those sent to the + * multicast group. When the wildcard address is used the socket may join multiple groups and also + * receive non-multicast datagrams sent directly to the host. The port the channel is bound to is + * important: only datagrams sent to the group on that port will be received. + * + * <p>Having bound a channel, the group can be joined. Memberships are either "any-source" or + * "source-specific". The type of membership is determined by the variant of {@code join} that is + * used. See {@link #join(java.net.InetAddress, java.net.NetworkInterface)} and + * {@link #join(java.net.InetAddress, java.net.NetworkInterface, java.net.InetAddress)} for more + * information. + * + * @since 1.7 + * @hide Until ready for a public API change + */ +public interface MulticastChannel extends NetworkChannel { + + // @hide Until ready for a public API change + // /** + // * {@inheritDoc} + // * + // * If the channel is currently part of one or more multicast groups then the memberships are + // * dropped and any associated {@code MembershipKey} objects are invalidated. + // */ + void close() throws IOException; + + /** + * Creates an any-source membership to the {@code groupAddress} on the specified + * {@code networkInterface}. Returns a {@code MembershipKey} that can be used to modify or + * {@link MembershipKey#drop()} the membership. See {@link MembershipKey#block(InetAddress)} and + * {@link MembershipKey#unblock(InetAddress)} for methods to modify source-address + * filtering. + * + * <p>A channel may join several groups. Each membership is network interface-specific: an + * application must join the group for each applicable network interface to receive datagrams. + * + * <p>Any-source and source-specific memberships cannot be mixed for a group address on a given + * network interface. An {@code IllegalStateException} will be thrown if joins of different types + * are attempted for a given {@code groupAddress}, {@code networkInterface} pair. Joining a + * multicast group with the same arguments as an existing, valid membership returns the same + * {@code MembershipKey}. + * + * <p>There is an OS-level limit on the number of multicast groups a process can join. + * This is typically 20. Attempts to join more than this result in a {@code SocketException}. + * + * @param groupAddress the multicast group address to join + * @param networkInterface the network address to join with + * @throws IllegalArgumentException + * if the group address is not a multicast address or the network interface does not + * support multicast + * @throws IllegalStateException + * if the channel already has a source-specific membership for the group/network interface + * @throws ClosedChannelException + * if the channel is closed + * @throws IOException + * if some other I/O error occurs + * @hide Until ready for a public API change + */ + MembershipKey join(InetAddress groupAddress, NetworkInterface networkInterface) + throws IOException; + + /** + * Creates a source-specific membership to the {@code groupAddress} on the specified + * {@code networkInterface} filtered by the {@code sourceAddress}. Returns a + * {@code MembershipKey} that can be used to {@link MembershipKey#drop()} the membership. + * + * <p>A channel may join several groups. Each membership is network interface-specific: an + * application must join the group for each applicable network interface to receive datagrams. + * + * <p>Any-source and source-specific memberships cannot be mixed for a group address on a given + * network interface. An {@code IllegalStateException} will be thrown if joins of different types + * are attempted for a given {@code groupAddress}, {@code networkInterface} pair. Joining a + * multicast group with the same arguments as an existing, valid membership returns the same + * {@code MembershipKey}. + * + * <p>There is an OS-level limit on the number of multicast groups a process can join. + * This is typically 20. Attempts to join more than this result in a {@code SocketException}. + * + * <p>There is an OS-level limit on the number of source addresses that can be joined for a given + * {@code groupAddress}, {@code networkInterface} pair. This is typically 10. Attempts to add + * more than this result in a {@code SocketException}. + * + * @param groupAddress the multicast group address to join + * @param networkInterface the network address to join with + * @param sourceAddress the source address to restrict datagrams to + * @throws IllegalArgumentException + * if the group address is not a multicast address, the network interface does not + * support multicast, or the {@code groupAddress} and {@code sourceAddress} are not of + * compatible types + * @throws IllegalStateException + * if the channel already has a source-specific membership for the group/network interface + * @throws ClosedChannelException + * if the channel is closed + * @throws IOException + * if some other I/O error occurs + * @hide Until ready for a public API change + */ + MembershipKey join( + InetAddress groupAddress, NetworkInterface networkInterface, InetAddress sourceAddress) + throws IOException; + +} diff --git a/luni/src/main/java/libcore/io/ForwardingOs.java b/luni/src/main/java/libcore/io/ForwardingOs.java index 64734a0..2de35e9 100644 --- a/luni/src/main/java/libcore/io/ForwardingOs.java +++ b/luni/src/main/java/libcore/io/ForwardingOs.java @@ -124,6 +124,7 @@ public class ForwardingOs implements Os { public void setsockoptInt(FileDescriptor fd, int level, int option, int value) throws ErrnoException { os.setsockoptInt(fd, level, option, value); } public void setsockoptIpMreqn(FileDescriptor fd, int level, int option, int value) throws ErrnoException { os.setsockoptIpMreqn(fd, level, option, value); } public void setsockoptGroupReq(FileDescriptor fd, int level, int option, StructGroupReq value) throws ErrnoException { os.setsockoptGroupReq(fd, level, option, value); } + public void setsockoptGroupSourceReq(FileDescriptor fd, int level, int option, StructGroupSourceReq value) throws ErrnoException { os.setsockoptGroupSourceReq(fd, level, option, value); } public void setsockoptLinger(FileDescriptor fd, int level, int option, StructLinger value) throws ErrnoException { os.setsockoptLinger(fd, level, option, value); } public void setsockoptTimeval(FileDescriptor fd, int level, int option, StructTimeval value) throws ErrnoException { os.setsockoptTimeval(fd, level, option, value); } public void setuid(int uid) throws ErrnoException { os.setuid(uid); } diff --git a/luni/src/main/java/libcore/io/IoBridge.java b/luni/src/main/java/libcore/io/IoBridge.java index a48a0ad..08d4837 100644 --- a/luni/src/main/java/libcore/io/IoBridge.java +++ b/luni/src/main/java/libcore/io/IoBridge.java @@ -71,16 +71,20 @@ public final class IoBridge { public static void bind(FileDescriptor fd, InetAddress address, int port) throws SocketException { - if (address instanceof Inet6Address && ((Inet6Address) address).getScopeId() == 0) { - // Linux won't let you bind a link-local address without a scope id. Find one. - NetworkInterface nif = NetworkInterface.getByInetAddress(address); - if (nif == null) { - throw new SocketException("Can't bind to a link-local address without a scope id: " + address); - } - try { - address = Inet6Address.getByAddress(address.getHostName(), address.getAddress(), nif.getIndex()); - } catch (UnknownHostException ex) { - throw new AssertionError(ex); // Can't happen. + if (address instanceof Inet6Address) { + Inet6Address inet6Address = (Inet6Address) address; + if (inet6Address.getScopeId() == 0 && inet6Address.isLinkLocalAddress()) { + // Linux won't let you bind a link-local address without a scope id. + // Find one. + NetworkInterface nif = NetworkInterface.getByInetAddress(address); + if (nif == null) { + throw new SocketException("Can't bind to a link-local address without a scope id: " + address); + } + try { + address = Inet6Address.getByAddress(address.getHostName(), address.getAddress(), nif.getIndex()); + } catch (UnknownHostException ex) { + throw new AssertionError(ex); // Can't happen. + } } } try { @@ -225,6 +229,10 @@ public final class IoBridge { // Socket options used by java.net but not exposed in SocketOptions. public static final int JAVA_MCAST_JOIN_GROUP = 19; public static final int JAVA_MCAST_LEAVE_GROUP = 20; + public static final int JAVA_MCAST_JOIN_SOURCE_GROUP = 21; + public static final int JAVA_MCAST_LEAVE_SOURCE_GROUP = 22; + public static final int JAVA_MCAST_BLOCK_SOURCE = 23; + public static final int JAVA_MCAST_UNBLOCK_SOURCE = 24; public static final int JAVA_IP_MULTICAST_TTL = 17; /** @@ -368,16 +376,46 @@ public final class IoBridge { return; case IoBridge.JAVA_MCAST_JOIN_GROUP: case IoBridge.JAVA_MCAST_LEAVE_GROUP: + { StructGroupReq groupReq = (StructGroupReq) value; int level = (groupReq.gr_group instanceof Inet4Address) ? IPPROTO_IP : IPPROTO_IPV6; int op = (option == JAVA_MCAST_JOIN_GROUP) ? MCAST_JOIN_GROUP : MCAST_LEAVE_GROUP; Libcore.os.setsockoptGroupReq(fd, level, op, groupReq); return; + } + case IoBridge.JAVA_MCAST_JOIN_SOURCE_GROUP: + case IoBridge.JAVA_MCAST_LEAVE_SOURCE_GROUP: + case IoBridge.JAVA_MCAST_BLOCK_SOURCE: + case IoBridge.JAVA_MCAST_UNBLOCK_SOURCE: + { + StructGroupSourceReq groupSourceReq = (StructGroupSourceReq) value; + int level = (groupSourceReq.gsr_group instanceof Inet4Address) + ? IPPROTO_IP : IPPROTO_IPV6; + int op = getGroupSourceReqOp(option); + Libcore.os.setsockoptGroupSourceReq(fd, level, op, groupSourceReq); + return; + } default: throw new SocketException("Unknown socket option: " + option); } } + private static int getGroupSourceReqOp(int javaValue) { + switch (javaValue) { + case IoBridge.JAVA_MCAST_JOIN_SOURCE_GROUP: + return MCAST_JOIN_SOURCE_GROUP; + case IoBridge.JAVA_MCAST_LEAVE_SOURCE_GROUP: + return MCAST_LEAVE_SOURCE_GROUP; + case IoBridge.JAVA_MCAST_BLOCK_SOURCE: + return MCAST_BLOCK_SOURCE; + case IoBridge.JAVA_MCAST_UNBLOCK_SOURCE: + return MCAST_UNBLOCK_SOURCE; + default: + throw new AssertionError( + "Unknown java value for setsocketopt op lookup: " + javaValue); + } + } + /** * java.io only throws FileNotFoundException when opening files, regardless of what actually * went wrong. Additionally, java.io is more restrictive than POSIX when it comes to opening diff --git a/luni/src/main/java/libcore/io/Os.java b/luni/src/main/java/libcore/io/Os.java index 426f2c1..a4541a7 100644 --- a/luni/src/main/java/libcore/io/Os.java +++ b/luni/src/main/java/libcore/io/Os.java @@ -117,6 +117,7 @@ public interface Os { public void setsockoptInt(FileDescriptor fd, int level, int option, int value) throws ErrnoException; public void setsockoptIpMreqn(FileDescriptor fd, int level, int option, int value) throws ErrnoException; public void setsockoptGroupReq(FileDescriptor fd, int level, int option, StructGroupReq value) throws ErrnoException; + public void setsockoptGroupSourceReq(FileDescriptor fd, int level, int option, StructGroupSourceReq value) throws ErrnoException; public void setsockoptLinger(FileDescriptor fd, int level, int option, StructLinger value) throws ErrnoException; public void setsockoptTimeval(FileDescriptor fd, int level, int option, StructTimeval value) throws ErrnoException; public void setuid(int uid) throws ErrnoException; diff --git a/luni/src/main/java/libcore/io/OsConstants.java b/luni/src/main/java/libcore/io/OsConstants.java index 0eb5850..49b0182 100644 --- a/luni/src/main/java/libcore/io/OsConstants.java +++ b/luni/src/main/java/libcore/io/OsConstants.java @@ -250,6 +250,10 @@ public final class OsConstants { public static final int MAP_SHARED = placeholder(); public static final int MCAST_JOIN_GROUP = placeholder(); public static final int MCAST_LEAVE_GROUP = placeholder(); + public static final int MCAST_JOIN_SOURCE_GROUP = placeholder(); + public static final int MCAST_LEAVE_SOURCE_GROUP = placeholder(); + public static final int MCAST_BLOCK_SOURCE = placeholder(); + public static final int MCAST_UNBLOCK_SOURCE = placeholder(); public static final int MCL_CURRENT = placeholder(); public static final int MCL_FUTURE = placeholder(); public static final int MSG_CTRUNC = placeholder(); diff --git a/luni/src/main/java/libcore/io/Posix.java b/luni/src/main/java/libcore/io/Posix.java index 35d3784..c6f13ea 100644 --- a/luni/src/main/java/libcore/io/Posix.java +++ b/luni/src/main/java/libcore/io/Posix.java @@ -168,6 +168,7 @@ public final class Posix implements Os { public native void setsockoptInt(FileDescriptor fd, int level, int option, int value) throws ErrnoException; public native void setsockoptIpMreqn(FileDescriptor fd, int level, int option, int value) throws ErrnoException; public native void setsockoptGroupReq(FileDescriptor fd, int level, int option, StructGroupReq value) throws ErrnoException; + public native void setsockoptGroupSourceReq(FileDescriptor fd, int level, int option, StructGroupSourceReq value) throws ErrnoException; public native void setsockoptLinger(FileDescriptor fd, int level, int option, StructLinger value) throws ErrnoException; public native void setsockoptTimeval(FileDescriptor fd, int level, int option, StructTimeval value) throws ErrnoException; public native void setuid(int uid) throws ErrnoException; diff --git a/luni/src/main/java/libcore/io/StructGroupSourceReq.java b/luni/src/main/java/libcore/io/StructGroupSourceReq.java new file mode 100644 index 0000000..3ac94b8 --- /dev/null +++ b/luni/src/main/java/libcore/io/StructGroupSourceReq.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.io; + +import java.net.InetAddress; + +/** + * Corresponds to C's {@code struct group_source_req}. + */ +public final class StructGroupSourceReq { + + public final int gsr_interface; + + public final InetAddress gsr_group; + + public final InetAddress gsr_source; + + public StructGroupSourceReq(int gsr_interface, InetAddress gsr_group, InetAddress gsr_source) { + this.gsr_interface = gsr_interface; + this.gsr_group = gsr_group; + this.gsr_source = gsr_source; + } + + @Override + public String toString() { + return "StructGroupSourceReq[gsr_interface=" + gsr_interface + ",gsr_group=" + gsr_group + + ",gsr_source=" + gsr_source + "]"; + } +} diff --git a/luni/src/main/native/NetworkUtilities.h b/luni/src/main/native/NetworkUtilities.h index 28e9fa5..6b720d4 100644 --- a/luni/src/main/native/NetworkUtilities.h +++ b/luni/src/main/native/NetworkUtilities.h @@ -35,7 +35,7 @@ bool inetAddressToSockaddr(JNIEnv* env, jobject inetAddress, int port, // An Inet4Address will be converted to a sockaddr_in. This is probably only useful for // getnameinfo(2), where we'll be presenting the result to the user and the user may actually // care whether the original address was pure IPv4 or an IPv4-mapped IPv6 address, and -// for the MCAST_JOIN_GROUP socket option. +// for the MCAST_JOIN_GROUP, MCAST_LEAVE_GROUP, and other multicast socket options. bool inetAddressToSockaddrVerbatim(JNIEnv* env, jobject inetAddress, int port, sockaddr_storage& ss, socklen_t& sa_len); diff --git a/luni/src/main/native/libcore_io_OsConstants.cpp b/luni/src/main/native/libcore_io_OsConstants.cpp index f9e9eed..cae1703 100644 --- a/luni/src/main/native/libcore_io_OsConstants.cpp +++ b/luni/src/main/native/libcore_io_OsConstants.cpp @@ -310,6 +310,18 @@ static void OsConstants_initConstants(JNIEnv* env, jclass c) { #if defined(MCAST_LEAVE_GROUP) initConstant(env, c, "MCAST_LEAVE_GROUP", MCAST_LEAVE_GROUP); #endif +#if defined(MCAST_JOIN_SOURCE_GROUP) + initConstant(env, c, "MCAST_JOIN_SOURCE_GROUP", MCAST_JOIN_SOURCE_GROUP); +#endif +#if defined(MCAST_LEAVE_SOURCE_GROUP) + initConstant(env, c, "MCAST_LEAVE_SOURCE_GROUP", MCAST_LEAVE_SOURCE_GROUP); +#endif +#if defined(MCAST_BLOCK_SOURCE) + initConstant(env, c, "MCAST_BLOCK_SOURCE", MCAST_BLOCK_SOURCE); +#endif +#if defined(MCAST_UNBLOCK_SOURCE) + initConstant(env, c, "MCAST_UNBLOCK_SOURCE", MCAST_UNBLOCK_SOURCE); +#endif initConstant(env, c, "MCL_CURRENT", MCL_CURRENT); initConstant(env, c, "MCL_FUTURE", MCL_FUTURE); initConstant(env, c, "MSG_CTRUNC", MSG_CTRUNC); diff --git a/luni/src/main/native/libcore_io_Posix.cpp b/luni/src/main/native/libcore_io_Posix.cpp index 1f38a57..b9f16a7 100644 --- a/luni/src/main/native/libcore_io_Posix.cpp +++ b/luni/src/main/native/libcore_io_Posix.cpp @@ -1193,6 +1193,7 @@ static void Posix_setsockoptInt(JNIEnv* env, jobject, jobject javaFd, jint level // Mac OS didn't support modern multicast APIs until 10.7. static void Posix_setsockoptIpMreqn(JNIEnv*, jobject, jobject, jint, jint, jint) { abort(); } static void Posix_setsockoptGroupReq(JNIEnv*, jobject, jobject, jint, jint, jobject) { abort(); } +static void Posix_setsockoptGroupSourceReq(JNIEnv*, jobject, jobject, jint, jint, jobject) { abort(); } #else static void Posix_setsockoptIpMreqn(JNIEnv* env, jobject, jobject javaFd, jint level, jint option, jint value) { ip_mreqn req; @@ -1221,6 +1222,7 @@ static void Posix_setsockoptGroupReq(JNIEnv* env, jobject, jobject javaFd, jint if (rc == -1 && errno == EINVAL) { // Maybe we're a 32-bit binary talking to a 64-bit kernel? // glibc doesn't automatically handle this. + // http://sourceware.org/bugzilla/show_bug.cgi?id=12080 struct group_req64 { uint32_t gr_interface; uint32_t my_padding; @@ -1233,6 +1235,48 @@ static void Posix_setsockoptGroupReq(JNIEnv* env, jobject, jobject javaFd, jint } throwIfMinusOne(env, "setsockopt", rc); } + +static void Posix_setsockoptGroupSourceReq(JNIEnv* env, jobject, jobject javaFd, jint level, jint option, jobject javaGroupSourceReq) { + socklen_t sa_len; + struct group_source_req req; + memset(&req, 0, sizeof(req)); + + static jfieldID gsrInterfaceFid = env->GetFieldID(JniConstants::structGroupSourceReqClass, "gsr_interface", "I"); + req.gsr_interface = env->GetIntField(javaGroupSourceReq, gsrInterfaceFid); + // Get the IPv4 or IPv6 multicast address to join or leave. + static jfieldID gsrGroupFid = env->GetFieldID(JniConstants::structGroupSourceReqClass, "gsr_group", "Ljava/net/InetAddress;"); + ScopedLocalRef<jobject> javaGroup(env, env->GetObjectField(javaGroupSourceReq, gsrGroupFid)); + if (!inetAddressToSockaddrVerbatim(env, javaGroup.get(), 0, req.gsr_group, sa_len)) { + return; + } + + // Get the IPv4 or IPv6 multicast address to add to the filter. + static jfieldID gsrSourceFid = env->GetFieldID(JniConstants::structGroupSourceReqClass, "gsr_source", "Ljava/net/InetAddress;"); + ScopedLocalRef<jobject> javaSource(env, env->GetObjectField(javaGroupSourceReq, gsrSourceFid)); + if (!inetAddressToSockaddrVerbatim(env, javaSource.get(), 0, req.gsr_source, sa_len)) { + return; + } + + int fd = jniGetFDFromFileDescriptor(env, javaFd); + int rc = TEMP_FAILURE_RETRY(setsockopt(fd, level, option, &req, sizeof(req))); + if (rc == -1 && errno == EINVAL) { + // Maybe we're a 32-bit binary talking to a 64-bit kernel? + // glibc doesn't automatically handle this. + // http://sourceware.org/bugzilla/show_bug.cgi?id=12080 + struct group_source_req64 { + uint32_t gsr_interface; + uint32_t my_padding; + sockaddr_storage gsr_group; + sockaddr_storage gsr_source; + }; + group_source_req64 req64; + req64.gsr_interface = req.gsr_interface; + memcpy(&req64.gsr_group, &req.gsr_group, sizeof(req.gsr_group)); + memcpy(&req64.gsr_source, &req.gsr_source, sizeof(req.gsr_source)); + rc = TEMP_FAILURE_RETRY(setsockopt(fd, level, option, &req64, sizeof(req64))); + } + throwIfMinusOne(env, "setsockopt", rc); +} #endif static void Posix_setsockoptLinger(JNIEnv* env, jobject, jobject javaFd, jint level, jint option, jobject javaLinger) { @@ -1471,6 +1515,7 @@ static JNINativeMethod gMethods[] = { NATIVE_METHOD(Posix, setsockoptInt, "(Ljava/io/FileDescriptor;III)V"), NATIVE_METHOD(Posix, setsockoptIpMreqn, "(Ljava/io/FileDescriptor;III)V"), NATIVE_METHOD(Posix, setsockoptGroupReq, "(Ljava/io/FileDescriptor;IILlibcore/io/StructGroupReq;)V"), + NATIVE_METHOD(Posix, setsockoptGroupSourceReq, "(Ljava/io/FileDescriptor;IILlibcore/io/StructGroupSourceReq;)V"), NATIVE_METHOD(Posix, setsockoptLinger, "(Ljava/io/FileDescriptor;IILlibcore/io/StructLinger;)V"), NATIVE_METHOD(Posix, setsockoptTimeval, "(Ljava/io/FileDescriptor;IILlibcore/io/StructTimeval;)V"), NATIVE_METHOD(Posix, setuid, "(I)V"), diff --git a/luni/src/test/java/libcore/java/nio/channels/DatagramChannelMulticastTest.java b/luni/src/test/java/libcore/java/nio/channels/DatagramChannelMulticastTest.java new file mode 100644 index 0000000..9882af5 --- /dev/null +++ b/luni/src/test/java/libcore/java/nio/channels/DatagramChannelMulticastTest.java @@ -0,0 +1,1073 @@ +package libcore.java.nio.channels; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.StandardSocketOptions; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.MembershipKey; +import java.util.ArrayList; +import java.util.Enumeration; + +/** + * Tests associated with multicast behavior of DatagramChannel. + */ +public class DatagramChannelMulticastTest extends TestCase { + + private static InetAddress lookup(String s) { + try { + return InetAddress.getByName(s); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + // These IP addresses aren't inherently "good" or "bad"; they're just used like that. + // We use the "good" addresses for our actual group, and the "bad" addresses are for + // a group that we won't actually set up. + private static final InetAddress GOOD_MULTICAST_IPv4 = lookup("239.255.0.1"); + private static final InetAddress BAD_MULTICAST_IPv4 = lookup("239.255.0.2"); + private static final InetAddress GOOD_MULTICAST_IPv6 = lookup("ff05::7:7"); + private static final InetAddress BAD_MULTICAST_IPv6 = lookup("ff05::7:8"); + + // Special addresses. + private static final InetAddress WILDCARD_IPv4 = lookup("0.0.0.0"); + private static final InetAddress WILDCARD_IPv6 = lookup("::"); + + // Arbitrary unicast addresses. Used when the value doesn't actually matter. e.g. for source + // filters. + private static final InetAddress UNICAST_IPv4_1 = lookup("192.168.1.1"); + private static final InetAddress UNICAST_IPv4_2 = lookup("192.168.1.2"); + private static final InetAddress UNICAST_IPv6_1 = lookup("2001:db8::1"); + private static final InetAddress UNICAST_IPv6_2 = lookup("2001:db8::2"); + + private NetworkInterface networkInterface1; + private NetworkInterface IPV6networkInterface1; + private NetworkInterface loopbackInterface; + + @Override + protected void setUp() throws Exception { + // The loopback interface isn't actually useful for sending/receiving multicast messages + // but it can be used as a dummy for tests where that does not matter. + loopbackInterface = NetworkInterface.getByInetAddress(InetAddress.getLoopbackAddress()); + assertNotNull(loopbackInterface); + assertTrue(loopbackInterface.isLoopback()); + assertFalse(loopbackInterface.supportsMulticast()); + + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); + + // only consider interfaces that have addresses associated with them. + // Otherwise tests don't work so well + if (interfaces != null) { + boolean atLeastOneInterface = false; + while (interfaces.hasMoreElements() && (atLeastOneInterface == false)) { + networkInterface1 = interfaces.nextElement(); + if (willWorkForMulticast(networkInterface1)) { + atLeastOneInterface = true; + } + } + + assertTrue("Test environment must have at least one environment capable of multicast", + atLeastOneInterface); + + // Find the first multicast-compatible interface that supports IPV6 if one exists + interfaces = NetworkInterface.getNetworkInterfaces(); + + boolean found = false; + while (interfaces.hasMoreElements() && !found) { + NetworkInterface nextInterface = interfaces.nextElement(); + if (willWorkForMulticast(nextInterface)) { + Enumeration<InetAddress> addresses = nextInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + final InetAddress nextAddress = addresses.nextElement(); + if (nextAddress instanceof Inet6Address) { + IPV6networkInterface1 = nextInterface; + found = true; + break; + } + } + } + } + } + } + + public void test_open() throws IOException { + DatagramChannel dc = DatagramChannel.open(); + + // Unlike MulticastSocket, DatagramChannel has SO_REUSEADDR set to false by default. + assertFalse(dc.getOption(StandardSocketOptions.SO_REUSEADDR)); + + assertNull(dc.getLocalAddress()); + assertTrue(dc.isOpen()); + assertFalse(dc.isConnected()); + } + + public void test_bind_null() throws Exception { + DatagramChannel dc = createReceiverChannel(); + assertNotNull(dc.getLocalAddress()); + assertTrue(dc.isOpen()); + assertFalse(dc.isConnected()); + + dc.close(); + try { + dc.getLocalAddress(); + fail(); + } catch (ClosedChannelException expected) { + } + assertFalse(dc.isOpen()); + assertFalse(dc.isConnected()); + } + + public void test_joinAnySource_afterClose() throws Exception { + DatagramChannel dc = createReceiverChannel(); + dc.close(); + try { + dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + fail(); + } catch (ClosedChannelException expected) { + } + } + + public void test_joinAnySource_nullGroupAddress() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(null, networkInterface1); + fail(); + } catch (NullPointerException expected) { + } + dc.close(); + } + + public void test_joinAnySource_nullNetworkInterface() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(GOOD_MULTICAST_IPv4, null); + fail(); + } catch (NullPointerException expected) { + } + dc.close(); + } + + public void test_joinAnySource_nonMulticastGroupAddress_IPv4() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(UNICAST_IPv4_1, networkInterface1); + fail(); + } catch (IllegalArgumentException expected) { + } + dc.close(); + } + + public void test_joinAnySource_nonMulticastGroupAddress_IPv6() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(UNICAST_IPv6_1, networkInterface1); + fail(); + } catch (IllegalArgumentException expected) { + } + dc.close(); + } + + public void test_joinAnySource_IPv4() throws Exception { + test_joinAnySource(GOOD_MULTICAST_IPv4, BAD_MULTICAST_IPv4); + } + + public void test_joinAnySource_IPv6() throws Exception { + test_joinAnySource(GOOD_MULTICAST_IPv6, BAD_MULTICAST_IPv6); + } + + private void test_joinAnySource(InetAddress group, InetAddress group2) throws Exception { + // Set up a receiver join the group on networkInterface1 + DatagramChannel receiverChannel = createReceiverChannel(); + InetSocketAddress localAddress = (InetSocketAddress) receiverChannel.getLocalAddress(); + receiverChannel.join(group, networkInterface1); + + String msg = "Hello World"; + sendMulticastMessage(group, localAddress.getPort(), msg); + + // now verify that we received the data as expected + ByteBuffer recvBuffer = ByteBuffer.allocate(100); + SocketAddress sourceAddress = receiverChannel.receive(recvBuffer); + assertNotNull(sourceAddress); + assertEquals(msg, new String(recvBuffer.array(), 0, recvBuffer.position())); + + // now verify that we didn't receive the second message + String msg2 = "Hello World - Different Group"; + sendMulticastMessage(group2, localAddress.getPort(), msg2); + recvBuffer.position(0); + SocketAddress sourceAddress2 = receiverChannel.receive(recvBuffer); + assertNull(sourceAddress2); + + receiverChannel.close(); + } + + public void test_joinAnySource_processLimit() throws Exception { + DatagramChannel dc = createReceiverChannel(); + for (byte i = 1; i <= 25; i++) { + InetAddress groupAddress = Inet4Address.getByName("239.255.0." + i); + try { + dc.join(groupAddress, networkInterface1); + } catch (SocketException e) { + // There is a limit, that's ok according to the RI docs. For this test a lower bound of 20 + // is used, which appears to be the default linux limit. + // See /proc/sys/net/ipv4/igmp_max_memberships + assertTrue(i > 20); + break; + } + } + + dc.close(); + } + + public void test_joinAnySource_blockLimit() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey key = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + for (byte i = 1; i <= 15; i++) { + InetAddress sourceAddress = Inet4Address.getByName("10.0.0." + i); + try { + key.block(sourceAddress); + } catch (SocketException e) { + // There is a limit, that's ok according to the RI docs. For this test a lower bound of 10 + // is used, which appears to be the default linux limit. + // See /proc/sys/net/ipv4/igmp_max_msf + assertTrue(i > 10); + break; + } + } + + dc.close(); + } + + /** Confirms that calling join() does not cause an implicit bind() to take place. */ + public void test_joinAnySource_doesNotCauseBind() throws Exception { + DatagramChannel dc = DatagramChannel.open(); + dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + assertNull(dc.getLocalAddress()); + + dc.close(); + } + + public void test_joinAnySource_networkInterfaces() throws Exception { + // Check that we can join on specific interfaces and that we only receive if data is + // received on that interface. This test is only really useful on devices with multiple + // non-loopback interfaces. + + ArrayList<NetworkInterface> realInterfaces = new ArrayList<NetworkInterface>(); + Enumeration<NetworkInterface> theInterfaces = NetworkInterface.getNetworkInterfaces(); + while (theInterfaces.hasMoreElements()) { + NetworkInterface thisInterface = theInterfaces.nextElement(); + if (thisInterface.getInetAddresses().hasMoreElements()) { + realInterfaces.add(thisInterface); + } + } + + for (int i = 0; i < realInterfaces.size(); i++) { + NetworkInterface thisInterface = realInterfaces.get(i); + if (!thisInterface.supportsMulticast()) { + // Skip interfaces that do not support multicast - there's no point in proving + // they cannot send / receive multicast messages. + continue; + } + + // get the first address on the interface + + // start server which is joined to the group and has + // only asked for packets on this interface + Enumeration<InetAddress> addresses = thisInterface.getInetAddresses(); + + NetworkInterface sendingInterface = null; + InetAddress group = null; + if (addresses.hasMoreElements()) { + InetAddress firstAddress = addresses.nextElement(); + if (firstAddress instanceof Inet4Address) { + group = GOOD_MULTICAST_IPv4; + sendingInterface = networkInterface1; + } else { + // if this interface only seems to support IPV6 addresses + group = GOOD_MULTICAST_IPv6; + sendingInterface = IPV6networkInterface1; + } + } + + DatagramChannel dc = createReceiverChannel(); + InetSocketAddress localAddress = (InetSocketAddress) dc.getLocalAddress(); + dc.join(group, thisInterface); + + // Now send out a package on sendingInterface. We should only see the packet if we send + // it on the same interface we are listening on (thisInterface). + String msg = "Hello World - Again" + thisInterface.getName(); + sendMulticastMessage(group, localAddress.getPort(), msg, sendingInterface); + + ByteBuffer recvBuffer = ByteBuffer.allocate(100); + SocketAddress sourceAddress = dc.receive(recvBuffer); + if (thisInterface.equals(sendingInterface)) { + assertEquals(msg, new String(recvBuffer.array(), 0, recvBuffer.position())); + } else { + assertNull(sourceAddress); + } + + dc.close(); + } + } + + /** Confirms that the scope of each membership is network interface-level. */ + public void test_join_canMixTypesOnDifferentInterfaces() throws Exception { + DatagramChannel dc = DatagramChannel.open(); + MembershipKey membershipKey1 = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + MembershipKey membershipKey2 = dc.join(GOOD_MULTICAST_IPv4, loopbackInterface, UNICAST_IPv4_1); + assertNotSame(membershipKey1, membershipKey2); + + dc.close(); + } + + + private DatagramChannel createReceiverChannel() throws Exception { + DatagramChannel dc = DatagramChannel.open(); + dc.bind(null /* leave the OS to determine the port, and use the wildcard address */); + configureChannelForReceiving(dc); + return dc; + } + + public void test_joinAnySource_multiple_joins_IPv4() + throws Exception { + test_joinAnySource_multiple_joins(GOOD_MULTICAST_IPv4); + } + + public void test_joinAnySource_multiple_joins_IPv6() + throws Exception { + test_joinAnySource_multiple_joins(GOOD_MULTICAST_IPv6); + } + + private void test_joinAnySource_multiple_joins(InetAddress group) throws Exception { + DatagramChannel dc = createReceiverChannel(); + + MembershipKey membershipKey1 = dc.join(group, networkInterface1); + + MembershipKey membershipKey2 = dc.join(group, loopbackInterface); + assertFalse(membershipKey1.equals(membershipKey2)); + + MembershipKey membershipKey1_2 = dc.join(group, networkInterface1); + assertEquals(membershipKey1, membershipKey1_2); + + dc.close(); + } + + public void test_joinAnySource_multicastLoopOption_IPv4() throws Exception { + test_joinAnySource_multicastLoopOption(GOOD_MULTICAST_IPv4); + } + + public void test_multicastLoopOption_IPv6() throws Exception { + test_joinAnySource_multicastLoopOption(GOOD_MULTICAST_IPv6); + } + + private void test_joinAnySource_multicastLoopOption(InetAddress group) throws Exception { + final String message = "Hello, world!"; + + DatagramChannel dc = createReceiverChannel(); + dc.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, true /* enable loop */); + configureChannelForReceiving(dc); + dc.join(group, networkInterface1); + + InetSocketAddress localAddress = (InetSocketAddress) dc.getLocalAddress(); + + // send the datagram + byte[] sendData = message.getBytes(); + ByteBuffer sendBuffer = ByteBuffer.wrap(sendData); + dc.send(sendBuffer, new InetSocketAddress(group, localAddress.getPort())); + + // receive the datagram + ByteBuffer recvBuffer = ByteBuffer.allocate(100); + SocketAddress sourceAddress = dc.receive(recvBuffer); + assertNotNull(sourceAddress); + + String recvMessage = new String(recvBuffer.array(), 0, recvBuffer.position()); + assertEquals(message, recvMessage); + + // Turn off loop + dc.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, false /* enable loopback */); + + // send another datagram + recvBuffer.position(0); + ByteBuffer sendBuffer2 = ByteBuffer.wrap(sendData); + dc.send(sendBuffer2, new InetSocketAddress(group, localAddress.getPort())); + + SocketAddress sourceAddress2 = dc.receive(recvBuffer); + assertNull(sourceAddress2); + + dc.close(); + } + + public void testMembershipKeyAccessors_IPv4() throws Exception { + testMembershipKeyAccessors(GOOD_MULTICAST_IPv4); + } + + public void testMembershipKeyAccessors_IPv6() throws Exception { + testMembershipKeyAccessors(GOOD_MULTICAST_IPv6); + } + + private void testMembershipKeyAccessors(InetAddress group) throws Exception { + DatagramChannel dc = createReceiverChannel(); + + MembershipKey key = dc.join(group, networkInterface1); + assertSame(dc, key.channel()); + assertSame(group, key.group()); + assertTrue(key.isValid()); + assertSame(networkInterface1, key.networkInterface()); + assertNull(key.sourceAddress()); + } + + public void test_dropAnySource_twice_IPv4() throws Exception { + test_dropAnySource_twice(GOOD_MULTICAST_IPv4); + } + + public void test_dropAnySource_twice_IPv6() throws Exception { + test_dropAnySource_twice(GOOD_MULTICAST_IPv6); + } + + private void test_dropAnySource_twice(InetAddress group) throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(group, networkInterface1); + + assertTrue(membershipKey.isValid()); + membershipKey.drop(); + assertFalse(membershipKey.isValid()); + + // Try to leave a group we are no longer a member of - should do nothing. + membershipKey.drop(); + + dc.close(); + } + + public void test_close_invalidatesMembershipKey() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + + assertTrue(membershipKey.isValid()); + + dc.close(); + + assertFalse(membershipKey.isValid()); + } + + public void test_block_null() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + try { + membershipKey.block(null); + fail(); + } catch (NullPointerException expected) { + } + + dc.close(); + } + + public void test_block_mixedAddressTypes_IPv4() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + try { + membershipKey.block(UNICAST_IPv6_1); + fail(); + } catch (IllegalArgumentException expected) { + } + + dc.close(); + } + + public void test_block_mixedAddressTypes_IPv6() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv6, networkInterface1); + try { + membershipKey.block(UNICAST_IPv4_1); + fail(); + } catch (IllegalArgumentException expected) { + } + + dc.close(); + } + + public void test_block_cannotBlockWithSourceSpecificMembership() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1, UNICAST_IPv4_1); + try { + membershipKey.block(UNICAST_IPv4_2); + fail(); + } catch (IllegalStateException expected) { + } + + dc.close(); + } + + public void test_block_multipleBlocksIgnored() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + membershipKey.block(UNICAST_IPv4_1); + + MembershipKey membershipKey2 = membershipKey.block(UNICAST_IPv4_1); + assertSame(membershipKey2, membershipKey); + + dc.close(); + } + + public void test_block_wildcardAddress() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + try { + membershipKey.block(WILDCARD_IPv4); + fail(); + } catch (IllegalArgumentException expected) { + } + + dc.close(); + } + + public void test_unblock_multipleUnblocksFail() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + + try { + membershipKey.unblock(UNICAST_IPv4_1); + fail(); + } catch (IllegalStateException expected) { + } + + assertTrue(membershipKey.isValid()); + + membershipKey.block(UNICAST_IPv4_1); + membershipKey.unblock(UNICAST_IPv4_1); + + try { + membershipKey.unblock(UNICAST_IPv4_1); + fail(); + } catch (IllegalStateException expected) { + } + + dc.close(); + } + + public void test_unblock_null() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + membershipKey.block(UNICAST_IPv4_1); + + try { + membershipKey.unblock(null); + fail(); + } catch (IllegalStateException expected) { + // Either of these exceptions are fine + } catch (NullPointerException expected) { + // Either of these exception are fine + } + + dc.close(); + } + + public void test_unblock_mixedAddressTypes_IPv4() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1); + try { + membershipKey.unblock(UNICAST_IPv6_1); + fail(); + } catch (IllegalStateException expected) { + // Either of these exceptions are fine + } catch (IllegalArgumentException expected) { + // Either of these exceptions are fine + } + + dc.close(); + } + + public void test_unblock_mixedAddressTypes_IPv6() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv6, networkInterface1); + try { + membershipKey.unblock(UNICAST_IPv4_1); + fail(); + } catch (IllegalStateException expected) { + // Either of these exceptions are fine + } catch (IllegalArgumentException expected) { + // Either of these exceptions are fine + } + + dc.close(); + } + + /** Checks that block() works when the receiver is bound to the multicast group address */ + public void test_block_filtersAsExpected_groupBind_ipv4() throws Exception { + InetAddress ipv4LocalAddress = getLocalIpv4Address(networkInterface1); + test_block_filtersAsExpected( + ipv4LocalAddress /* senderBindAddress */, + GOOD_MULTICAST_IPv4 /* receiverBindAddress */, + GOOD_MULTICAST_IPv4 /* groupAddress */); + } + + /** Checks that block() works when the receiver is bound to the multicast group address */ + public void test_block_filtersAsExpected_groupBind_ipv6() throws Exception { + InetAddress ipv6LocalAddress = getLocalIpv6Address(IPV6networkInterface1); + test_block_filtersAsExpected( + ipv6LocalAddress /* senderBindAddress */, + GOOD_MULTICAST_IPv6 /* receiverBindAddress */, + GOOD_MULTICAST_IPv6 /* groupAddress */); + } + + /** Checks that block() works when the receiver is bound to the "any" address */ + public void test_block_filtersAsExpected_anyBind_ipv4() throws Exception { + InetAddress ipv4LocalAddress = getLocalIpv4Address(networkInterface1); + test_block_filtersAsExpected( + ipv4LocalAddress /* senderBindAddress */, + WILDCARD_IPv4 /* receiverBindAddress */, + GOOD_MULTICAST_IPv4 /* groupAddress */); + } + + /** Checks that block() works when the receiver is bound to the "any" address */ + public void test_block_filtersAsExpected_anyBind_ipv6() throws Exception { + InetAddress ipv6LocalAddress = getLocalIpv6Address(IPV6networkInterface1); + test_block_filtersAsExpected( + ipv6LocalAddress /* senderBindAddress */, + WILDCARD_IPv6 /* receiverBindAddress */, + GOOD_MULTICAST_IPv6 /* groupAddress */); + } + + private void test_block_filtersAsExpected( + InetAddress senderBindAddress, InetAddress receiverBindAddress, InetAddress groupAddress) + throws Exception { + + DatagramChannel sendingChannel = DatagramChannel.open(); + // In order to block a sender the sender's address must be known. The sendingChannel is + // explicitly bound to a known, non-loopback address. + sendingChannel.bind(new InetSocketAddress(senderBindAddress, 0)); + InetSocketAddress sendingAddress = (InetSocketAddress) sendingChannel.getLocalAddress(); + + DatagramChannel receivingChannel = DatagramChannel.open(); + configureChannelForReceiving(receivingChannel); + receivingChannel.bind( + new InetSocketAddress(receiverBindAddress, 0) /* local port left to the OS to determine */); + InetSocketAddress localReceivingAddress = + (InetSocketAddress) receivingChannel.getLocalAddress(); + InetSocketAddress groupSocketAddress = + new InetSocketAddress(groupAddress, localReceivingAddress.getPort()); + MembershipKey membershipKey = + receivingChannel.join(groupSocketAddress.getAddress(), networkInterface1); + + ByteBuffer receiveBuffer = ByteBuffer.allocate(10); + + // Send a message. It should be received. + String msg1 = "Hello1"; + sendMessage(sendingChannel, msg1, groupSocketAddress); + InetSocketAddress sourceAddress1 = (InetSocketAddress) receivingChannel.receive(receiveBuffer); + assertEquals(sourceAddress1, sendingAddress); + assertEquals(msg1, new String(receiveBuffer.array(), 0, receiveBuffer.position())); + + // Now block the sender + membershipKey.block(sendingAddress.getAddress()); + + // Send a message. It should be filtered. + String msg2 = "Hello2"; + sendMessage(sendingChannel, msg2, groupSocketAddress); + receiveBuffer.position(0); + InetSocketAddress sourceAddress2 = (InetSocketAddress) receivingChannel.receive(receiveBuffer); + assertNull(sourceAddress2); + + // Now unblock the sender + membershipKey.unblock(sendingAddress.getAddress()); + + // Send a message. It should be received. + String msg3 = "Hello3"; + sendMessage(sendingChannel, msg3, groupSocketAddress); + receiveBuffer.position(0); + InetSocketAddress sourceAddress3 = (InetSocketAddress) receivingChannel.receive(receiveBuffer); + assertEquals(sourceAddress3, sendingAddress); + assertEquals(msg3, new String(receiveBuffer.array(), 0, receiveBuffer.position())); + + sendingChannel.close(); + receivingChannel.close(); + } + + public void test_joinSourceSpecific_nullGroupAddress() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(null, networkInterface1, UNICAST_IPv4_1); + fail(); + } catch (NullPointerException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_afterClose() throws Exception { + DatagramChannel dc = createReceiverChannel(); + dc.close(); + try { + dc.join(GOOD_MULTICAST_IPv4, networkInterface1, UNICAST_IPv4_1); + fail(); + } catch (ClosedChannelException expected) { + } + } + + public void test_joinSourceSpecific_nullNetworkInterface() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(GOOD_MULTICAST_IPv4, null, UNICAST_IPv4_1); + fail(); + } catch (NullPointerException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_nonMulticastGroupAddress_IPv4() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(UNICAST_IPv4_1, networkInterface1, UNICAST_IPv4_1); + fail(); + } catch (IllegalArgumentException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_nonMulticastGroupAddress_IPv6() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(UNICAST_IPv6_1, IPV6networkInterface1, UNICAST_IPv6_1); + fail(); + } catch (IllegalArgumentException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_nullSourceAddress_IPv4() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(GOOD_MULTICAST_IPv4, networkInterface1, null); + fail(); + } catch (NullPointerException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_nullSourceAddress_IPv6() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(GOOD_MULTICAST_IPv6, networkInterface1, null); + fail(); + } catch (NullPointerException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_mixedAddressTypes() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(GOOD_MULTICAST_IPv4, networkInterface1, UNICAST_IPv6_1); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + dc.join(GOOD_MULTICAST_IPv6, networkInterface1, UNICAST_IPv4_1); + fail(); + } catch (IllegalArgumentException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_nonUnicastSourceAddress_IPv4() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(GOOD_MULTICAST_IPv4, networkInterface1, BAD_MULTICAST_IPv4); + fail(); + } catch (IllegalArgumentException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_nonUniicastSourceAddress_IPv6() throws Exception { + DatagramChannel dc = createReceiverChannel(); + try { + dc.join(GOOD_MULTICAST_IPv6, networkInterface1, BAD_MULTICAST_IPv6); + fail(); + } catch (IllegalArgumentException expected) { + } + dc.close(); + } + + public void test_joinSourceSpecific_multipleSourceAddressLimit() throws Exception { + DatagramChannel dc = createReceiverChannel(); + for (byte i = 1; i <= 20; i++) { + InetAddress sourceAddress = Inet4Address.getByAddress(new byte[] { 10, 0, 0, i}); + try { + dc.join(GOOD_MULTICAST_IPv4, networkInterface1, sourceAddress); + } catch (SocketException e) { + // There is a limit, that's ok according to the RI docs. For this test a lower bound of 10 + // is used, which appears to be the default linux limit. See /proc/sys/net/ipv4/igmp_max_msf + assertTrue(i > 10); + break; + } + } + + dc.close(); + } + + /** + * Checks that a source-specific join() works when the receiver is bound to the multicast group + * address + */ + public void test_joinSourceSpecific_null() throws Exception { + InetAddress ipv4LocalAddress = getLocalIpv4Address(networkInterface1); + test_joinSourceSpecific( + ipv4LocalAddress /* senderBindAddress */, + GOOD_MULTICAST_IPv4 /* receiverBindAddress */, + GOOD_MULTICAST_IPv4 /* groupAddress */, + UNICAST_IPv4_1 /* badSenderAddress */); + } + + /** + * Checks that a source-specific join() works when the receiver is bound to the multicast group + * address + */ + public void test_joinSourceSpecific_groupBind_ipv4() throws Exception { + InetAddress ipv4LocalAddress = getLocalIpv4Address(networkInterface1); + test_joinSourceSpecific( + ipv4LocalAddress /* senderBindAddress */, + GOOD_MULTICAST_IPv4 /* receiverBindAddress */, + GOOD_MULTICAST_IPv4 /* groupAddress */, + UNICAST_IPv4_1 /* badSenderAddress */); + } + + /** + * Checks that a source-specific join() works when the receiver is bound to the multicast group + * address + */ + public void test_joinSourceSpecific_groupBind_ipv6() throws Exception { + InetAddress ipv6LocalAddress = getLocalIpv6Address(IPV6networkInterface1); + test_joinSourceSpecific( + ipv6LocalAddress /* senderBindAddress */, + GOOD_MULTICAST_IPv6 /* receiverBindAddress */, + GOOD_MULTICAST_IPv6 /* groupAddress */, + UNICAST_IPv6_1 /* badSenderAddress */); + } + + /** Checks that a source-specific join() works when the receiver is bound to the "any" address */ + public void test_joinSourceSpecific_anyBind_ipv4() throws Exception { + InetAddress ipv4LocalAddress = getLocalIpv4Address(networkInterface1); + test_joinSourceSpecific( + ipv4LocalAddress /* senderBindAddress */, + WILDCARD_IPv4 /* receiverBindAddress */, + GOOD_MULTICAST_IPv4 /* groupAddress */, + UNICAST_IPv4_1 /* badSenderAddress */); + } + + /** Checks that a source-specific join() works when the receiver is bound to the "any" address */ + public void test_joinSourceSpecific_anyBind_ipv6() throws Exception { + InetAddress ipv6LocalAddress = getLocalIpv6Address(IPV6networkInterface1); + test_joinSourceSpecific( + ipv6LocalAddress /* senderBindAddress */, + WILDCARD_IPv6 /* receiverBindAddress */, + GOOD_MULTICAST_IPv6 /* groupAddress */, + UNICAST_IPv6_1 /* badSenderAddress */); + } + + /** + * Checks that the source-specific membership is correctly source-filtering. + * + * @param senderBindAddress the address to bind the sender socket to + * @param receiverBindAddress the address to bind the receiver socket to + * @param groupAddress the group address to join + * @param badSenderAddress a unicast address to join to perform a negative test + */ + private void test_joinSourceSpecific( + InetAddress senderBindAddress, InetAddress receiverBindAddress, InetAddress groupAddress, + InetAddress badSenderAddress) + throws Exception { + DatagramChannel sendingChannel = DatagramChannel.open(); + // In order to be source-specific the sender's address must be known. The sendingChannel is + // explicitly bound to a known, non-loopback address. + sendingChannel.bind(new InetSocketAddress(senderBindAddress, 0)); + InetSocketAddress sendingAddress = (InetSocketAddress) sendingChannel.getLocalAddress(); + + DatagramChannel receivingChannel = DatagramChannel.open(); + receivingChannel.bind( + new InetSocketAddress(receiverBindAddress, 0) /* local port left to the OS to determine */); + configureChannelForReceiving(receivingChannel); + + InetSocketAddress localReceivingAddress = + (InetSocketAddress) receivingChannel.getLocalAddress(); + InetSocketAddress groupSocketAddress = + new InetSocketAddress(groupAddress, localReceivingAddress.getPort()); + MembershipKey membershipKey1 = receivingChannel + .join(groupSocketAddress.getAddress(), networkInterface1, senderBindAddress); + + ByteBuffer receiveBuffer = ByteBuffer.allocate(10); + + // Send a message. It should be received. + String msg1 = "Hello1"; + sendMessage(sendingChannel, msg1, groupSocketAddress); + InetSocketAddress sourceAddress1 = (InetSocketAddress) receivingChannel.receive(receiveBuffer); + assertEquals(sourceAddress1, sendingAddress); + assertEquals(msg1, new String(receiveBuffer.array(), 0, receiveBuffer.position())); + + membershipKey1.drop(); + + receivingChannel.join(groupSocketAddress.getAddress(), networkInterface1, badSenderAddress); + + // Send a message. It should not be received. + String msg2 = "Hello2"; + sendMessage(sendingChannel, msg2, groupSocketAddress); + InetSocketAddress sourceAddress2 = (InetSocketAddress) receivingChannel.receive(receiveBuffer); + assertNull(sourceAddress2); + + receivingChannel.close(); + sendingChannel.close(); + } + + public void test_dropSourceSpecific_twice_IPv4() throws Exception { + test_dropSourceSpecific_twice( + GOOD_MULTICAST_IPv4 /* groupAddress */, UNICAST_IPv4_1 /* sourceAddress */); + } + + public void test_dropSourceSpecific_twice_IPv6() throws Exception { + test_dropSourceSpecific_twice( + GOOD_MULTICAST_IPv6 /* groupAddress */, UNICAST_IPv6_1 /* sourceAddress */); + } + + private void test_dropSourceSpecific_twice(InetAddress groupAddress, InetAddress sourceAddress) + throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(groupAddress, networkInterface1, sourceAddress); + + assertTrue(membershipKey.isValid()); + membershipKey.drop(); + assertFalse(membershipKey.isValid()); + + // Try to leave a group we are no longer a member of - should do nothing. + membershipKey.drop(); + + dc.close(); + } + + public void test_dropSourceSpecific_sourceKeysAreIndependent_IPv4() throws Exception { + test_dropSourceSpecific_sourceKeysAreIndependent( + GOOD_MULTICAST_IPv4 /* groupAddress */, + UNICAST_IPv4_1 /* sourceAddress1 */, + UNICAST_IPv4_2 /* sourceAddress2 */); + } + + public void test_dropSourceSpecific_sourceKeysAreIndependent_IPv6() throws Exception { + test_dropSourceSpecific_sourceKeysAreIndependent( + GOOD_MULTICAST_IPv6 /* groupAddress */, + UNICAST_IPv6_1 /* sourceAddress1 */, + UNICAST_IPv6_2 /* sourceAddress2 */); + } + + private void test_dropSourceSpecific_sourceKeysAreIndependent( + InetAddress groupAddress, InetAddress sourceAddress1, InetAddress sourceAddress2) + throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey1 = dc.join(groupAddress, networkInterface1, sourceAddress1); + MembershipKey membershipKey2 = dc.join(groupAddress, networkInterface1, sourceAddress2); + assertFalse(membershipKey1.equals(membershipKey2)); + assertTrue(membershipKey1.isValid()); + assertTrue(membershipKey2.isValid()); + + membershipKey1.drop(); + + assertFalse(membershipKey1.isValid()); + assertTrue(membershipKey2.isValid()); + + dc.close(); + } + + public void test_drop_keyBehaviorAfterDrop() throws Exception { + DatagramChannel dc = createReceiverChannel(); + MembershipKey membershipKey = dc.join(GOOD_MULTICAST_IPv4, networkInterface1, UNICAST_IPv4_1); + membershipKey.drop(); + assertFalse(membershipKey.isValid()); + + try { + membershipKey.block(UNICAST_IPv4_1); + } catch (IllegalStateException expected) { + } + + try { + membershipKey.unblock(UNICAST_IPv4_1); + } catch (IllegalStateException expected) { + } + + assertSame(dc, membershipKey.channel()); + assertSame(GOOD_MULTICAST_IPv4, membershipKey.group()); + assertSame(UNICAST_IPv4_1, membershipKey.sourceAddress()); + assertSame(networkInterface1, membershipKey.networkInterface()); + } + + private static void configureChannelForReceiving(DatagramChannel receivingChannel) + throws Exception { + + // NOTE: At the time of writing setSoTimeout() has no effect in the RI, making these tests hang + // if the channel is in blocking mode. configureBlocking(false) is used instead and rely on the + // network to the local host being instantaneous. + // receivingChannel.socket().setSoTimeout(200); + // receivingChannel.configureBlocking(true); + receivingChannel.configureBlocking(false); + } + + private static boolean willWorkForMulticast(NetworkInterface iface) throws IOException { + return iface.isUp() + // Typically loopback interfaces do not support multicast, but they are ruled out + // explicitly here anyway. + && !iface.isLoopback() && iface.supportsMulticast() + && iface.getInetAddresses().hasMoreElements(); + } + + private static void sendMulticastMessage(InetAddress group, int port, String msg) + throws IOException { + sendMulticastMessage(group, port, msg, null /* networkInterface */); + } + + private static void sendMulticastMessage( + InetAddress group, int port, String msg, NetworkInterface sendingInterface) + throws IOException { + // Any datagram socket can send to a group. It does not need to have joined the group. + DatagramChannel dc = DatagramChannel.open(); + if (sendingInterface != null) { + // For some reason, if set, this must be set to a real (non-loopback) device for an IPv6 + // group, but can be loopback for an IPv4 group. + dc.setOption(StandardSocketOptions.IP_MULTICAST_IF, sendingInterface); + } + sendMessage(dc, msg, new InetSocketAddress(group, port)); + dc.close(); + } + + private static void sendMessage( + DatagramChannel sendingChannel, String msg, InetSocketAddress targetAddress) + throws IOException { + + ByteBuffer sendBuffer = ByteBuffer.wrap(msg.getBytes()); + sendingChannel.send(sendBuffer, targetAddress); + } + + private static InetAddress getLocalIpv4Address(NetworkInterface networkInterface) { + for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) { + if (interfaceAddress.getAddress() instanceof Inet4Address) { + return interfaceAddress.getAddress(); + } + } + throw new AssertionFailedError("Unable to find local IPv4 address for " + networkInterface); + } + + private static InetAddress getLocalIpv6Address(NetworkInterface networkInterface) { + for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) { + if (interfaceAddress.getAddress() instanceof Inet6Address) { + return interfaceAddress.getAddress(); + } + } + throw new AssertionFailedError("Unable to find local IPv6 address for " + networkInterface); + } + +} diff --git a/luni/src/test/java/libcore/java/nio/channels/DatagramChannelTest.java b/luni/src/test/java/libcore/java/nio/channels/DatagramChannelTest.java index 5927583..cd4bb22 100644 --- a/luni/src/test/java/libcore/java/nio/channels/DatagramChannelTest.java +++ b/luni/src/test/java/libcore/java/nio/channels/DatagramChannelTest.java @@ -19,13 +19,17 @@ package libcore.java.nio.channels; import java.io.IOException; import java.net.DatagramSocket; import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; +import java.net.SocketException; import java.net.SocketOption; import java.net.StandardSocketOptions; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.DatagramChannel; +import java.nio.channels.UnresolvedAddressException; import java.util.Enumeration; import java.util.Set; @@ -446,4 +450,120 @@ public class DatagramChannelTest extends junit.framework.TestCase { dc.close(); } + public void test_bind_unresolvedAddress() throws IOException { + DatagramChannel dc = DatagramChannel.open(); + try { + dc.bind(new InetSocketAddress("unresolvedname", 31415)); + fail(); + } catch (UnresolvedAddressException expected) { + } + + assertNull(dc.getLocalAddress()); + assertTrue(dc.isOpen()); + assertFalse(dc.isConnected()); + + dc.close(); + } + + public void test_bind_noReuseAddress() throws Exception { + DatagramChannel dc1 = DatagramChannel.open(); + dc1.setOption(StandardSocketOptions.SO_REUSEADDR, false); + DatagramChannel dc2 = DatagramChannel.open(); + dc1.setOption(StandardSocketOptions.SO_REUSEADDR, false); + + dc1.bind(null); + + try { + dc2.bind(dc1.getLocalAddress()); + fail(); + } catch (IOException expected) {} + + dc1.close(); + dc2.close(); + } + + public void test_bind_withReuseAddress() throws Exception { + DatagramChannel dc1 = DatagramChannel.open(); + dc1.setOption(StandardSocketOptions.SO_REUSEADDR, true); + DatagramChannel dc2 = DatagramChannel.open(); + dc2.setOption(StandardSocketOptions.SO_REUSEADDR, true); + + dc1.bind(null); + dc2.bind(dc1.getLocalAddress()); + + dc1.close(); + dc2.close(); + } + + public void test_bind_any_IPv4() throws Exception { + test_bind_any(InetAddress.getByName("0.0.0.0")); + } + + public void test_bind_any_IPv6() throws Exception { + test_bind_any(InetAddress.getByName("::")); + } + + private void test_bind_any(InetAddress bindAddress) throws Exception { + DatagramChannel dc = DatagramChannel.open(); + dc.bind(new InetSocketAddress(bindAddress, 0)); + + assertTrue(dc.isOpen()); + assertFalse(dc.isConnected()); + + InetSocketAddress actualAddress = (InetSocketAddress) dc.getLocalAddress(); + assertTrue(actualAddress.getAddress().isAnyLocalAddress()); + assertTrue(actualAddress.getPort() > 0); + + dc.close(); + } + + public void test_bind_loopback_IPv4() throws Exception { + test_bind(InetAddress.getByName("127.0.0.1")); + } + + public void test_bind_loopback_IPv6() throws Exception { + test_bind(InetAddress.getByName("::1")); + } + + public void test_bind_IPv4() throws Exception { + InetAddress bindAddress = getNonLoopbackNetworkInterfaceAddress(true /* ipv4 */); + test_bind(bindAddress); + } + + public void test_bind_IPv6() throws Exception { + InetAddress bindAddress = getNonLoopbackNetworkInterfaceAddress(false /* ipv4 */); + test_bind(bindAddress); + } + + private void test_bind(InetAddress bindAddress) throws IOException { + DatagramChannel dc = DatagramChannel.open(); + dc.bind(new InetSocketAddress(bindAddress, 0)); + + InetSocketAddress actualAddress = (InetSocketAddress) dc.getLocalAddress(); + assertEquals(bindAddress, actualAddress.getAddress()); + assertTrue(actualAddress.getPort() > 0); + + dc.close(); + } + + private static InetAddress getNonLoopbackNetworkInterfaceAddress(boolean ipv4) + throws SocketException { + + Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + if (networkInterface.isLoopback() || !networkInterface.isUp()) { + continue; + } + Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses(); + while (inetAddresses.hasMoreElements()) { + InetAddress inetAddress = inetAddresses.nextElement(); + if ( (ipv4 && inetAddress instanceof Inet4Address) + || (!ipv4 && inetAddress instanceof Inet6Address)) { + return inetAddress; + } + } + } + return null; + } } diff --git a/luni/src/test/java/libcore/java/nio/channels/ServerSocketChannelTest.java b/luni/src/test/java/libcore/java/nio/channels/ServerSocketChannelTest.java index ed39d46..e819d82 100644 --- a/luni/src/test/java/libcore/java/nio/channels/ServerSocketChannelTest.java +++ b/luni/src/test/java/libcore/java/nio/channels/ServerSocketChannelTest.java @@ -26,6 +26,7 @@ import java.net.StandardSocketOptions; import java.nio.channels.ClosedChannelException; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +import java.nio.channels.UnresolvedAddressException; import java.util.Enumeration; import java.util.Set; @@ -63,6 +64,20 @@ public class ServerSocketChannelTest extends junit.framework.TestCase { } } + public void test_bind_unresolvedAddress() throws IOException { + ServerSocketChannel ssc = ServerSocketChannel.open(); + try { + ssc.bind(new InetSocketAddress("unresolvedname", 31415)); + fail(); + } catch (UnresolvedAddressException expected) { + } + + assertNull(ssc.getLocalAddress()); + assertTrue(ssc.isOpen()); + + ssc.close(); + } + public void test_bind_nullBindsToAll() throws Exception { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.bind(null); diff --git a/luni/src/test/java/libcore/java/nio/channels/SocketChannelTest.java b/luni/src/test/java/libcore/java/nio/channels/SocketChannelTest.java index 118a168..bd23b3f 100644 --- a/luni/src/test/java/libcore/java/nio/channels/SocketChannelTest.java +++ b/luni/src/test/java/libcore/java/nio/channels/SocketChannelTest.java @@ -31,6 +31,7 @@ import java.nio.channels.ClosedChannelException; import java.nio.channels.SocketChannel; import java.nio.channels.Selector; import java.nio.channels.SelectionKey; +import java.nio.channels.UnresolvedAddressException; import java.util.Set; import tests.io.MockOs; @@ -162,6 +163,21 @@ public class SocketChannelTest extends junit.framework.TestCase { } } + public void test_bind_unresolvedAddress() throws IOException { + SocketChannel sc = SocketChannel.open(); + try { + sc.bind(new InetSocketAddress("unresolvedname", 31415)); + fail(); + } catch (UnresolvedAddressException expected) { + } + + assertNull(sc.getLocalAddress()); + assertTrue(sc.isOpen()); + assertFalse(sc.isConnected()); + + sc.close(); + } + /** Checks that the SocketChannel and associated Socket agree on the socket state. */ public void test_bind_socketStateSync() throws IOException { SocketChannel sc = SocketChannel.open(); |