diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index 451435c909977..34e9476b3e08f 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -161,7 +161,7 @@ public class NetworkUtils { * @param hostAddress an int coding for an IPv4 address, where higher-order int byte is * lower-order IPv4 address byte */ - public static InetAddress intToInet4AddressHTL(int hostAddress) { + public static Inet4Address intToInet4AddressHTL(int hostAddress) { return intToInet4AddressHTH(Integer.reverseBytes(hostAddress)); } @@ -169,14 +169,14 @@ public class NetworkUtils { * Convert a IPv4 address from an integer to an InetAddress (0x01020304 -> 1.2.3.4) * @param hostAddress an int coding for an IPv4 address */ - public static InetAddress intToInet4AddressHTH(int hostAddress) { + public static Inet4Address intToInet4AddressHTH(int hostAddress) { byte[] addressBytes = { (byte) (0xff & (hostAddress >> 24)), (byte) (0xff & (hostAddress >> 16)), (byte) (0xff & (hostAddress >> 8)), (byte) (0xff & hostAddress) }; try { - return InetAddress.getByAddress(addressBytes); + return (Inet4Address) InetAddress.getByAddress(addressBytes); } catch (UnknownHostException e) { throw new AssertionError(); } @@ -408,6 +408,28 @@ public class NetworkUtils { return new Pair(address, prefixLength); } + /** + * Get a prefix mask as Inet4Address for a given prefix length. + * + *

For example 20 -> 255.255.240.0 + */ + public static Inet4Address getPrefixMaskAsInet4Address(int prefixLength) + throws IllegalArgumentException { + return intToInet4AddressHTH(prefixLengthToV4NetmaskIntHTH(prefixLength)); + } + + /** + * Get the broadcast address for a given prefix. + * + *

For example 192.168.0.1/24 -> 192.168.0.255 + */ + public static Inet4Address getBroadcastAddress(Inet4Address addr, int prefixLength) + throws IllegalArgumentException { + final int intBroadcastAddr = inet4AddressToIntHTH(addr) + | ~prefixLengthToV4NetmaskIntHTH(prefixLength); + return intToInet4AddressHTH(intBroadcastAddr); + } + /** * Check if IP address type is consistent between two InetAddress. * @return true if both are the same type. False otherwise. diff --git a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java index b7a55a47a6a6c..7e57c9f463503 100644 --- a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java +++ b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java @@ -512,7 +512,7 @@ class DhcpLeaseRepository { // Loop until a free address is found, or there are no more addresses. // There is slightly less than this many usable addresses, but some extra looping is OK for (int i = 0; i < mNumAddresses; i++) { - final Inet4Address addr = (Inet4Address) intToInet4AddressHTH(intAddr); + final Inet4Address addr = intToInet4AddressHTH(intAddr); if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) { return new DhcpLease(clientId, hwAddr, addr, expTime, hostname); } diff --git a/services/net/java/android/net/dhcp/DhcpServingParams.java b/services/net/java/android/net/dhcp/DhcpServingParams.java new file mode 100644 index 0000000000000..ba9d116d0bd51 --- /dev/null +++ b/services/net/java/android/net/dhcp/DhcpServingParams.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2018 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 android.net.dhcp; + +import static android.net.NetworkUtils.getPrefixMaskAsInet4Address; +import static android.net.dhcp.DhcpPacket.INFINITE_LEASE; +import static android.net.util.NetworkConstants.IPV4_MAX_MTU; +import static android.net.util.NetworkConstants.IPV4_MIN_MTU; + +import static java.lang.Integer.toUnsignedLong; + +import android.annotation.NonNull; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.NetworkUtils; + +import java.net.Inet4Address; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Parameters used by the DhcpServer to serve requests. + * + *

Instances are immutable. Use {@link DhcpServingParams.Builder} to instantiate. + * @hide + */ +public class DhcpServingParams { + public static final int MTU_UNSET = 0; + public static final int MIN_PREFIX_LENGTH = 16; + public static final int MAX_PREFIX_LENGTH = 30; + + /** Server inet address and prefix to serve */ + @NonNull + public final LinkAddress serverAddr; + + /** + * Default routers to be advertised to DHCP clients. May be empty. + * This set is provided by {@link DhcpServingParams.Builder} and is immutable. + */ + @NonNull + public final Set defaultRouters; + + /** + * DNS servers to be advertised to DHCP clients. May be empty. + * This set is provided by {@link DhcpServingParams.Builder} and is immutable. + */ + @NonNull + public final Set dnsServers; + + /** + * Excluded addresses that the DHCP server is not allowed to assign to clients. + * This set is provided by {@link DhcpServingParams.Builder} and is immutable. + */ + @NonNull + public final Set excludedAddrs; + + // DHCP uses uint32. Use long for clearer code, and check range when building. + public final long dhcpLeaseTimeSecs; + public final int linkMtu; + + /** + * Checked exception thrown when some parameters used to build {@link DhcpServingParams} are + * missing or invalid. + */ + public static class InvalidParameterException extends Exception { + public InvalidParameterException(String message) { + super(message); + } + } + + private DhcpServingParams(@NonNull LinkAddress serverAddr, + @NonNull Set defaultRouters, + @NonNull Set dnsServers, @NonNull Set excludedAddrs, + long dhcpLeaseTimeSecs, int linkMtu) { + this.serverAddr = serverAddr; + this.defaultRouters = defaultRouters; + this.dnsServers = dnsServers; + this.excludedAddrs = excludedAddrs; + this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs; + this.linkMtu = linkMtu; + } + + @NonNull + public Inet4Address getServerInet4Addr() { + return (Inet4Address) serverAddr.getAddress(); + } + + /** + * Get the served prefix mask as an IPv4 address. + * + *

For example, if the served prefix is 192.168.42.0/24, this will return 255.255.255.0. + */ + @NonNull + public Inet4Address getPrefixMaskAsAddress() { + return getPrefixMaskAsInet4Address(serverAddr.getPrefixLength()); + } + + /** + * Get the server broadcast address. + * + *

For example, if the server {@link LinkAddress} is 192.168.42.1/24, this will return + * 192.168.42.255. + */ + @NonNull + public Inet4Address getBroadcastAddress() { + return NetworkUtils.getBroadcastAddress(getServerInet4Addr(), serverAddr.getPrefixLength()); + } + + /** + * Utility class to create new instances of {@link DhcpServingParams} while checking validity + * of the parameters. + */ + public static class Builder { + private LinkAddress serverAddr; + private Set defaultRouters; + private Set dnsServers; + private Set excludedAddrs; + private long dhcpLeaseTimeSecs; + private int linkMtu = MTU_UNSET; + + /** + * Set the server address and served prefix for the DHCP server. + * + *

This parameter is required. + */ + public Builder setServerAddr(@NonNull LinkAddress serverAddr) { + this.serverAddr = serverAddr; + return this; + } + + /** + * Set the default routers to be advertised to DHCP clients. + * + *

Each router must be inside the served prefix. This may be an empty set, but it must + * always be set explicitly before building the {@link DhcpServingParams}. + */ + public Builder setDefaultRouters(@NonNull Set defaultRouters) { + this.defaultRouters = defaultRouters; + return this; + } + + /** + * Set the DNS servers to be advertised to DHCP clients. + * + *

This may be an empty set, but it must always be set explicitly before building the + * {@link DhcpServingParams}. + */ + public Builder setDnsServers(@NonNull Set dnsServers) { + this.dnsServers = dnsServers; + return this; + } + + /** + * Set excluded addresses that the DHCP server is not allowed to assign to clients. + * + *

This parameter is optional. DNS servers and default routers are always excluded + * and do not need to be set here. + */ + public Builder setExcludedAddrs(@NonNull Set excludedAddrs) { + this.excludedAddrs = excludedAddrs; + return this; + } + + /** + * Set the lease time for leases assigned by the DHCP server. + * + *

This parameter is required. + */ + public Builder setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) { + this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs; + return this; + } + + /** + * Set the link MTU to be advertised to DHCP clients. + * + *

If set to {@link #MTU_UNSET}, no MTU will be advertised to clients. This parameter + * is optional and defaults to {@link #MTU_UNSET}. + */ + public Builder setLinkMtu(int linkMtu) { + this.linkMtu = linkMtu; + return this; + } + + /** + * Create a new {@link DhcpServingParams} instance based on parameters set in the builder. + * + *

This method has no side-effects. If it does not throw, a valid + * {@link DhcpServingParams} is returned. + * @return The constructed parameters. + * @throws InvalidParameterException At least one parameter is missing or invalid. + */ + @NonNull + public DhcpServingParams build() throws InvalidParameterException { + if (serverAddr == null) { + throw new InvalidParameterException("Missing serverAddr"); + } + if (defaultRouters == null) { + throw new InvalidParameterException("Missing defaultRouters"); + } + if (dnsServers == null) { + // Empty set is OK, but enforce explicitly setting it + throw new InvalidParameterException("Missing dnsServers"); + } + if (dhcpLeaseTimeSecs <= 0 || dhcpLeaseTimeSecs > toUnsignedLong(INFINITE_LEASE)) { + throw new InvalidParameterException("Invalid lease time: " + dhcpLeaseTimeSecs); + } + if (linkMtu != MTU_UNSET && (linkMtu < IPV4_MIN_MTU || linkMtu > IPV4_MAX_MTU)) { + throw new InvalidParameterException("Invalid link MTU: " + linkMtu); + } + if (!serverAddr.isIPv4()) { + throw new InvalidParameterException("serverAddr must be IPv4"); + } + if (serverAddr.getPrefixLength() < MIN_PREFIX_LENGTH + || serverAddr.getPrefixLength() > MAX_PREFIX_LENGTH) { + throw new InvalidParameterException("Prefix length is not in supported range"); + } + + final IpPrefix prefix = makeIpPrefix(serverAddr); + for (Inet4Address addr : defaultRouters) { + if (!prefix.contains(addr)) { + throw new InvalidParameterException(String.format( + "Default router %s is not in server prefix %s", addr, serverAddr)); + } + } + + final Set excl = new HashSet<>(); + if (excludedAddrs != null) { + excl.addAll(excludedAddrs); + } + excl.add((Inet4Address) serverAddr.getAddress()); + excl.addAll(defaultRouters); + excl.addAll(dnsServers); + + return new DhcpServingParams(serverAddr, + Collections.unmodifiableSet(new HashSet<>(defaultRouters)), + Collections.unmodifiableSet(new HashSet<>(dnsServers)), + Collections.unmodifiableSet(excl), + dhcpLeaseTimeSecs, linkMtu); + } + } + + @NonNull + static IpPrefix makeIpPrefix(@NonNull LinkAddress addr) { + return new IpPrefix(addr.getAddress(), addr.getPrefixLength()); + } +} diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java index de04fd0ced04d..3defe56939f5e 100644 --- a/services/net/java/android/net/util/NetworkConstants.java +++ b/services/net/java/android/net/util/NetworkConstants.java @@ -77,10 +77,12 @@ public final class NetworkConstants { /** * IPv4 constants. * - * See als: + * See also: * - https://tools.ietf.org/html/rfc791 */ public static final int IPV4_HEADER_MIN_LEN = 20; + public static final int IPV4_MIN_MTU = 68; + public static final int IPV4_MAX_MTU = 65_535; public static final int IPV4_IHL_MASK = 0xf; public static final int IPV4_FLAGS_OFFSET = 6; public static final int IPV4_FRAGMENT_MASK = 0x1fff; diff --git a/tests/net/java/android/net/NetworkUtilsTest.java b/tests/net/java/android/net/NetworkUtilsTest.java index 2b172dac4865e..3452819835f5b 100644 --- a/tests/net/java/android/net/NetworkUtilsTest.java +++ b/tests/net/java/android/net/NetworkUtilsTest.java @@ -24,6 +24,8 @@ import static android.net.NetworkUtils.intToInet4AddressHTL; import static android.net.NetworkUtils.netmaskToPrefixLength; import static android.net.NetworkUtils.prefixLengthToV4NetmaskIntHTH; import static android.net.NetworkUtils.prefixLengthToV4NetmaskIntHTL; +import static android.net.NetworkUtils.getBroadcastAddress; +import static android.net.NetworkUtils.getPrefixMaskAsInet4Address; import static junit.framework.Assert.assertEquals; @@ -125,7 +127,6 @@ public class NetworkUtilsTest { assertInvalidNetworkMask(IPv4Address("255.255.0.255")); } - @Test public void testPrefixLengthToV4NetmaskIntHTL() { assertEquals(0, prefixLengthToV4NetmaskIntHTL(0)); @@ -266,4 +267,44 @@ public class NetworkUtilsTest { assertEquals(BigInteger.valueOf(7l - 4 + 4 + 16 + 65536), NetworkUtils.routedIPv6AddressCount(set)); } + + @Test + public void testGetPrefixMaskAsAddress() { + assertEquals("255.255.240.0", getPrefixMaskAsInet4Address(20).getHostAddress()); + assertEquals("255.0.0.0", getPrefixMaskAsInet4Address(8).getHostAddress()); + assertEquals("0.0.0.0", getPrefixMaskAsInet4Address(0).getHostAddress()); + assertEquals("255.255.255.255", getPrefixMaskAsInet4Address(32).getHostAddress()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetPrefixMaskAsAddress_PrefixTooLarge() { + getPrefixMaskAsInet4Address(33); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetPrefixMaskAsAddress_NegativePrefix() { + getPrefixMaskAsInet4Address(-1); + } + + @Test + public void testGetBroadcastAddress() { + assertEquals("192.168.15.255", + getBroadcastAddress(IPv4Address("192.168.0.123"), 20).getHostAddress()); + assertEquals("192.255.255.255", + getBroadcastAddress(IPv4Address("192.168.0.123"), 8).getHostAddress()); + assertEquals("192.168.0.123", + getBroadcastAddress(IPv4Address("192.168.0.123"), 32).getHostAddress()); + assertEquals("255.255.255.255", + getBroadcastAddress(IPv4Address("192.168.0.123"), 0).getHostAddress()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetBroadcastAddress_PrefixTooLarge() { + getBroadcastAddress(IPv4Address("192.168.0.123"), 33); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetBroadcastAddress_NegativePrefix() { + getBroadcastAddress(IPv4Address("192.168.0.123"), -1); + } } diff --git a/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java b/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java new file mode 100644 index 0000000000000..dfa09a9f205c6 --- /dev/null +++ b/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2018 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 android.net.dhcp; + +import static android.net.dhcp.DhcpServingParams.MTU_UNSET; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static java.net.InetAddress.parseNumericAddress; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.LinkAddress; +import android.net.dhcp.DhcpServingParams.InvalidParameterException; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DhcpServingParamsTest { + @NonNull + private DhcpServingParams.Builder mBuilder; + + private static final Set TEST_DEFAULT_ROUTERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124"))); + private static final long TEST_LEASE_TIME_SECS = 3600L; + private static final Set TEST_DNS_SERVERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127"))); + private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2"); + private static final LinkAddress TEST_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20); + private static final int TEST_MTU = 1500; + private static final Set TEST_EXCLUDED_ADDRS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201"))); + + @Before + public void setUp() { + mBuilder = new DhcpServingParams.Builder() + .setDefaultRouters(TEST_DEFAULT_ROUTERS) + .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS) + .setDnsServers(TEST_DNS_SERVERS) + .setServerAddr(TEST_LINKADDR) + .setLinkMtu(TEST_MTU) + .setExcludedAddrs(TEST_EXCLUDED_ADDRS); + } + + @Test + public void testBuild_Immutable() throws InvalidParameterException { + final Set routers = new HashSet<>(TEST_DEFAULT_ROUTERS); + final Set dnsServers = new HashSet<>(TEST_DNS_SERVERS); + final Set excludedAddrs = new HashSet<>(TEST_EXCLUDED_ADDRS); + + final DhcpServingParams params = mBuilder + .setDefaultRouters(routers) + .setDnsServers(dnsServers) + .setExcludedAddrs(excludedAddrs) + .build(); + + // Modifications to source objects should not affect builder or final parameters + final Inet4Address addedAddr = parseAddr("192.168.0.223"); + routers.add(addedAddr); + dnsServers.add(addedAddr); + excludedAddrs.add(addedAddr); + + assertEquals(TEST_DEFAULT_ROUTERS, params.defaultRouters); + assertEquals(TEST_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs); + assertEquals(TEST_DNS_SERVERS, params.dnsServers); + assertEquals(TEST_LINKADDR, params.serverAddr); + assertEquals(TEST_MTU, params.linkMtu); + + assertContains(params.excludedAddrs, TEST_EXCLUDED_ADDRS); + assertContains(params.excludedAddrs, TEST_DEFAULT_ROUTERS); + assertContains(params.excludedAddrs, TEST_DNS_SERVERS); + assertContains(params.excludedAddrs, TEST_SERVER_ADDR); + + assertFalse("excludedAddrs should not contain " + addedAddr, + params.excludedAddrs.contains(addedAddr)); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_NegativeLeaseTime() throws InvalidParameterException { + mBuilder.setDhcpLeaseTimeSecs(-1).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_LeaseTimeTooLarge() throws InvalidParameterException { + // Set lease time larger than max value for uint32 + mBuilder.setDhcpLeaseTimeSecs(1L << 32).build(); + } + + @Test + public void testBuild_InfiniteLeaseTime() throws InvalidParameterException { + final long infiniteLeaseTime = 0xffffffffL; + final DhcpServingParams params = mBuilder + .setDhcpLeaseTimeSecs(infiniteLeaseTime).build(); + assertEquals(infiniteLeaseTime, params.dhcpLeaseTimeSecs); + assertTrue(params.dhcpLeaseTimeSecs > 0L); + } + + @Test + public void testBuild_UnsetMtu() throws InvalidParameterException { + final DhcpServingParams params = mBuilder.setLinkMtu(MTU_UNSET).build(); + assertEquals(MTU_UNSET, params.linkMtu); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_MtuTooSmall() throws InvalidParameterException { + mBuilder.setLinkMtu(20).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_MtuTooLarge() throws InvalidParameterException { + mBuilder.setLinkMtu(65_536).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_IPv6Addr() throws InvalidParameterException { + mBuilder.setServerAddr(new LinkAddress(parseNumericAddress("fe80::1111"), 120)).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_PrefixTooLarge() throws InvalidParameterException { + mBuilder.setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 15)).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_PrefixTooSmall() throws InvalidParameterException { + mBuilder.setDefaultRouters(Collections.singleton(parseAddr("192.168.0.254"))) + .setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 31)) + .build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_RouterNotInPrefix() throws InvalidParameterException { + mBuilder.setDefaultRouters(Collections.singleton(parseAddr("192.168.254.254"))).build(); + } + + private static void assertContains(@NonNull Set set, @NonNull Set subset) { + for (final T elem : subset) { + assertContains(set, elem); + } + } + + private static void assertContains(@NonNull Set set, @Nullable T elem) { + assertTrue("Set does not contain " + elem, set.contains(elem)); + } + + @NonNull + private static Inet4Address parseAddr(@NonNull String inet4Addr) { + return (Inet4Address) parseNumericAddress(inet4Addr); + } +}