From 0e3d0923a65d654c57bd0ce24d84a257702e83db Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Tue, 4 Dec 2018 12:13:09 +0900 Subject: Move DhcpServer to NetworkStack app Test: atest FrameworksNetTests && atest NetworkStackTests Bug: b/112869080 Change-Id: I96c40e63e9ceb37b67705bdd4d120307e114715b --- Android.bp | 3 + core/java/android/net/ConnectivityManager.java | 2 + core/java/android/net/INetworkStackConnector.aidl | 6 +- .../android/net/INetworkStackStatusCallback.aidl | 22 + core/java/android/net/NetworkStack.java | 19 + .../java/android/net/dhcp/DhcpServerCallbacks.java | 33 ++ core/java/android/net/dhcp/IDhcpServer.aidl | 32 + .../android/net/dhcp/IDhcpServerCallbacks.aidl | 24 + packages/NetworkStack/Android.bp | 4 + .../src/android/net/dhcp/DhcpLease.java | 153 +++++ .../src/android/net/dhcp/DhcpLeaseRepository.java | 545 +++++++++++++++++ .../src/android/net/dhcp/DhcpPacketListener.java | 88 +++ .../src/android/net/dhcp/DhcpServer.java | 651 +++++++++++++++++++++ .../src/android/net/dhcp/DhcpServingParams.java | 370 ++++++++++++ .../src/android/net/util/SharedLog.java | 197 +++++++ .../com/android/server/NetworkStackService.java | 49 +- .../android/server/util/NetworkStackConstants.java | 45 ++ .../com/android/server/util/PermissionUtil.java | 42 ++ packages/NetworkStack/tests/Android.bp | 35 ++ packages/NetworkStack/tests/AndroidManifest.xml | 25 + packages/NetworkStack/tests/AndroidTest.xml | 29 + .../android/net/dhcp/DhcpLeaseRepositoryTest.java | 539 +++++++++++++++++ .../tests/src/android/net/dhcp/DhcpServerTest.java | 327 +++++++++++ .../android/net/dhcp/DhcpServingParamsTest.java | 220 +++++++ .../src/com/android/server/util/SharedLogTest.java | 96 +++ .../com/android/server/connectivity/Tethering.java | 2 +- .../tethering/TetheringDependencies.java | 4 +- services/net/Android.bp | 16 + services/net/java/android/net/dhcp/DhcpLease.java | 153 ----- .../java/android/net/dhcp/DhcpLeaseRepository.java | 544 ----------------- services/net/java/android/net/dhcp/DhcpPacket.java | 6 +- .../java/android/net/dhcp/DhcpPacketListener.java | 88 --- services/net/java/android/net/dhcp/DhcpServer.java | 582 ------------------ .../java/android/net/dhcp/DhcpServingParams.java | 365 ------------ services/net/java/android/net/ip/IpServer.java | 144 ++++- .../java/android/net/util/NetworkConstants.java | 5 - services/net/java/android/net/util/SharedLog.java | 1 + .../android/net/dhcp/DhcpLeaseRepositoryTest.java | 539 ----------------- .../net/java/android/net/dhcp/DhcpServerTest.java | 321 ---------- .../android/net/dhcp/DhcpServingParamsTest.java | 216 ------- tests/net/java/android/net/ip/IpServerTest.java | 56 +- .../android/server/connectivity/TetheringTest.java | 100 ++-- 42 files changed, 3781 insertions(+), 2917 deletions(-) create mode 100644 core/java/android/net/INetworkStackStatusCallback.aidl create mode 100644 core/java/android/net/dhcp/DhcpServerCallbacks.java create mode 100644 core/java/android/net/dhcp/IDhcpServer.aidl create mode 100644 core/java/android/net/dhcp/IDhcpServerCallbacks.aidl create mode 100644 packages/NetworkStack/src/android/net/dhcp/DhcpLease.java create mode 100644 packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java create mode 100644 packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java create mode 100644 packages/NetworkStack/src/android/net/dhcp/DhcpServer.java create mode 100644 packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java create mode 100644 packages/NetworkStack/src/android/net/util/SharedLog.java create mode 100644 packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java create mode 100644 packages/NetworkStack/src/com/android/server/util/PermissionUtil.java create mode 100644 packages/NetworkStack/tests/Android.bp create mode 100644 packages/NetworkStack/tests/AndroidManifest.xml create mode 100644 packages/NetworkStack/tests/AndroidTest.xml create mode 100644 packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java create mode 100644 packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java create mode 100644 packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java create mode 100644 packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java delete mode 100644 services/net/java/android/net/dhcp/DhcpLease.java delete mode 100644 services/net/java/android/net/dhcp/DhcpLeaseRepository.java delete mode 100644 services/net/java/android/net/dhcp/DhcpPacketListener.java delete mode 100644 services/net/java/android/net/dhcp/DhcpServer.java delete mode 100644 services/net/java/android/net/dhcp/DhcpServingParams.java delete mode 100644 tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java delete mode 100644 tests/net/java/android/net/dhcp/DhcpServerTest.java delete mode 100644 tests/net/java/android/net/dhcp/DhcpServingParamsTest.java diff --git a/Android.bp b/Android.bp index 482b19dbd8d7..565f4e05dde4 100644 --- a/Android.bp +++ b/Android.bp @@ -825,7 +825,10 @@ aidl_interface { local_include_dir: "core/java", srcs: [ "core/java/android/net/INetworkStackConnector.aidl", + "core/java/android/net/INetworkStackStatusCallback.aidl", "core/java/android/net/dhcp/DhcpServingParamsParcel.aidl", + "core/java/android/net/dhcp/IDhcpServer.aidl", + "core/java/android/net/dhcp/IDhcpServerCallbacks.aidl", ], api_dir: "aidl/networkstack", } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 61d5a9127743..529bb10afad5 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -2475,6 +2475,8 @@ public class ConnectivityManager { public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; /** {@hide} */ public static final int TETHER_ERROR_PROVISION_FAILED = 11; + /** {@hide} */ + public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; /** * Get a more detailed error code after a Tethering or Untethering diff --git a/core/java/android/net/INetworkStackConnector.aidl b/core/java/android/net/INetworkStackConnector.aidl index 29f882858c05..be0dc07f4b23 100644 --- a/core/java/android/net/INetworkStackConnector.aidl +++ b/core/java/android/net/INetworkStackConnector.aidl @@ -15,7 +15,11 @@ */ package android.net; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpServerCallbacks; + /** @hide */ oneway interface INetworkStackConnector { - // TODO: requestDhcpServer(), etc. will go here + void makeDhcpServer(in String ifName, in DhcpServingParamsParcel params, + in IDhcpServerCallbacks cb); } \ No newline at end of file diff --git a/core/java/android/net/INetworkStackStatusCallback.aidl b/core/java/android/net/INetworkStackStatusCallback.aidl new file mode 100644 index 000000000000..51032d80a172 --- /dev/null +++ b/core/java/android/net/INetworkStackStatusCallback.aidl @@ -0,0 +1,22 @@ +/* + * 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; + +/** @hide */ +oneway interface INetworkStackStatusCallback { + void onStatusAvailable(int statusCode); +} \ No newline at end of file diff --git a/core/java/android/net/NetworkStack.java b/core/java/android/net/NetworkStack.java index 82a4e31a81dd..d4a0ec632383 100644 --- a/core/java/android/net/NetworkStack.java +++ b/core/java/android/net/NetworkStack.java @@ -25,9 +25,12 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpServerCallbacks; import android.os.Binder; import android.os.IBinder; import android.os.Process; +import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.util.Slog; @@ -58,6 +61,22 @@ public class NetworkStack { public NetworkStack() { } + /** + * Create a DHCP server according to the specified parameters. + * + *

The server will be returned asynchronously through the provided callbacks. + */ + public void makeDhcpServer(final String ifName, final DhcpServingParamsParcel params, + final IDhcpServerCallbacks cb) { + requestConnector(connector -> { + try { + connector.makeDhcpServer(ifName, params, cb); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + }); + } + private class NetworkStackConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { diff --git a/core/java/android/net/dhcp/DhcpServerCallbacks.java b/core/java/android/net/dhcp/DhcpServerCallbacks.java new file mode 100644 index 000000000000..bb56876c77f5 --- /dev/null +++ b/core/java/android/net/dhcp/DhcpServerCallbacks.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Convenience wrapper around IDhcpServerCallbacks.Stub that implements getInterfaceVersion(). + * @hide + */ +public abstract class DhcpServerCallbacks extends IDhcpServerCallbacks.Stub { + // TODO: add @Override here once the API is versioned + + /** + * Get the version of the aidl interface implemented by the callbacks. + */ + public int getInterfaceVersion() { + // TODO: return IDhcpServerCallbacks.VERSION; + return 0; + } +} diff --git a/core/java/android/net/dhcp/IDhcpServer.aidl b/core/java/android/net/dhcp/IDhcpServer.aidl new file mode 100644 index 000000000000..559433b13962 --- /dev/null +++ b/core/java/android/net/dhcp/IDhcpServer.aidl @@ -0,0 +1,32 @@ +/** + * 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 perNmissions and + * limitations under the License. + */ + +package android.net.dhcp; + +import android.net.INetworkStackStatusCallback; +import android.net.dhcp.DhcpServingParamsParcel; + +/** @hide */ +oneway interface IDhcpServer { + const int STATUS_UNKNOWN = 0; + const int STATUS_SUCCESS = 1; + const int STATUS_INVALID_ARGUMENT = 2; + const int STATUS_UNKNOWN_ERROR = 3; + + void start(in INetworkStackStatusCallback cb); + void updateParams(in DhcpServingParamsParcel params, in INetworkStackStatusCallback cb); + void stop(in INetworkStackStatusCallback cb); +} diff --git a/core/java/android/net/dhcp/IDhcpServerCallbacks.aidl b/core/java/android/net/dhcp/IDhcpServerCallbacks.aidl new file mode 100644 index 000000000000..7ab4dcdbe584 --- /dev/null +++ b/core/java/android/net/dhcp/IDhcpServerCallbacks.aidl @@ -0,0 +1,24 @@ +/** + * 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 perNmissions and + * limitations under the License. + */ + +package android.net.dhcp; + +import android.net.dhcp.IDhcpServer; + +/** @hide */ +oneway interface IDhcpServerCallbacks { + void onDhcpServerCreated(int statusCode, in IDhcpServer server); +} diff --git a/packages/NetworkStack/Android.bp b/packages/NetworkStack/Android.bp index 55bb5175c28d..4688848dee0f 100644 --- a/packages/NetworkStack/Android.bp +++ b/packages/NetworkStack/Android.bp @@ -22,6 +22,10 @@ java_library { srcs: [ "src/**/*.java", ], + static_libs: [ + "dhcp-packet-lib", + "frameworks-net-shared-utils", + ] } // Updatable network stack packaged as an application diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java b/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java new file mode 100644 index 000000000000..6849cfadc22a --- /dev/null +++ b/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java @@ -0,0 +1,153 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.MacAddress; +import android.os.SystemClock; +import android.text.TextUtils; + +import com.android.internal.util.HexDump; + +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Objects; + +/** + * An IPv4 address assignment done through DHCPv4. + * @hide + */ +public class DhcpLease { + public static final long EXPIRATION_NEVER = Long.MAX_VALUE; + public static final String HOSTNAME_NONE = null; + + @Nullable + private final byte[] mClientId; + @NonNull + private final MacAddress mHwAddr; + @NonNull + private final Inet4Address mNetAddr; + /** + * Expiration time for the lease, to compare with {@link SystemClock#elapsedRealtime()}. + */ + private final long mExpTime; + @Nullable + private final String mHostname; + + public DhcpLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address netAddr, long expTime, @Nullable String hostname) { + mClientId = (clientId == null ? null : Arrays.copyOf(clientId, clientId.length)); + mHwAddr = hwAddr; + mNetAddr = netAddr; + mExpTime = expTime; + mHostname = hostname; + } + + /** + * Get the clientId associated with this lease, if any. + * + *

If the lease is not associated to a clientId, this returns null. + */ + @Nullable + public byte[] getClientId() { + if (mClientId == null) { + return null; + } + return Arrays.copyOf(mClientId, mClientId.length); + } + + @NonNull + public MacAddress getHwAddr() { + return mHwAddr; + } + + @Nullable + public String getHostname() { + return mHostname; + } + + @NonNull + public Inet4Address getNetAddr() { + return mNetAddr; + } + + public long getExpTime() { + return mExpTime; + } + + /** + * Push back the expiration time of this lease. If the provided time is sooner than the original + * expiration time, the lease time will not be updated. + * + *

The lease hostname is updated with the provided one if set. + * @return A {@link DhcpLease} with expiration time set to max(expTime, currentExpTime) + */ + public DhcpLease renewedLease(long expTime, @Nullable String hostname) { + return new DhcpLease(mClientId, mHwAddr, mNetAddr, Math.max(expTime, mExpTime), + (hostname == null ? mHostname : hostname)); + } + + /** + * Determine whether this lease matches a client with the specified parameters. + * @param clientId clientId of the client if any, or null otherwise. + * @param hwAddr Hardware address of the client. + */ + public boolean matchesClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) { + if (mClientId != null) { + return Arrays.equals(mClientId, clientId); + } else { + return clientId == null && mHwAddr.equals(hwAddr); + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DhcpLease)) { + return false; + } + final DhcpLease other = (DhcpLease) obj; + return Arrays.equals(mClientId, other.mClientId) + && mHwAddr.equals(other.mHwAddr) + && mNetAddr.equals(other.mNetAddr) + && mExpTime == other.mExpTime + && TextUtils.equals(mHostname, other.mHostname); + } + + @Override + public int hashCode() { + return Objects.hash(mClientId, mHwAddr, mNetAddr, mHostname, mExpTime); + } + + static String clientIdToString(byte[] bytes) { + if (bytes == null) { + return "null"; + } + return HexDump.toHexString(bytes); + } + + static String inet4AddrToString(@Nullable Inet4Address addr) { + return (addr == null) ? "null" : addr.getHostAddress(); + } + + @Override + public String toString() { + return String.format("clientId: %s, hwAddr: %s, netAddr: %s, expTime: %d, hostname: %s", + clientIdToString(mClientId), mHwAddr.toString(), inet4AddrToString(mNetAddr), + mExpTime, mHostname); + } +} diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java b/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java new file mode 100644 index 000000000000..0d298de4f5f8 --- /dev/null +++ b/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java @@ -0,0 +1,545 @@ +/* + * 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.inet4AddressToIntHTH; +import static android.net.NetworkUtils.intToInet4AddressHTH; +import static android.net.NetworkUtils.prefixLengthToV4NetmaskIntHTH; +import static android.net.dhcp.DhcpLease.EXPIRATION_NEVER; +import static android.net.dhcp.DhcpLease.inet4AddrToString; + +import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_BITS; + +import static java.lang.Math.min; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.IpPrefix; +import android.net.MacAddress; +import android.net.dhcp.DhcpServer.Clock; +import android.net.util.SharedLog; +import android.util.ArrayMap; + +import java.net.Inet4Address; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; + +/** + * A repository managing IPv4 address assignments through DHCPv4. + * + *

This class is not thread-safe. All public methods should be called on a common thread or + * use some synchronization mechanism. + * + *

Methods are optimized for a small number of allocated leases, assuming that most of the time + * only 2~10 addresses will be allocated, which is the common case. Managing a large number of + * addresses is supported but will be slower: some operations have complexity in O(num_leases). + * @hide + */ +class DhcpLeaseRepository { + public static final byte[] CLIENTID_UNSPEC = null; + public static final Inet4Address INETADDR_UNSPEC = null; + + @NonNull + private final SharedLog mLog; + @NonNull + private final Clock mClock; + + @NonNull + private IpPrefix mPrefix; + @NonNull + private Set mReservedAddrs; + private int mSubnetAddr; + private int mSubnetMask; + private int mNumAddresses; + private long mLeaseTimeMs; + + /** + * Next timestamp when committed or declined leases should be checked for expired ones. This + * will always be lower than or equal to the time for the first lease to expire: it's OK not to + * update this when removing entries, but it must always be updated when adding/updating. + */ + private long mNextExpirationCheck = EXPIRATION_NEVER; + + static class DhcpLeaseException extends Exception { + DhcpLeaseException(String message) { + super(message); + } + } + + static class OutOfAddressesException extends DhcpLeaseException { + OutOfAddressesException(String message) { + super(message); + } + } + + static class InvalidAddressException extends DhcpLeaseException { + InvalidAddressException(String message) { + super(message); + } + } + + static class InvalidSubnetException extends DhcpLeaseException { + InvalidSubnetException(String message) { + super(message); + } + } + + /** + * Leases by IP address + */ + private final ArrayMap mCommittedLeases = new ArrayMap<>(); + + /** + * Map address -> expiration timestamp in ms. Addresses are guaranteed to be valid as defined + * by {@link #isValidAddress(Inet4Address)}, but are not necessarily otherwise available for + * assignment. + */ + private final LinkedHashMap mDeclinedAddrs = new LinkedHashMap<>(); + + DhcpLeaseRepository(@NonNull IpPrefix prefix, @NonNull Set reservedAddrs, + long leaseTimeMs, @NonNull SharedLog log, @NonNull Clock clock) { + updateParams(prefix, reservedAddrs, leaseTimeMs); + mLog = log; + mClock = clock; + } + + public void updateParams(@NonNull IpPrefix prefix, @NonNull Set reservedAddrs, + long leaseTimeMs) { + mPrefix = prefix; + mReservedAddrs = Collections.unmodifiableSet(new HashSet<>(reservedAddrs)); + mSubnetMask = prefixLengthToV4NetmaskIntHTH(prefix.getPrefixLength()); + mSubnetAddr = inet4AddressToIntHTH((Inet4Address) prefix.getAddress()) & mSubnetMask; + mNumAddresses = 1 << (IPV4_ADDR_BITS - prefix.getPrefixLength()); + mLeaseTimeMs = leaseTimeMs; + + cleanMap(mCommittedLeases); + cleanMap(mDeclinedAddrs); + } + + /** + * From a map keyed by {@link Inet4Address}, remove entries where the key is invalid (as + * specified by {@link #isValidAddress(Inet4Address)}), or is a reserved address. + */ + private void cleanMap(Map map) { + final Iterator> it = map.entrySet().iterator(); + while (it.hasNext()) { + final Inet4Address addr = it.next().getKey(); + if (!isValidAddress(addr) || mReservedAddrs.contains(addr)) { + it.remove(); + } + } + } + + /** + * Get a DHCP offer, to reply to a DHCPDISCOVER. Follows RFC2131 #4.3.1. + * + * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} + * @param relayAddr Internet address of the relay (giaddr), can be {@link Inet4Address#ANY} + * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC} + * @param hostname Client-provided hostname, or {@link DhcpLease#HOSTNAME_NONE} + * @throws OutOfAddressesException The server does not have any available address + * @throws InvalidSubnetException The lease was requested from an unsupported subnet + */ + @NonNull + public DhcpLease getOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address relayAddr, @Nullable Inet4Address reqAddr, + @Nullable String hostname) throws OutOfAddressesException, InvalidSubnetException { + final long currentTime = mClock.elapsedRealtime(); + final long expTime = currentTime + mLeaseTimeMs; + + removeExpiredLeases(currentTime); + checkValidRelayAddr(relayAddr); + + final DhcpLease currentLease = findByClient(clientId, hwAddr); + final DhcpLease newLease; + if (currentLease != null) { + newLease = currentLease.renewedLease(expTime, hostname); + mLog.log("Offering extended lease " + newLease); + // Do not update lease time in the map: the offer is not committed yet. + } else if (reqAddr != null && isValidAddress(reqAddr) && isAvailable(reqAddr)) { + newLease = new DhcpLease(clientId, hwAddr, reqAddr, expTime, hostname); + mLog.log("Offering requested lease " + newLease); + } else { + newLease = makeNewOffer(clientId, hwAddr, expTime, hostname); + mLog.log("Offering new generated lease " + newLease); + } + return newLease; + } + + private void checkValidRelayAddr(@Nullable Inet4Address relayAddr) + throws InvalidSubnetException { + // As per #4.3.1, addresses are assigned based on the relay address if present. This + // implementation only assigns addresses if the relayAddr is inside our configured subnet. + // This also applies when the client requested a specific address for consistency between + // requests, and with older behavior. + if (isIpAddrOutsidePrefix(mPrefix, relayAddr)) { + throw new InvalidSubnetException("Lease requested by relay from outside of subnet"); + } + } + + private static boolean isIpAddrOutsidePrefix(@NonNull IpPrefix prefix, + @Nullable Inet4Address addr) { + return addr != null && !addr.equals(Inet4Address.ANY) && !prefix.contains(addr); + } + + @Nullable + private DhcpLease findByClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) { + for (DhcpLease lease : mCommittedLeases.values()) { + if (lease.matchesClient(clientId, hwAddr)) { + return lease; + } + } + + // Note this differs from dnsmasq behavior, which would match by hwAddr if clientId was + // given but no lease keyed on clientId matched. This would prevent one interface from + // obtaining multiple leases with different clientId. + return null; + } + + /** + * Make a lease conformant to a client DHCPREQUEST or renew the client's existing lease, + * commit it to the repository and return it. + * + *

This method always succeeds and commits the lease if it does not throw, and has no side + * effects if it throws. + * + * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} + * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC} + * @param sidSet Whether the server identifier was set in the request + * @return The newly created or renewed lease + * @throws InvalidAddressException The client provided an address that conflicts with its + * current configuration, or other committed/reserved leases. + */ + @NonNull + public DhcpLease requestLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address clientAddr, @NonNull Inet4Address relayAddr, + @Nullable Inet4Address reqAddr, boolean sidSet, @Nullable String hostname) + throws InvalidAddressException, InvalidSubnetException { + final long currentTime = mClock.elapsedRealtime(); + removeExpiredLeases(currentTime); + checkValidRelayAddr(relayAddr); + final DhcpLease assignedLease = findByClient(clientId, hwAddr); + + final Inet4Address leaseAddr = reqAddr != null ? reqAddr : clientAddr; + if (assignedLease != null) { + if (sidSet && reqAddr != null) { + // Client in SELECTING state; remove any current lease before creating a new one. + mCommittedLeases.remove(assignedLease.getNetAddr()); + } else if (!assignedLease.getNetAddr().equals(leaseAddr)) { + // reqAddr null (RENEWING/REBINDING): client renewing its own lease for clientAddr. + // reqAddr set with sid not set (INIT-REBOOT): client verifying configuration. + // In both cases, throw if clientAddr or reqAddr does not match the known lease. + throw new InvalidAddressException("Incorrect address for client in " + + (reqAddr != null ? "INIT-REBOOT" : "RENEWING/REBINDING")); + } + } + + // In the init-reboot case, RFC2131 #4.3.2 says that the server must not reply if + // assignedLease == null, but dnsmasq will let the client use the requested address if + // available, when configured with --dhcp-authoritative. This is preferable to avoid issues + // if the server lost the lease DB: the client would not get a reply because the server + // does not know their lease. + // Similarly in RENEWING/REBINDING state, create a lease when possible if the + // client-provided lease is unknown. + final DhcpLease lease = + checkClientAndMakeLease(clientId, hwAddr, leaseAddr, hostname, currentTime); + mLog.logf("DHCPREQUEST assignedLease %s, reqAddr=%s, sidSet=%s: created/renewed lease %s", + assignedLease, inet4AddrToString(reqAddr), sidSet, lease); + return lease; + } + + /** + * Check that the client can request the specified address, make or renew the lease if yes, and + * commit it. + * + *

This method always succeeds and returns the lease if it does not throw, and has no + * side-effect if it throws. + * + * @return The newly created or renewed, committed lease + * @throws InvalidAddressException The client provided an address that conflicts with its + * current configuration, or other committed/reserved leases. + */ + private DhcpLease checkClientAndMakeLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address addr, @Nullable String hostname, long currentTime) + throws InvalidAddressException { + final long expTime = currentTime + mLeaseTimeMs; + final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null); + if (currentLease != null && !currentLease.matchesClient(clientId, hwAddr)) { + throw new InvalidAddressException("Address in use"); + } + + final DhcpLease lease; + if (currentLease == null) { + if (isValidAddress(addr) && !mReservedAddrs.contains(addr)) { + lease = new DhcpLease(clientId, hwAddr, addr, expTime, hostname); + } else { + throw new InvalidAddressException("Lease not found and address unavailable"); + } + } else { + lease = currentLease.renewedLease(expTime, hostname); + } + commitLease(lease); + return lease; + } + + private void commitLease(@NonNull DhcpLease lease) { + mCommittedLeases.put(lease.getNetAddr(), lease); + maybeUpdateEarliestExpiration(lease.getExpTime()); + } + + /** + * Delete a committed lease from the repository. + * + * @return true if a lease matching parameters was found. + */ + public boolean releaseLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address addr) { + final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null); + if (currentLease == null) { + mLog.w("Could not release unknown lease for " + inet4AddrToString(addr)); + return false; + } + if (currentLease.matchesClient(clientId, hwAddr)) { + mCommittedLeases.remove(addr); + mLog.log("Released lease " + currentLease); + return true; + } + mLog.w(String.format("Not releasing lease %s: does not match client (cid %s, hwAddr %s)", + currentLease, DhcpLease.clientIdToString(clientId), hwAddr)); + return false; + } + + public void markLeaseDeclined(@NonNull Inet4Address addr) { + if (mDeclinedAddrs.containsKey(addr) || !isValidAddress(addr)) { + mLog.logf("Not marking %s as declined: already declined or not assignable", + inet4AddrToString(addr)); + return; + } + final long expTime = mClock.elapsedRealtime() + mLeaseTimeMs; + mDeclinedAddrs.put(addr, expTime); + mLog.logf("Marked %s as declined expiring %d", inet4AddrToString(addr), expTime); + maybeUpdateEarliestExpiration(expTime); + } + + /** + * Get the list of currently valid committed leases in the repository. + */ + @NonNull + public List getCommittedLeases() { + removeExpiredLeases(mClock.elapsedRealtime()); + return new ArrayList<>(mCommittedLeases.values()); + } + + /** + * Get the set of addresses that have been marked as declined in the repository. + */ + @NonNull + public Set getDeclinedAddresses() { + removeExpiredLeases(mClock.elapsedRealtime()); + return new HashSet<>(mDeclinedAddrs.keySet()); + } + + /** + * Given the expiration time of a new committed lease or declined address, update + * {@link #mNextExpirationCheck} so it stays lower than or equal to the time for the first lease + * to expire. + */ + private void maybeUpdateEarliestExpiration(long expTime) { + if (expTime < mNextExpirationCheck) { + mNextExpirationCheck = expTime; + } + } + + /** + * Remove expired entries from a map keyed by {@link Inet4Address}. + * + * @param tag Type of lease in the map, for logging + * @param getExpTime Functor returning the expiration time for an object in the map. + * Must not return null. + * @return The lowest expiration time among entries remaining in the map + */ + private long removeExpired(long currentTime, @NonNull Map map, + @NonNull String tag, @NonNull Function getExpTime) { + final Iterator> it = map.entrySet().iterator(); + long firstExpiration = EXPIRATION_NEVER; + while (it.hasNext()) { + final Entry lease = it.next(); + final long expTime = getExpTime.apply(lease.getValue()); + if (expTime <= currentTime) { + mLog.logf("Removing expired %s lease for %s (expTime=%s, currentTime=%s)", + tag, lease.getKey(), expTime, currentTime); + it.remove(); + } else { + firstExpiration = min(firstExpiration, expTime); + } + } + return firstExpiration; + } + + /** + * Go through committed and declined leases and remove the expired ones. + */ + private void removeExpiredLeases(long currentTime) { + if (currentTime < mNextExpirationCheck) { + return; + } + + final long commExp = removeExpired( + currentTime, mCommittedLeases, "committed", DhcpLease::getExpTime); + final long declExp = removeExpired( + currentTime, mDeclinedAddrs, "declined", Function.identity()); + + mNextExpirationCheck = min(commExp, declExp); + } + + private boolean isAvailable(@NonNull Inet4Address addr) { + return !mReservedAddrs.contains(addr) && !mCommittedLeases.containsKey(addr); + } + + /** + * Get the 0-based index of an address in the subnet. + * + *

Given ordering of addresses 5.6.7.8 < 5.6.7.9 < 5.6.8.0, the index on a subnet is defined + * so that the first address is 0, the second 1, etc. For example on a /16, 192.168.0.0 -> 0, + * 192.168.0.1 -> 1, 192.168.1.0 -> 256 + * + */ + private int getAddrIndex(int addr) { + return addr & ~mSubnetMask; + } + + private int getAddrByIndex(int index) { + return mSubnetAddr | index; + } + + /** + * Get a valid address starting from the supplied one. + * + *

This only checks that the address is numerically valid for assignment, not whether it is + * already in use. The return value is always inside the configured prefix, even if the supplied + * address is not. + * + *

If the provided address is valid, it is returned as-is. Otherwise, the next valid + * address (with the ordering in {@link #getAddrIndex(int)}) is returned. + */ + private int getValidAddress(int addr) { + final int lastByteMask = 0xff; + int addrIndex = getAddrIndex(addr); // 0-based index of the address in the subnet + + // Some OSes do not handle addresses in .255 or .0 correctly: avoid those. + final int lastByte = getAddrByIndex(addrIndex) & lastByteMask; + if (lastByte == lastByteMask) { + // Avoid .255 address, and .0 address that follows + addrIndex = (addrIndex + 2) % mNumAddresses; + } else if (lastByte == 0) { + // Avoid .0 address + addrIndex = (addrIndex + 1) % mNumAddresses; + } + + // Do not use first or last address of range + if (addrIndex == 0 || addrIndex == mNumAddresses - 1) { + // Always valid and not end of range since prefixLength is at most 30 in serving params + addrIndex = 1; + } + return getAddrByIndex(addrIndex); + } + + /** + * Returns whether the address is in the configured subnet and part of the assignable range. + */ + private boolean isValidAddress(Inet4Address addr) { + final int intAddr = inet4AddressToIntHTH(addr); + return getValidAddress(intAddr) == intAddr; + } + + private int getNextAddress(int addr) { + final int addrIndex = getAddrIndex(addr); + final int nextAddress = getAddrByIndex((addrIndex + 1) % mNumAddresses); + return getValidAddress(nextAddress); + } + + /** + * Calculate a first candidate address for a client by hashing the hardware address. + * + *

This will be a valid address as checked by {@link #getValidAddress(int)}, but may be + * in use. + * + * @return An IPv4 address encoded as 32-bit int + */ + private int getFirstClientAddress(MacAddress hwAddr) { + // This follows dnsmasq behavior. Advantages are: clients will often get the same + // offers for different DISCOVER even if the lease was not yet accepted or has expired, + // and address generation will generally not need to loop through many allocated addresses + // until it finds a free one. + int hash = 0; + for (byte b : hwAddr.toByteArray()) { + hash += b + (b << 8) + (b << 16); + } + // This implementation will not always result in the same IPs as dnsmasq would give out in + // Android <= P, because it includes invalid and reserved addresses in mNumAddresses while + // the configured ranges for dnsmasq did not. + final int addrIndex = hash % mNumAddresses; + return getValidAddress(getAddrByIndex(addrIndex)); + } + + /** + * Create a lease that can be offered to respond to a client DISCOVER. + * + *

This method always succeeds and returns the lease if it does not throw. If no non-declined + * address is available, it will try to offer the oldest declined address if valid. + * + * @throws OutOfAddressesException The server has no address left to offer + */ + private DhcpLease makeNewOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + long expTime, @Nullable String hostname) throws OutOfAddressesException { + int intAddr = getFirstClientAddress(hwAddr); + // 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 = intToInet4AddressHTH(intAddr); + if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) { + return new DhcpLease(clientId, hwAddr, addr, expTime, hostname); + } + intAddr = getNextAddress(intAddr); + } + + // Try freeing DECLINEd addresses if out of addresses. + final Iterator it = mDeclinedAddrs.keySet().iterator(); + while (it.hasNext()) { + final Inet4Address addr = it.next(); + it.remove(); + mLog.logf("Out of addresses in address pool: dropped declined addr %s", + inet4AddrToString(addr)); + // isValidAddress() is always verified for entries in mDeclinedAddrs. + // However declined addresses may have been requested (typically by the machine that was + // already using the address) after being declined. + if (isAvailable(addr)) { + return new DhcpLease(clientId, hwAddr, addr, expTime, hostname); + } + } + + throw new OutOfAddressesException("No address available for offer"); + } +} diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java b/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java new file mode 100644 index 000000000000..dce8b619494e --- /dev/null +++ b/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java @@ -0,0 +1,88 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.util.FdEventsReader; +import android.os.Handler; +import android.system.Os; + +import java.io.FileDescriptor; +import java.net.Inet4Address; +import java.net.InetSocketAddress; + +/** + * A {@link FdEventsReader} to receive and parse {@link DhcpPacket}. + * @hide + */ +abstract class DhcpPacketListener extends FdEventsReader { + static final class Payload { + protected final byte[] mBytes = new byte[DhcpPacket.MAX_LENGTH]; + protected Inet4Address mSrcAddr; + protected int mSrcPort; + } + + DhcpPacketListener(@NonNull Handler handler) { + super(handler, new Payload()); + } + + @Override + protected int recvBufSize(@NonNull Payload buffer) { + return buffer.mBytes.length; + } + + @Override + protected final void handlePacket(@NonNull Payload recvbuf, int length) { + if (recvbuf.mSrcAddr == null) { + return; + } + + try { + final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf.mBytes, length, + DhcpPacket.ENCAP_BOOTP); + onReceive(packet, recvbuf.mSrcAddr, recvbuf.mSrcPort); + } catch (DhcpPacket.ParseException e) { + logParseError(recvbuf.mBytes, length, e); + } + } + + @Override + protected int readPacket(@NonNull FileDescriptor fd, @NonNull Payload packetBuffer) + throws Exception { + final InetSocketAddress addr = new InetSocketAddress(); + final int read = Os.recvfrom( + fd, packetBuffer.mBytes, 0, packetBuffer.mBytes.length, 0 /* flags */, addr); + + // Buffers with null srcAddr will be dropped in handlePacket() + packetBuffer.mSrcAddr = inet4AddrOrNull(addr); + packetBuffer.mSrcPort = addr.getPort(); + return read; + } + + @Nullable + private static Inet4Address inet4AddrOrNull(@NonNull InetSocketAddress addr) { + return addr.getAddress() instanceof Inet4Address + ? (Inet4Address) addr.getAddress() + : null; + } + + protected abstract void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr, + int srcPort); + protected abstract void logParseError(@NonNull byte[] packet, int length, + @NonNull DhcpPacket.ParseException e); +} diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java b/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java new file mode 100644 index 000000000000..14e293694ebd --- /dev/null +++ b/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java @@ -0,0 +1,651 @@ +/* + * 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.getBroadcastAddress; +import static android.net.NetworkUtils.getPrefixMaskAsInet4Address; +import static android.net.TrafficStats.TAG_SYSTEM_DHCP_SERVER; +import static android.net.dhcp.DhcpPacket.DHCP_CLIENT; +import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME; +import static android.net.dhcp.DhcpPacket.DHCP_SERVER; +import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP; +import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOL_SOCKET; +import static android.system.OsConstants.SO_BINDTODEVICE; +import static android.system.OsConstants.SO_BROADCAST; +import static android.system.OsConstants.SO_REUSEADDR; + +import static com.android.server.util.NetworkStackConstants.INFINITE_LEASE; +import static com.android.server.util.PermissionUtil.checkNetworkStackCallingPermission; + +import static java.lang.Integer.toUnsignedLong; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.INetworkStackStatusCallback; +import android.net.MacAddress; +import android.net.NetworkUtils; +import android.net.TrafficStats; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.SystemClock; +import android.system.ErrnoException; +import android.system.Os; +import android.text.TextUtils; +import android.util.Pair; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.HexDump; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * A DHCPv4 server. + * + *

This server listens for and responds to packets on a single interface. It considers itself + * authoritative for all leases on the subnet, which means that DHCP requests for unknown leases of + * unknown hosts receive a reply instead of being ignored. + * + *

The server is single-threaded (including send/receive operations): all internal operations are + * done on the provided {@link Looper}. Public methods are thread-safe and will schedule operations + * on the looper asynchronously. + * @hide + */ +public class DhcpServer extends IDhcpServer.Stub { + private static final String REPO_TAG = "Repository"; + + // Lease time to transmit to client instead of a negative time in case a lease expired before + // the server could send it (if the server process is suspended for example). + private static final int EXPIRED_FALLBACK_LEASE_TIME_SECS = 120; + + private static final int CMD_START_DHCP_SERVER = 1; + private static final int CMD_STOP_DHCP_SERVER = 2; + private static final int CMD_UPDATE_PARAMS = 3; + + @NonNull + private final HandlerThread mHandlerThread; + @NonNull + private final String mIfName; + @NonNull + private final DhcpLeaseRepository mLeaseRepo; + @NonNull + private final SharedLog mLog; + @NonNull + private final Dependencies mDeps; + @NonNull + private final Clock mClock; + + @Nullable + private volatile ServerHandler mHandler; + + // Accessed only on the handler thread + @Nullable + private DhcpPacketListener mPacketListener; + @Nullable + private FileDescriptor mSocket; + @NonNull + private DhcpServingParams mServingParams; + + /** + * Clock to be used by DhcpServer to track time for lease expiration. + * + *

The clock should track time as may be measured by clients obtaining a lease. It does not + * need to be monotonous across restarts of the server as long as leases are cleared when the + * server is stopped. + */ + public static class Clock { + /** + * @see SystemClock#elapsedRealtime() + */ + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + } + + /** + * Dependencies for the DhcpServer. Useful to be mocked in tests. + */ + public interface Dependencies { + /** + * Send a packet to the specified datagram socket. + * + * @param fd File descriptor of the socket. + * @param buffer Data to be sent. + * @param dst Destination address of the packet. + */ + void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer, + @NonNull InetAddress dst) throws ErrnoException, IOException; + + /** + * Create a DhcpLeaseRepository for the server. + * @param servingParams Parameters used to serve DHCP requests. + * @param log Log to be used by the repository. + * @param clock Clock that the repository must use to track time. + */ + DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams, + @NonNull SharedLog log, @NonNull Clock clock); + + /** + * Create a packet listener that will send packets to be processed. + */ + DhcpPacketListener makePacketListener(); + + /** + * Create a clock that the server will use to track time. + */ + Clock makeClock(); + + /** + * Add an entry to the ARP cache table. + * @param fd Datagram socket file descriptor that must use the new entry. + */ + void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr, + @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException; + + /** + * Verify that the caller is allowed to call public methods on DhcpServer. + * @throws SecurityException The caller is not allowed to call public methods on DhcpServer. + */ + void checkCaller() throws SecurityException; + } + + private class DependenciesImpl implements Dependencies { + @Override + public void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer, + @NonNull InetAddress dst) throws ErrnoException, IOException { + Os.sendto(fd, buffer, 0, dst, DhcpPacket.DHCP_CLIENT); + } + + @Override + public DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams, + @NonNull SharedLog log, @NonNull Clock clock) { + return new DhcpLeaseRepository( + DhcpServingParams.makeIpPrefix(servingParams.serverAddr), + servingParams.excludedAddrs, + servingParams.dhcpLeaseTimeSecs * 1000, log.forSubComponent(REPO_TAG), clock); + } + + @Override + public DhcpPacketListener makePacketListener() { + return new PacketListener(); + } + + @Override + public Clock makeClock() { + return new Clock(); + } + + @Override + public void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr, + @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException { + NetworkUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd); + } + + @Override + public void checkCaller() { + checkNetworkStackCallingPermission(); + } + } + + private static class MalformedPacketException extends Exception { + MalformedPacketException(String message, Throwable t) { + super(message, t); + } + } + + public DhcpServer(@NonNull String ifName, + @NonNull DhcpServingParams params, @NonNull SharedLog log) { + this(new HandlerThread(DhcpServer.class.getSimpleName() + "." + ifName), + ifName, params, log, null); + } + + @VisibleForTesting + DhcpServer(@NonNull HandlerThread handlerThread, @NonNull String ifName, + @NonNull DhcpServingParams params, @NonNull SharedLog log, + @Nullable Dependencies deps) { + if (deps == null) { + deps = new DependenciesImpl(); + } + mHandlerThread = handlerThread; + mIfName = ifName; + mServingParams = params; + mLog = log; + mDeps = deps; + mClock = deps.makeClock(); + mLeaseRepo = deps.makeLeaseRepository(mServingParams, mLog, mClock); + } + + /** + * Start listening for and responding to packets. + * + *

It is not legal to call this method more than once; in particular the server cannot be + * restarted after being stopped. + */ + @Override + public void start(@Nullable INetworkStackStatusCallback cb) { + mDeps.checkCaller(); + mHandlerThread.start(); + mHandler = new ServerHandler(mHandlerThread.getLooper()); + sendMessage(CMD_START_DHCP_SERVER, cb); + } + + /** + * Update serving parameters. All subsequently received requests will be handled with the new + * parameters, and current leases that are incompatible with the new parameters are dropped. + */ + @Override + public void updateParams(@Nullable DhcpServingParamsParcel params, + @Nullable INetworkStackStatusCallback cb) throws RemoteException { + mDeps.checkCaller(); + final DhcpServingParams parsedParams; + try { + // throws InvalidParameterException with null params + parsedParams = DhcpServingParams.fromParcelableObject(params); + } catch (DhcpServingParams.InvalidParameterException e) { + mLog.e("Invalid parameters sent to DhcpServer", e); + if (cb != null) { + cb.onStatusAvailable(STATUS_INVALID_ARGUMENT); + } + return; + } + sendMessage(CMD_UPDATE_PARAMS, new Pair<>(parsedParams, cb)); + } + + /** + * Stop listening for packets. + * + *

As the server is stopped asynchronously, some packets may still be processed shortly after + * calling this method. + */ + @Override + public void stop(@Nullable INetworkStackStatusCallback cb) { + mDeps.checkCaller(); + sendMessage(CMD_STOP_DHCP_SERVER, cb); + } + + private void sendMessage(int what, @Nullable Object obj) { + if (mHandler == null) { + mLog.e("Attempting to send a command to stopped DhcpServer: " + what); + return; + } + mHandler.sendMessage(mHandler.obtainMessage(what, obj)); + } + + private class ServerHandler extends Handler { + ServerHandler(@NonNull Looper looper) { + super(looper); + } + + @Override + public void handleMessage(@NonNull Message msg) { + final INetworkStackStatusCallback cb; + switch (msg.what) { + case CMD_UPDATE_PARAMS: + final Pair pair = + (Pair) msg.obj; + final DhcpServingParams params = pair.first; + mServingParams = params; + mLeaseRepo.updateParams( + DhcpServingParams.makeIpPrefix(mServingParams.serverAddr), + params.excludedAddrs, + params.dhcpLeaseTimeSecs); + + cb = pair.second; + break; + case CMD_START_DHCP_SERVER: + mPacketListener = mDeps.makePacketListener(); + mPacketListener.start(); + cb = (INetworkStackStatusCallback) msg.obj; + break; + case CMD_STOP_DHCP_SERVER: + if (mPacketListener != null) { + mPacketListener.stop(); + mPacketListener = null; + } + mHandlerThread.quitSafely(); + cb = (INetworkStackStatusCallback) msg.obj; + break; + default: + return; + } + if (cb != null) { + try { + cb.onStatusAvailable(STATUS_SUCCESS); + } catch (RemoteException e) { + mLog.e("Could not send status back to caller", e); + } + } + } + } + + @VisibleForTesting + void processPacket(@NonNull DhcpPacket packet, int srcPort) { + final String packetType = packet.getClass().getSimpleName(); + if (srcPort != DHCP_CLIENT) { + mLog.logf("Ignored packet of type %s sent from client port %d", packetType, srcPort); + return; + } + + mLog.log("Received packet of type " + packetType); + final Inet4Address sid = packet.mServerIdentifier; + if (sid != null && !sid.equals(mServingParams.serverAddr.getAddress())) { + mLog.log("Packet ignored due to wrong server identifier: " + sid); + return; + } + + try { + if (packet instanceof DhcpDiscoverPacket) { + processDiscover((DhcpDiscoverPacket) packet); + } else if (packet instanceof DhcpRequestPacket) { + processRequest((DhcpRequestPacket) packet); + } else if (packet instanceof DhcpReleasePacket) { + processRelease((DhcpReleasePacket) packet); + } else { + mLog.e("Unknown packet type: " + packet.getClass().getSimpleName()); + } + } catch (MalformedPacketException e) { + // Not an internal error: only logging exception message, not stacktrace + mLog.e("Ignored malformed packet: " + e.getMessage()); + } + } + + private void logIgnoredPacketInvalidSubnet(DhcpLeaseRepository.InvalidSubnetException e) { + // Not an internal error: only logging exception message, not stacktrace + mLog.e("Ignored packet from invalid subnet: " + e.getMessage()); + } + + private void processDiscover(@NonNull DhcpDiscoverPacket packet) + throws MalformedPacketException { + final DhcpLease lease; + final MacAddress clientMac = getMacAddr(packet); + try { + lease = mLeaseRepo.getOffer(packet.getExplicitClientIdOrNull(), clientMac, + packet.mRelayIp, packet.mRequestedIp, packet.mHostName); + } catch (DhcpLeaseRepository.OutOfAddressesException e) { + transmitNak(packet, "Out of addresses to offer"); + return; + } catch (DhcpLeaseRepository.InvalidSubnetException e) { + logIgnoredPacketInvalidSubnet(e); + return; + } + + transmitOffer(packet, lease, clientMac); + } + + private void processRequest(@NonNull DhcpRequestPacket packet) throws MalformedPacketException { + // If set, packet SID matches with this server's ID as checked in processPacket(). + final boolean sidSet = packet.mServerIdentifier != null; + final DhcpLease lease; + final MacAddress clientMac = getMacAddr(packet); + try { + lease = mLeaseRepo.requestLease(packet.getExplicitClientIdOrNull(), clientMac, + packet.mClientIp, packet.mRelayIp, packet.mRequestedIp, sidSet, + packet.mHostName); + } catch (DhcpLeaseRepository.InvalidAddressException e) { + transmitNak(packet, "Invalid requested address"); + return; + } catch (DhcpLeaseRepository.InvalidSubnetException e) { + logIgnoredPacketInvalidSubnet(e); + return; + } + + transmitAck(packet, lease, clientMac); + } + + private void processRelease(@NonNull DhcpReleasePacket packet) + throws MalformedPacketException { + final byte[] clientId = packet.getExplicitClientIdOrNull(); + final MacAddress macAddr = getMacAddr(packet); + // Don't care about success (there is no ACK/NAK); logging is already done in the repository + mLeaseRepo.releaseLease(clientId, macAddr, packet.mClientIp); + } + + private Inet4Address getAckOrOfferDst(@NonNull DhcpPacket request, @NonNull DhcpLease lease, + boolean broadcastFlag) { + // Unless relayed or broadcast, send to client IP if already configured on the client, or to + // the lease address if the client has no configured address + if (!isEmpty(request.mRelayIp)) { + return request.mRelayIp; + } else if (broadcastFlag) { + return (Inet4Address) Inet4Address.ALL; + } else if (!isEmpty(request.mClientIp)) { + return request.mClientIp; + } else { + return lease.getNetAddr(); + } + } + + /** + * Determine whether the broadcast flag should be set in the BOOTP packet flags. This does not + * apply to NAK responses, which should always have it set. + */ + private static boolean getBroadcastFlag(@NonNull DhcpPacket request, @NonNull DhcpLease lease) { + // No broadcast flag if the client already has a configured IP to unicast to. RFC2131 #4.1 + // has some contradictions regarding broadcast behavior if a client already has an IP + // configured and sends a request with both ciaddr (renew/rebind) and the broadcast flag + // set. Sending a unicast response to ciaddr matches previous behavior and is more + // efficient. + // If the client has no configured IP, broadcast if requested by the client or if the lease + // address cannot be used to send a unicast reply either. + return isEmpty(request.mClientIp) && (request.mBroadcast || isEmpty(lease.getNetAddr())); + } + + /** + * Get the hostname from a lease if non-empty and requested in the incoming request. + * @param request The incoming request. + * @return The hostname, or null if not requested or empty. + */ + @Nullable + private static String getHostnameIfRequested(@NonNull DhcpPacket request, + @NonNull DhcpLease lease) { + return request.hasRequestedParam(DHCP_HOST_NAME) && !TextUtils.isEmpty(lease.getHostname()) + ? lease.getHostname() + : null; + } + + private boolean transmitOffer(@NonNull DhcpPacket request, @NonNull DhcpLease lease, + @NonNull MacAddress clientMac) { + final boolean broadcastFlag = getBroadcastFlag(request, lease); + final int timeout = getLeaseTimeout(lease); + final Inet4Address prefixMask = + getPrefixMaskAsInet4Address(mServingParams.serverAddr.getPrefixLength()); + final Inet4Address broadcastAddr = getBroadcastAddress( + mServingParams.getServerInet4Addr(), mServingParams.serverAddr.getPrefixLength()); + final String hostname = getHostnameIfRequested(request, lease); + final ByteBuffer offerPacket = DhcpPacket.buildOfferPacket( + ENCAP_BOOTP, request.mTransId, broadcastFlag, mServingParams.getServerInet4Addr(), + request.mRelayIp, lease.getNetAddr(), request.mClientMac, timeout, prefixMask, + broadcastAddr, new ArrayList<>(mServingParams.defaultRouters), + new ArrayList<>(mServingParams.dnsServers), + mServingParams.getServerInet4Addr(), null /* domainName */, hostname, + mServingParams.metered, (short) mServingParams.linkMtu); + + return transmitOfferOrAckPacket(offerPacket, request, lease, clientMac, broadcastFlag); + } + + private boolean transmitAck(@NonNull DhcpPacket request, @NonNull DhcpLease lease, + @NonNull MacAddress clientMac) { + // TODO: replace DhcpPacket's build methods with real builders and use common code with + // transmitOffer above + final boolean broadcastFlag = getBroadcastFlag(request, lease); + final int timeout = getLeaseTimeout(lease); + final String hostname = getHostnameIfRequested(request, lease); + final ByteBuffer ackPacket = DhcpPacket.buildAckPacket(ENCAP_BOOTP, request.mTransId, + broadcastFlag, mServingParams.getServerInet4Addr(), request.mRelayIp, + lease.getNetAddr(), request.mClientIp, request.mClientMac, timeout, + mServingParams.getPrefixMaskAsAddress(), mServingParams.getBroadcastAddress(), + new ArrayList<>(mServingParams.defaultRouters), + new ArrayList<>(mServingParams.dnsServers), + mServingParams.getServerInet4Addr(), null /* domainName */, hostname, + mServingParams.metered, (short) mServingParams.linkMtu); + + return transmitOfferOrAckPacket(ackPacket, request, lease, clientMac, broadcastFlag); + } + + private boolean transmitNak(DhcpPacket request, String message) { + mLog.w("Transmitting NAK: " + message); + // Always set broadcast flag for NAK: client may not have a correct IP + final ByteBuffer nakPacket = DhcpPacket.buildNakPacket( + ENCAP_BOOTP, request.mTransId, mServingParams.getServerInet4Addr(), + request.mRelayIp, request.mClientMac, true /* broadcast */, message); + + final Inet4Address dst = isEmpty(request.mRelayIp) + ? (Inet4Address) Inet4Address.ALL + : request.mRelayIp; + return transmitPacket(nakPacket, DhcpNakPacket.class.getSimpleName(), dst); + } + + private boolean transmitOfferOrAckPacket(@NonNull ByteBuffer buf, @NonNull DhcpPacket request, + @NonNull DhcpLease lease, @NonNull MacAddress clientMac, boolean broadcastFlag) { + mLog.logf("Transmitting %s with lease %s", request.getClass().getSimpleName(), lease); + // Client may not yet respond to ARP for the lease address, which may be the destination + // address. Add an entry to the ARP cache to save future ARP probes and make sure the + // packet reaches its destination. + if (!addArpEntry(clientMac, lease.getNetAddr())) { + // Logging for error already done + return false; + } + final Inet4Address dst = getAckOrOfferDst(request, lease, broadcastFlag); + return transmitPacket(buf, request.getClass().getSimpleName(), dst); + } + + private boolean transmitPacket(@NonNull ByteBuffer buf, @NonNull String packetTypeTag, + @NonNull Inet4Address dst) { + try { + mDeps.sendPacket(mSocket, buf, dst); + } catch (ErrnoException | IOException e) { + mLog.e("Can't send packet " + packetTypeTag, e); + return false; + } + return true; + } + + private boolean addArpEntry(@NonNull MacAddress macAddr, @NonNull Inet4Address inetAddr) { + try { + mDeps.addArpEntry(inetAddr, macAddr, mIfName, mSocket); + return true; + } catch (IOException e) { + mLog.e("Error adding client to ARP table", e); + return false; + } + } + + /** + * Get the remaining lease time in seconds, starting from {@link Clock#elapsedRealtime()}. + * + *

This is an unsigned 32-bit integer, so it cannot be read as a standard (signed) Java int. + * The return value is only intended to be used to populate the lease time field in a DHCP + * response, considering that lease time is an unsigned 32-bit integer field in DHCP packets. + * + *

Lease expiration times are tracked internally with millisecond precision: this method + * returns a rounded down value. + */ + private int getLeaseTimeout(@NonNull DhcpLease lease) { + final long remainingTimeSecs = (lease.getExpTime() - mClock.elapsedRealtime()) / 1000; + if (remainingTimeSecs < 0) { + mLog.e("Processing expired lease " + lease); + return EXPIRED_FALLBACK_LEASE_TIME_SECS; + } + + if (remainingTimeSecs >= toUnsignedLong(INFINITE_LEASE)) { + return INFINITE_LEASE; + } + + return (int) remainingTimeSecs; + } + + /** + * Get the client MAC address from a packet. + * + * @throws MalformedPacketException The address in the packet uses an unsupported format. + */ + @NonNull + private MacAddress getMacAddr(@NonNull DhcpPacket packet) throws MalformedPacketException { + try { + return MacAddress.fromBytes(packet.getClientMac()); + } catch (IllegalArgumentException e) { + final String message = "Invalid MAC address in packet: " + + HexDump.dumpHexString(packet.getClientMac()); + throw new MalformedPacketException(message, e); + } + } + + private static boolean isEmpty(@Nullable Inet4Address address) { + return address == null || Inet4Address.ANY.equals(address); + } + + private class PacketListener extends DhcpPacketListener { + PacketListener() { + super(mHandler); + } + + @Override + protected void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr, + int srcPort) { + processPacket(packet, srcPort); + } + + @Override + protected void logError(@NonNull String msg, Exception e) { + mLog.e("Error receiving packet: " + msg, e); + } + + @Override + protected void logParseError(@NonNull byte[] packet, int length, + @NonNull DhcpPacket.ParseException e) { + mLog.e("Error parsing packet", e); + } + + @Override + protected FileDescriptor createFd() { + // TODO: have and use an API to set a socket tag without going through the thread tag + final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_DHCP_SERVER); + try { + mSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + Os.setsockoptInt(mSocket, SOL_SOCKET, SO_REUSEADDR, 1); + // SO_BINDTODEVICE actually takes a string. This works because the first member + // of struct ifreq is a NULL-terminated interface name. + // TODO: add a setsockoptString() + Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mIfName); + Os.setsockoptInt(mSocket, SOL_SOCKET, SO_BROADCAST, 1); + Os.bind(mSocket, Inet4Address.ANY, DHCP_SERVER); + NetworkUtils.protectFromVpn(mSocket); + + return mSocket; + } catch (IOException | ErrnoException e) { + mLog.e("Error creating UDP socket", e); + DhcpServer.this.stop(null); + return null; + } finally { + TrafficStats.setThreadStatsTag(oldTag); + } + } + } +} diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java b/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java new file mode 100644 index 000000000000..f38888aafbd6 --- /dev/null +++ b/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java @@ -0,0 +1,370 @@ +/* + * 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.NetworkUtils.intToInet4AddressHTH; + +import static com.android.server.util.NetworkStackConstants.INFINITE_LEASE; +import static com.android.server.util.NetworkStackConstants.IPV4_MAX_MTU; +import static com.android.server.util.NetworkStackConstants.IPV4_MIN_MTU; + +import static java.lang.Integer.toUnsignedLong; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.NetworkUtils; + +import com.google.android.collect.Sets; + +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; + + /** + * Indicates whether the DHCP server should send the ANDROID_METERED vendor-specific option. + */ + public final boolean metered; + + /** + * 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, boolean metered) { + this.serverAddr = serverAddr; + this.defaultRouters = defaultRouters; + this.dnsServers = dnsServers; + this.excludedAddrs = excludedAddrs; + this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs; + this.linkMtu = linkMtu; + this.metered = metered; + } + + /** + * Create parameters from a stable AIDL-compatible parcel. + * @throws InvalidParameterException The parameters parcelable is null or invalid. + */ + public static DhcpServingParams fromParcelableObject(@Nullable DhcpServingParamsParcel parcel) + throws InvalidParameterException { + if (parcel == null) { + throw new InvalidParameterException("Null serving parameters"); + } + final LinkAddress serverAddr = new LinkAddress( + intToInet4AddressHTH(parcel.serverAddr), + parcel.serverAddrPrefixLength); + return new Builder() + .setServerAddr(serverAddr) + .setDefaultRouters(toInet4AddressSet(parcel.defaultRouters)) + .setDnsServers(toInet4AddressSet(parcel.dnsServers)) + .setExcludedAddrs(toInet4AddressSet(parcel.excludedAddrs)) + .setDhcpLeaseTimeSecs(parcel.dhcpLeaseTimeSecs) + .setLinkMtu(parcel.linkMtu) + .setMetered(parcel.metered) + .build(); + } + + private static Set toInet4AddressSet(@Nullable int[] addrs) { + if (addrs == null) { + return new HashSet<>(0); + } + + final HashSet res = new HashSet<>(); + for (int addr : addrs) { + res.add(intToInet4AddressHTH(addr)); + } + return res; + } + + @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 mServerAddr; + private Set mDefaultRouters; + private Set mDnsServers; + private Set mExcludedAddrs; + private long mDhcpLeaseTimeSecs; + private int mLinkMtu = MTU_UNSET; + private boolean mMetered; + + /** + * Set the server address and served prefix for the DHCP server. + * + *

This parameter is required. + */ + public Builder setServerAddr(@NonNull LinkAddress serverAddr) { + this.mServerAddr = 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.mDefaultRouters = defaultRouters; + 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 list of routers, + * but it must always be set explicitly before building the {@link DhcpServingParams}. + */ + public Builder setDefaultRouters(@NonNull Inet4Address... defaultRouters) { + return setDefaultRouters(Sets.newArraySet(defaultRouters)); + } + + /** + * Convenience method to build the parameters with no default router. + * + *

Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address. + */ + public Builder withNoDefaultRouter() { + return setDefaultRouters(); + } + + /** + * 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.mDnsServers = dnsServers; + return this; + } + + /** + * Set the DNS servers to be advertised to DHCP clients. + * + *

This may be an empty list of servers, but it must always be set explicitly before + * building the {@link DhcpServingParams}. + */ + public Builder setDnsServers(@NonNull Inet4Address... dnsServers) { + return setDnsServers(Sets.newArraySet(dnsServers)); + } + + /** + * Convenience method to build the parameters with no DNS server. + * + *

Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address. + */ + public Builder withNoDnsServer() { + return setDnsServers(); + } + + /** + * 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.mExcludedAddrs = excludedAddrs; + 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 Inet4Address... excludedAddrs) { + return setExcludedAddrs(Sets.newArraySet(excludedAddrs)); + } + + /** + * Set the lease time for leases assigned by the DHCP server. + * + *

This parameter is required. + */ + public Builder setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) { + this.mDhcpLeaseTimeSecs = 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.mLinkMtu = linkMtu; + return this; + } + + /** + * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option. + * + *

If not set, the default value is false. + */ + public Builder setMetered(boolean metered) { + this.mMetered = metered; + 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 (mServerAddr == null) { + throw new InvalidParameterException("Missing serverAddr"); + } + if (mDefaultRouters == null) { + throw new InvalidParameterException("Missing defaultRouters"); + } + if (mDnsServers == null) { + // Empty set is OK, but enforce explicitly setting it + throw new InvalidParameterException("Missing dnsServers"); + } + if (mDhcpLeaseTimeSecs <= 0 || mDhcpLeaseTimeSecs > toUnsignedLong(INFINITE_LEASE)) { + throw new InvalidParameterException("Invalid lease time: " + mDhcpLeaseTimeSecs); + } + if (mLinkMtu != MTU_UNSET && (mLinkMtu < IPV4_MIN_MTU || mLinkMtu > IPV4_MAX_MTU)) { + throw new InvalidParameterException("Invalid link MTU: " + mLinkMtu); + } + if (!mServerAddr.isIPv4()) { + throw new InvalidParameterException("serverAddr must be IPv4"); + } + if (mServerAddr.getPrefixLength() < MIN_PREFIX_LENGTH + || mServerAddr.getPrefixLength() > MAX_PREFIX_LENGTH) { + throw new InvalidParameterException("Prefix length is not in supported range"); + } + + final IpPrefix prefix = makeIpPrefix(mServerAddr); + for (Inet4Address addr : mDefaultRouters) { + if (!prefix.contains(addr)) { + throw new InvalidParameterException(String.format( + "Default router %s is not in server prefix %s", addr, mServerAddr)); + } + } + + final Set excl = new HashSet<>(); + if (mExcludedAddrs != null) { + excl.addAll(mExcludedAddrs); + } + excl.add((Inet4Address) mServerAddr.getAddress()); + excl.addAll(mDefaultRouters); + excl.addAll(mDnsServers); + + return new DhcpServingParams(mServerAddr, + Collections.unmodifiableSet(new HashSet<>(mDefaultRouters)), + Collections.unmodifiableSet(new HashSet<>(mDnsServers)), + Collections.unmodifiableSet(excl), + mDhcpLeaseTimeSecs, mLinkMtu, mMetered); + } + } + + /** + * Utility method to create an IpPrefix with the address and prefix length of a LinkAddress. + */ + @NonNull + static IpPrefix makeIpPrefix(@NonNull LinkAddress addr) { + return new IpPrefix(addr.getAddress(), addr.getPrefixLength()); + } +} diff --git a/packages/NetworkStack/src/android/net/util/SharedLog.java b/packages/NetworkStack/src/android/net/util/SharedLog.java new file mode 100644 index 000000000000..74bc1470293f --- /dev/null +++ b/packages/NetworkStack/src/android/net/util/SharedLog.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 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.util; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; +import android.util.LocalLog; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.StringJoiner; + + +/** + * Class to centralize logging functionality for tethering. + * + * All access to class methods other than dump() must be on the same thread. + * + * @hide + */ +public class SharedLog { + private static final int DEFAULT_MAX_RECORDS = 500; + private static final String COMPONENT_DELIMITER = "."; + + private enum Category { + NONE, + ERROR, + MARK, + WARN, + }; + + private final LocalLog mLocalLog; + // The tag to use for output to the system log. This is not output to the + // LocalLog because that would be redundant. + private final String mTag; + // The component (or subcomponent) of a system that is sharing this log. + // This can grow in depth if components call forSubComponent() to obtain + // their SharedLog instance. The tag is not included in the component for + // brevity. + private final String mComponent; + + public SharedLog(String tag) { + this(DEFAULT_MAX_RECORDS, tag); + } + + public SharedLog(int maxRecords, String tag) { + this(new LocalLog(maxRecords), tag, tag); + } + + private SharedLog(LocalLog localLog, String tag, String component) { + mLocalLog = localLog; + mTag = tag; + mComponent = component; + } + + /** + * Create a SharedLog based on this log with an additional component prefix on each logged line. + */ + public SharedLog forSubComponent(String component) { + if (!isRootLogInstance()) { + component = mComponent + COMPONENT_DELIMITER + component; + } + return new SharedLog(mLocalLog, mTag, component); + } + + /** + * Dump the contents of this log. + * + *

This method may be called on any thread. + */ + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mLocalLog.readOnlyLocalLog().dump(fd, writer, args); + } + + ////// + // Methods that both log an entry and emit it to the system log. + ////// + + /** + * Log an error due to an exception. This does not include the exception stacktrace. + * + *

The log entry will be also added to the system log. + * @see #e(String, Throwable) + */ + public void e(Exception e) { + Log.e(mTag, record(Category.ERROR, e.toString())); + } + + /** + * Log an error message. + * + *

The log entry will be also added to the system log. + */ + public void e(String msg) { + Log.e(mTag, record(Category.ERROR, msg)); + } + + /** + * Log an error due to an exception, with the exception stacktrace if provided. + * + *

The error and exception message appear in the shared log, but the stacktrace is only + * logged in general log output (logcat). The log entry will be also added to the system log. + */ + public void e(@NonNull String msg, @Nullable Throwable exception) { + if (exception == null) { + e(msg); + return; + } + Log.e(mTag, record(Category.ERROR, msg + ": " + exception.getMessage()), exception); + } + + /** + * Log an informational message. + * + *

The log entry will be also added to the system log. + */ + public void i(String msg) { + Log.i(mTag, record(Category.NONE, msg)); + } + + /** + * Log a warning message. + * + *

The log entry will be also added to the system log. + */ + public void w(String msg) { + Log.w(mTag, record(Category.WARN, msg)); + } + + ////// + // Methods that only log an entry (and do NOT emit to the system log). + ////// + + /** + * Log a general message to be only included in the in-memory log. + * + *

The log entry will *not* be added to the system log. + */ + public void log(String msg) { + record(Category.NONE, msg); + } + + /** + * Log a general, formatted message to be only included in the in-memory log. + * + *

The log entry will *not* be added to the system log. + * @see String#format(String, Object...) + */ + public void logf(String fmt, Object... args) { + log(String.format(fmt, args)); + } + + /** + * Log a message with MARK level. + * + *

The log entry will *not* be added to the system log. + */ + public void mark(String msg) { + record(Category.MARK, msg); + } + + private String record(Category category, String msg) { + final String entry = logLine(category, msg); + mLocalLog.log(entry); + return entry; + } + + private String logLine(Category category, String msg) { + final StringJoiner sj = new StringJoiner(" "); + if (!isRootLogInstance()) sj.add("[" + mComponent + "]"); + if (category != Category.NONE) sj.add(category.toString()); + return sj.add(msg).toString(); + } + + // Check whether this SharedLog instance is nominally the top level in + // a potential hierarchy of shared logs (the root of a tree), + // or is a subcomponent within the hierarchy. + private boolean isRootLogInstance() { + return TextUtils.isEmpty(mComponent) || mComponent.equals(mTag); + } +} diff --git a/packages/NetworkStack/src/com/android/server/NetworkStackService.java b/packages/NetworkStack/src/com/android/server/NetworkStackService.java index 5afaf586f74d..7fea1e038cee 100644 --- a/packages/NetworkStack/src/com/android/server/NetworkStackService.java +++ b/packages/NetworkStack/src/com/android/server/NetworkStackService.java @@ -16,15 +16,24 @@ package com.android.server; -import static android.os.Binder.getCallingUid; +import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; +import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR; + +import static com.android.server.util.PermissionUtil.checkNetworkStackCallingPermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Service; import android.content.Intent; import android.net.INetworkStackConnector; +import android.net.dhcp.DhcpServer; +import android.net.dhcp.DhcpServingParams; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpServerCallbacks; +import android.net.util.SharedLog; import android.os.IBinder; -import android.os.Process; +import android.os.RemoteException; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -54,21 +63,37 @@ public class NetworkStackService extends Service { } private static class NetworkStackConnector extends INetworkStackConnector.Stub { - // TODO: makeDhcpServer(), etc. will go here. + @NonNull + private final SharedLog mLog = new SharedLog(TAG); + + @Override + public void makeDhcpServer(@NonNull String ifName, @NonNull DhcpServingParamsParcel params, + @NonNull IDhcpServerCallbacks cb) throws RemoteException { + checkNetworkStackCallingPermission(); + final DhcpServer server; + try { + server = new DhcpServer( + ifName, + DhcpServingParams.fromParcelableObject(params), + mLog.forSubComponent(ifName + ".DHCP")); + } catch (DhcpServingParams.InvalidParameterException e) { + mLog.e("Invalid DhcpServingParams", e); + cb.onDhcpServerCreated(STATUS_INVALID_ARGUMENT, null); + return; + } catch (Exception e) { + mLog.e("Unknown error starting DhcpServer", e); + cb.onDhcpServerCreated(STATUS_UNKNOWN_ERROR, null); + return; + } + cb.onDhcpServerCreated(STATUS_SUCCESS, server); + } @Override protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) { - checkCaller(); + checkNetworkStackCallingPermission(); fout.println("NetworkStack logs:"); - // TODO: dump logs here - } - } - - private static void checkCaller() { - // TODO: check that the calling PID is the system server. - if (getCallingUid() != Process.SYSTEM_UID && getCallingUid() != Process.ROOT_UID) { - throw new SecurityException("Invalid caller: " + getCallingUid()); + mLog.dump(fd, fout, args); } } } diff --git a/packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java b/packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java new file mode 100644 index 000000000000..bb5900c53e52 --- /dev/null +++ b/packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java @@ -0,0 +1,45 @@ +/* + * 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 com.android.server.util; + +/** + * Network constants used by the network stack. + */ +public final class NetworkStackConstants { + + /** + * IPv4 constants. + * + * See also: + * - https://tools.ietf.org/html/rfc791 + */ + public static final int IPV4_ADDR_BITS = 32; + public static final int IPV4_MIN_MTU = 68; + public static final int IPV4_MAX_MTU = 65_535; + + /** + * DHCP constants. + * + * See also: + * - https://tools.ietf.org/html/rfc2131 + */ + public static final int INFINITE_LEASE = 0xffffffff; + + private NetworkStackConstants() { + throw new UnsupportedOperationException("This class is not to be instantiated"); + } +} diff --git a/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java new file mode 100644 index 000000000000..733f87393c32 --- /dev/null +++ b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java @@ -0,0 +1,42 @@ +/* + * 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 com.android.server.util; + +import static android.os.Binder.getCallingUid; + +import android.os.Process; + +/** + * Utility class to check calling permissions on the network stack. + */ +public final class PermissionUtil { + + /** + * Check that the caller is allowed to communicate with the network stack. + * @throws SecurityException The caller is not allowed to communicate with the network stack. + */ + public static void checkNetworkStackCallingPermission() { + // TODO: check that the calling PID is the system server. + if (getCallingUid() != Process.SYSTEM_UID && getCallingUid() != Process.ROOT_UID) { + throw new SecurityException("Invalid caller: " + getCallingUid()); + } + } + + private PermissionUtil() { + throw new UnsupportedOperationException("This class is not to be instantiated"); + } +} diff --git a/packages/NetworkStack/tests/Android.bp b/packages/NetworkStack/tests/Android.bp new file mode 100644 index 000000000000..bd7ff2a75703 --- /dev/null +++ b/packages/NetworkStack/tests/Android.bp @@ -0,0 +1,35 @@ +// +// 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. +// + +android_test { + name: "NetworkStackTests", + srcs: ["src/**/*.java"], + static_libs: [ + "android-support-test", + "mockito-target-extended-minus-junit4", + "NetworkStackLib", + "testables", + ], + libs: [ + "android.test.runner", + "android.test.base", + ], + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ] +} \ No newline at end of file diff --git a/packages/NetworkStack/tests/AndroidManifest.xml b/packages/NetworkStack/tests/AndroidManifest.xml new file mode 100644 index 000000000000..8b8474f57e28 --- /dev/null +++ b/packages/NetworkStack/tests/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/NetworkStack/tests/AndroidTest.xml b/packages/NetworkStack/tests/AndroidTest.xml new file mode 100644 index 000000000000..6b08b57731b7 --- /dev/null +++ b/packages/NetworkStack/tests/AndroidTest.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java new file mode 100644 index 000000000000..51d50d9eb13a --- /dev/null +++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java @@ -0,0 +1,539 @@ +/* + * 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.InetAddresses.parseNumericAddress; +import static android.net.dhcp.DhcpLease.HOSTNAME_NONE; +import static android.net.dhcp.DhcpLeaseRepository.CLIENTID_UNSPEC; +import static android.net.dhcp.DhcpLeaseRepository.INETADDR_UNSPEC; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +import static java.lang.String.format; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.IpPrefix; +import android.net.MacAddress; +import android.net.dhcp.DhcpServer.Clock; +import android.net.util.SharedLog; +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 org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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 DhcpLeaseRepositoryTest { + private static final Inet4Address INET4_ANY = (Inet4Address) Inet4Address.ANY; + private static final Inet4Address TEST_DEF_ROUTER = parseAddr4("192.168.42.247"); + private static final Inet4Address TEST_SERVER_ADDR = parseAddr4("192.168.42.241"); + private static final Inet4Address TEST_RESERVED_ADDR = parseAddr4("192.168.42.243"); + private static final MacAddress TEST_MAC_1 = MacAddress.fromBytes( + new byte[] { 5, 4, 3, 2, 1, 0 }); + private static final MacAddress TEST_MAC_2 = MacAddress.fromBytes( + new byte[] { 0, 1, 2, 3, 4, 5 }); + private static final MacAddress TEST_MAC_3 = MacAddress.fromBytes( + new byte[] { 0, 1, 2, 3, 4, 6 }); + private static final Inet4Address TEST_INETADDR_1 = parseAddr4("192.168.42.248"); + private static final Inet4Address TEST_INETADDR_2 = parseAddr4("192.168.42.249"); + private static final String TEST_HOSTNAME_1 = "hostname1"; + private static final String TEST_HOSTNAME_2 = "hostname2"; + private static final IpPrefix TEST_IP_PREFIX = new IpPrefix(TEST_SERVER_ADDR, 22); + private static final long TEST_TIME = 100L; + private static final int TEST_LEASE_TIME_MS = 3_600_000; + private static final Set TEST_EXCL_SET = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + TEST_SERVER_ADDR, TEST_DEF_ROUTER, TEST_RESERVED_ADDR))); + + @NonNull + private SharedLog mLog; + @NonNull @Mock + private Clock mClock; + @NonNull + private DhcpLeaseRepository mRepo; + + private static Inet4Address parseAddr4(String inet4Addr) { + return (Inet4Address) parseNumericAddress(inet4Addr); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLog = new SharedLog("DhcpLeaseRepositoryTest"); + when(mClock.elapsedRealtime()).thenReturn(TEST_TIME); + mRepo = new DhcpLeaseRepository( + TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, mLog, mClock); + } + + /** + * Request a number of addresses through offer/request. Useful to test address exhaustion. + * @param nAddr Number of addresses to request. + */ + private void requestAddresses(byte nAddr) throws Exception { + final HashSet addrs = new HashSet<>(); + byte[] hwAddrBytes = new byte[] { 8, 4, 3, 2, 1, 0 }; + for (byte i = 0; i < nAddr; i++) { + hwAddrBytes[5] = i; + MacAddress newMac = MacAddress.fromBytes(hwAddrBytes); + final String hostname = "host_" + i; + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, newMac, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, hostname); + + assertNotNull(lease); + assertEquals(newMac, lease.getHwAddr()); + assertEquals(hostname, lease.getHostname()); + assertTrue(format("Duplicate address allocated: %s in %s", lease.getNetAddr(), addrs), + addrs.add(lease.getNetAddr())); + + requestLeaseSelecting(newMac, lease.getNetAddr(), hostname); + } + } + + @Test + public void testAddressExhaustion() throws Exception { + // Use a /28 to quickly run out of addresses + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses + requestAddresses((byte) 11); + + try { + mRepo.getOffer(null, TEST_MAC_2, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + fail("Should be out of addresses"); + } catch (DhcpLeaseRepository.OutOfAddressesException e) { + // Expected + } + } + + @Test + public void testUpdateParams_LeaseCleanup() throws Exception { + // Inside /28: + final Inet4Address reqAddrIn28 = parseAddr4("192.168.42.242"); + final Inet4Address declinedAddrIn28 = parseAddr4("192.168.42.245"); + + // Inside /28, but not available there (first address of the range) + final Inet4Address declinedFirstAddrIn28 = parseAddr4("192.168.42.240"); + + final DhcpLease reqAddrIn28Lease = requestLeaseSelecting(TEST_MAC_1, reqAddrIn28); + mRepo.markLeaseDeclined(declinedAddrIn28); + mRepo.markLeaseDeclined(declinedFirstAddrIn28); + + // Inside /22, but outside /28: + final Inet4Address reqAddrIn22 = parseAddr4("192.168.42.3"); + final Inet4Address declinedAddrIn22 = parseAddr4("192.168.42.4"); + + final DhcpLease reqAddrIn22Lease = requestLeaseSelecting(TEST_MAC_3, reqAddrIn22); + mRepo.markLeaseDeclined(declinedAddrIn22); + + // Address that will be reserved in the updateParams call below + final Inet4Address reservedAddr = parseAddr4("192.168.42.244"); + final DhcpLease reservedAddrLease = requestLeaseSelecting(TEST_MAC_2, reservedAddr); + + // Update from /22 to /28 and add another reserved address + Set newReserved = new HashSet<>(TEST_EXCL_SET); + newReserved.add(reservedAddr); + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), newReserved, TEST_LEASE_TIME_MS); + + assertHasLease(reqAddrIn28Lease); + assertDeclined(declinedAddrIn28); + + assertNotDeclined(declinedFirstAddrIn28); + + assertNoLease(reqAddrIn22Lease); + assertNotDeclined(declinedAddrIn22); + + assertNoLease(reservedAddrLease); + } + + @Test + public void testGetOffer_StableAddress() throws Exception { + for (final MacAddress macAddr : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + // Same lease is offered twice + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(lease, newLease); + } + } + + @Test + public void testUpdateParams_UsesNewPrefix() throws Exception { + final IpPrefix newPrefix = new IpPrefix(parseAddr4("192.168.123.0"), 24); + mRepo.updateParams(newPrefix, TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertTrue(newPrefix.contains(lease.getNetAddr())); + } + + @Test + public void testGetOffer_ExistingLease() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, TEST_HOSTNAME_1); + + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_ClientIdHasExistingLease() throws Exception { + final byte[] clientId = new byte[] { 1, 2 }; + mRepo.requestLease(clientId, TEST_MAC_1, INET4_ANY /* clientAddr */, + INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1); + + // Different MAC, but same clientId + DhcpLease offer = mRepo.getOffer(clientId, TEST_MAC_2, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_DifferentClientId() throws Exception { + final byte[] clientId1 = new byte[] { 1, 2 }; + final byte[] clientId2 = new byte[] { 3, 4 }; + mRepo.requestLease(clientId1, TEST_MAC_1, INET4_ANY /* clientAddr */, + INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1); + + // Same MAC, different client ID + DhcpLease offer = mRepo.getOffer(clientId2, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + // Obtains a different address + assertNotEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(HOSTNAME_NONE, offer.getHostname()); + assertEquals(TEST_MAC_1, offer.getHwAddr()); + } + + @Test + public void testGetOffer_RequestedAddress() throws Exception { + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, + TEST_INETADDR_1 /* reqAddr */, TEST_HOSTNAME_1); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_RequestedAddressInUse() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY /* relayAddr */, + TEST_INETADDR_1 /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(TEST_INETADDR_1, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressReserved() throws Exception { + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, + TEST_RESERVED_ADDR /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(TEST_RESERVED_ADDR, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressInvalid() throws Exception { + final Inet4Address invalidAddr = parseAddr4("192.168.42.0"); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, + invalidAddr /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(invalidAddr, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressOutsideSubnet() throws Exception { + final Inet4Address invalidAddr = parseAddr4("192.168.254.2"); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, + invalidAddr /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(invalidAddr, offer.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class) + public void testGetOffer_RelayInInvalidSubnet() throws Exception { + mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* relayAddr */, + INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_SelectingTwice() throws Exception { + final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, + TEST_HOSTNAME_1); + + // Second request from same client for a different address + final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_2, + TEST_HOSTNAME_2); + + assertEquals(TEST_INETADDR_1, lease1.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, lease1.getHostname()); + + assertEquals(TEST_INETADDR_2, lease2.getNetAddr()); + assertEquals(TEST_HOSTNAME_2, lease2.getHostname()); + + // First address freed when client requested a different one: another client can request it + final DhcpLease lease3 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, lease3.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingInvalid() throws Exception { + requestLeaseSelecting(TEST_MAC_1, parseAddr4("192.168.254.5")); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingInUse() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingReserved() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_RESERVED_ADDR); + } + + @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class) + public void testRequestLease_SelectingRelayInInvalidSubnet() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* clientAddr */, + parseAddr4("192.168.128.1") /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, + true /* sidSet */, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_InitReboot() throws Exception { + // Request address once + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + + // init-reboot (sidSet == false): verify configuration + final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_1); + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_InitRebootWrongAddr() throws Exception { + // Request address once + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + // init-reboot with different requested address + requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2); + } + + @Test + public void testRequestLease_InitRebootUnknownAddr() throws Exception { + // init-reboot with unknown requested address + final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2); + // RFC2131 says we should not reply to accommodate other servers, but since we are + // authoritative we allow creating the lease to avoid issues with lost lease DB (same as + // dnsmasq behavior) + assertEquals(TEST_INETADDR_2, lease.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_InitRebootWrongSubnet() throws Exception { + requestLeaseInitReboot(TEST_MAC_1, parseAddr4("192.168.254.2")); + } + + @Test + public void testRequestLease_Renewing() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + + final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); + + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test + public void testRequestLease_RenewingUnknownAddr() throws Exception { + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); + // Allows renewing an unknown address if available + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_RenewingAddrInUse() throws Exception { + requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); + requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_RenewingInvalidAddr() throws Exception { + requestLeaseRenewing(TEST_MAC_1, parseAddr4("192.168.254.2")); + } + + @Test + public void testReleaseLease() throws Exception { + final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + + assertHasLease(lease1); + assertTrue(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); + assertNoLease(lease1); + + final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); + assertEquals(TEST_INETADDR_1, lease2.getNetAddr()); + } + + @Test + public void testReleaseLease_UnknownLease() { + assertFalse(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); + } + + @Test + public void testReleaseLease_StableOffer() throws Exception { + for (MacAddress mac : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, mac, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + requestLeaseSelecting(mac, lease.getNetAddr()); + mRepo.releaseLease(CLIENTID_UNSPEC, mac, lease.getNetAddr()); + + // Same lease is offered after it was released + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, mac, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(lease.getNetAddr(), newLease.getNetAddr()); + } + } + + @Test + public void testMarkLeaseDeclined() throws Exception { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + mRepo.markLeaseDeclined(lease.getNetAddr()); + + // Same lease is not offered again + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(lease.getNetAddr(), newLease.getNetAddr()); + } + + @Test + public void testMarkLeaseDeclined_UsedIfOutOfAddresses() throws Exception { + // Use a /28 to quickly run out of addresses + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + mRepo.markLeaseDeclined(TEST_INETADDR_1); + mRepo.markLeaseDeclined(TEST_INETADDR_2); + + // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses + requestAddresses((byte) 9); + + // Last 2 addresses: addresses marked declined should be used + final DhcpLease firstLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_1); + requestLeaseSelecting(TEST_MAC_1, firstLease.getNetAddr()); + + final DhcpLease secondLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_2); + requestLeaseSelecting(TEST_MAC_2, secondLease.getNetAddr()); + + // Now out of addresses + try { + mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_3, INET4_ANY /* relayAddr */, + INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + fail("Repository should be out of addresses and throw"); + } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ } + + assertEquals(TEST_INETADDR_1, firstLease.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, firstLease.getHostname()); + assertEquals(TEST_INETADDR_2, secondLease.getNetAddr()); + assertEquals(TEST_HOSTNAME_2, secondLease.getHostname()); + } + + private DhcpLease requestLease(@NonNull MacAddress macAddr, @NonNull Inet4Address clientAddr, + @Nullable Inet4Address reqAddr, @Nullable String hostname, boolean sidSet) + throws DhcpLeaseRepository.DhcpLeaseException { + return mRepo.requestLease(CLIENTID_UNSPEC, macAddr, clientAddr, INET4_ANY /* relayAddr */, + reqAddr, sidSet, hostname); + } + + /** + * Request a lease simulating a client in the SELECTING state. + */ + private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr, + @NonNull Inet4Address reqAddr, @Nullable String hostname) + throws DhcpLeaseRepository.DhcpLeaseException { + return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, hostname, + true /* sidSet */); + } + + /** + * Request a lease simulating a client in the SELECTING state. + */ + private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr, + @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException { + return requestLeaseSelecting(macAddr, reqAddr, HOSTNAME_NONE); + } + + /** + * Request a lease simulating a client in the INIT-REBOOT state. + */ + private DhcpLease requestLeaseInitReboot(@NonNull MacAddress macAddr, + @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException { + return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, HOSTNAME_NONE, + false /* sidSet */); + } + + /** + * Request a lease simulating a client in the RENEWING state. + */ + private DhcpLease requestLeaseRenewing(@NonNull MacAddress macAddr, + @NonNull Inet4Address clientAddr) throws DhcpLeaseRepository.DhcpLeaseException { + // Renewing: clientAddr filled in, no reqAddr + return requestLease(macAddr, clientAddr, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE, + true /* sidSet */); + } + + private void assertNoLease(DhcpLease lease) { + assertFalse("Leases contain " + lease, mRepo.getCommittedLeases().contains(lease)); + } + + private void assertHasLease(DhcpLease lease) { + assertTrue("Leases do not contain " + lease, mRepo.getCommittedLeases().contains(lease)); + } + + private void assertNotDeclined(Inet4Address addr) { + assertFalse("Address is declined: " + addr, mRepo.getDeclinedAddresses().contains(addr)); + } + + private void assertDeclined(Inet4Address addr) { + assertTrue("Address is not declined: " + addr, mRepo.getDeclinedAddresses().contains(addr)); + } +} diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java new file mode 100644 index 000000000000..d4c1e2e16731 --- /dev/null +++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java @@ -0,0 +1,327 @@ +/* + * 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.InetAddresses.parseNumericAddress; +import static android.net.dhcp.DhcpPacket.DHCP_CLIENT; +import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME; +import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP; +import static android.net.dhcp.DhcpPacket.INADDR_ANY; +import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.INetworkStackStatusCallback; +import android.net.LinkAddress; +import android.net.MacAddress; +import android.net.dhcp.DhcpLeaseRepository.InvalidAddressException; +import android.net.dhcp.DhcpLeaseRepository.OutOfAddressesException; +import android.net.dhcp.DhcpServer.Clock; +import android.net.dhcp.DhcpServer.Dependencies; +import android.net.util.SharedLog; +import android.os.HandlerThread; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.Inet4Address; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +@RunWithLooper +public class DhcpServerTest { + private static final String TEST_IFACE = "testiface"; + + private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2"); + private static final LinkAddress TEST_SERVER_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20); + private static final Set TEST_DEFAULT_ROUTERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124"))); + private static final Set TEST_DNS_SERVERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127"))); + private static final Set TEST_EXCLUDED_ADDRS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201"))); + private static final long TEST_LEASE_TIME_SECS = 3600L; + private static final int TEST_MTU = 1500; + private static final String TEST_HOSTNAME = "testhostname"; + + private static final int TEST_TRANSACTION_ID = 123; + private static final byte[] TEST_CLIENT_MAC_BYTES = new byte [] { 1, 2, 3, 4, 5, 6 }; + private static final MacAddress TEST_CLIENT_MAC = MacAddress.fromBytes(TEST_CLIENT_MAC_BYTES); + private static final Inet4Address TEST_CLIENT_ADDR = parseAddr("192.168.0.42"); + + private static final long TEST_CLOCK_TIME = 1234L; + private static final int TEST_LEASE_EXPTIME_SECS = 3600; + private static final DhcpLease TEST_LEASE = new DhcpLease(null, TEST_CLIENT_MAC, + TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME, + null /* hostname */); + private static final DhcpLease TEST_LEASE_WITH_HOSTNAME = new DhcpLease(null, TEST_CLIENT_MAC, + TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME, TEST_HOSTNAME); + + @NonNull @Mock + private Dependencies mDeps; + @NonNull @Mock + private DhcpLeaseRepository mRepository; + @NonNull @Mock + private Clock mClock; + @NonNull @Mock + private DhcpPacketListener mPacketListener; + + @NonNull @Captor + private ArgumentCaptor mSentPacketCaptor; + @NonNull @Captor + private ArgumentCaptor mResponseDstAddrCaptor; + + @NonNull + private HandlerThread mHandlerThread; + @NonNull + private TestableLooper mLooper; + @NonNull + private DhcpServer mServer; + + @Nullable + private String mPrevShareClassloaderProp; + + private final INetworkStackStatusCallback mAssertSuccessCallback = + new INetworkStackStatusCallback.Stub() { + @Override + public void onStatusAvailable(int statusCode) { + assertEquals(STATUS_SUCCESS, statusCode); + } + }; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mDeps.makeLeaseRepository(any(), any(), any())).thenReturn(mRepository); + when(mDeps.makeClock()).thenReturn(mClock); + when(mDeps.makePacketListener()).thenReturn(mPacketListener); + doNothing().when(mDeps) + .sendPacket(any(), mSentPacketCaptor.capture(), mResponseDstAddrCaptor.capture()); + when(mClock.elapsedRealtime()).thenReturn(TEST_CLOCK_TIME); + + final DhcpServingParams servingParams = new DhcpServingParams.Builder() + .setDefaultRouters(TEST_DEFAULT_ROUTERS) + .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS) + .setDnsServers(TEST_DNS_SERVERS) + .setServerAddr(TEST_SERVER_LINKADDR) + .setLinkMtu(TEST_MTU) + .setExcludedAddrs(TEST_EXCLUDED_ADDRS) + .build(); + + mLooper = TestableLooper.get(this); + mHandlerThread = spy(new HandlerThread("TestDhcpServer")); + when(mHandlerThread.getLooper()).thenReturn(mLooper.getLooper()); + mServer = new DhcpServer(mHandlerThread, TEST_IFACE, servingParams, + new SharedLog(DhcpServerTest.class.getSimpleName()), mDeps); + + mServer.start(mAssertSuccessCallback); + mLooper.processAllMessages(); + } + + @After + public void tearDown() throws Exception { + mServer.stop(mAssertSuccessCallback); + mLooper.processMessages(1); + verify(mPacketListener, times(1)).stop(); + verify(mHandlerThread, times(1)).quitSafely(); + } + + @Test + public void testStart() throws Exception { + verify(mPacketListener, times(1)).start(); + } + + @Test + public void testDiscover() throws Exception { + // TODO: refactor packet construction to eliminate unnecessary/confusing/duplicate fields + when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC), + eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */)) + .thenReturn(TEST_LEASE); + + final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID, + (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES, + false /* broadcast */, INADDR_ANY /* srcIp */); + mServer.processPacket(discover, DHCP_CLIENT); + + assertResponseSentTo(TEST_CLIENT_ADDR); + final DhcpOfferPacket packet = assertOffer(getPacket()); + assertMatchesTestLease(packet); + } + + @Test + public void testDiscover_OutOfAddresses() throws Exception { + when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC), + eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */)) + .thenThrow(new OutOfAddressesException("Test exception")); + + final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID, + (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES, + false /* broadcast */, INADDR_ANY /* srcIp */); + mServer.processPacket(discover, DHCP_CLIENT); + + assertResponseSentTo(INADDR_BROADCAST); + final DhcpNakPacket packet = assertNak(getPacket()); + assertMatchesClient(packet); + } + + private DhcpRequestPacket makeRequestSelectingPacket() { + final DhcpRequestPacket request = new DhcpRequestPacket(TEST_TRANSACTION_ID, + (short) 0 /* secs */, INADDR_ANY /* clientIp */, INADDR_ANY /* relayIp */, + TEST_CLIENT_MAC_BYTES, false /* broadcast */); + request.mServerIdentifier = TEST_SERVER_ADDR; + request.mRequestedIp = TEST_CLIENT_ADDR; + return request; + } + + @Test + public void testRequest_Selecting_Ack() throws Exception { + when(mRepository.requestLease(isNull() /* clientId */, eq(TEST_CLIENT_MAC), + eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */, + eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, eq(TEST_HOSTNAME))) + .thenReturn(TEST_LEASE_WITH_HOSTNAME); + + final DhcpRequestPacket request = makeRequestSelectingPacket(); + request.mHostName = TEST_HOSTNAME; + request.mRequestedParams = new byte[] { DHCP_HOST_NAME }; + mServer.processPacket(request, DHCP_CLIENT); + + assertResponseSentTo(TEST_CLIENT_ADDR); + final DhcpAckPacket packet = assertAck(getPacket()); + assertMatchesTestLease(packet, TEST_HOSTNAME); + } + + @Test + public void testRequest_Selecting_Nak() throws Exception { + when(mRepository.requestLease(isNull(), eq(TEST_CLIENT_MAC), + eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */, + eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, isNull() /* hostname */)) + .thenThrow(new InvalidAddressException("Test error")); + + final DhcpRequestPacket request = makeRequestSelectingPacket(); + mServer.processPacket(request, DHCP_CLIENT); + + assertResponseSentTo(INADDR_BROADCAST); + final DhcpNakPacket packet = assertNak(getPacket()); + assertMatchesClient(packet); + } + + @Test + public void testRequest_Selecting_WrongClientPort() throws Exception { + final DhcpRequestPacket request = makeRequestSelectingPacket(); + mServer.processPacket(request, 50000); + + verify(mRepository, never()) + .requestLease(any(), any(), any(), any(), any(), anyBoolean(), any()); + verify(mDeps, never()).sendPacket(any(), any(), any()); + } + + @Test + public void testRelease() throws Exception { + final DhcpReleasePacket release = new DhcpReleasePacket(TEST_TRANSACTION_ID, + TEST_SERVER_ADDR, TEST_CLIENT_ADDR, + INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES); + mServer.processPacket(release, DHCP_CLIENT); + + verify(mRepository, times(1)) + .releaseLease(isNull(), eq(TEST_CLIENT_MAC), eq(TEST_CLIENT_ADDR)); + } + + /* TODO: add more tests once packet construction is refactored, including: + * - usage of giaddr + * - usage of broadcast bit + * - other request states (init-reboot/renewing/rebinding) + */ + + private void assertMatchesTestLease(@NonNull DhcpPacket packet, @Nullable String hostname) { + assertMatchesClient(packet); + assertFalse(packet.hasExplicitClientId()); + assertEquals(TEST_SERVER_ADDR, packet.mServerIdentifier); + assertEquals(TEST_CLIENT_ADDR, packet.mYourIp); + assertNotNull(packet.mLeaseTime); + assertEquals(TEST_LEASE_EXPTIME_SECS, (int) packet.mLeaseTime); + assertEquals(hostname, packet.mHostName); + } + + private void assertMatchesTestLease(@NonNull DhcpPacket packet) { + assertMatchesTestLease(packet, null); + } + + private void assertMatchesClient(@NonNull DhcpPacket packet) { + assertEquals(TEST_TRANSACTION_ID, packet.mTransId); + assertEquals(TEST_CLIENT_MAC, MacAddress.fromBytes(packet.mClientMac)); + } + + private void assertResponseSentTo(@NonNull Inet4Address addr) { + assertEquals(addr, mResponseDstAddrCaptor.getValue()); + } + + private static DhcpNakPacket assertNak(@Nullable DhcpPacket packet) { + assertTrue(packet instanceof DhcpNakPacket); + return (DhcpNakPacket) packet; + } + + private static DhcpAckPacket assertAck(@Nullable DhcpPacket packet) { + assertTrue(packet instanceof DhcpAckPacket); + return (DhcpAckPacket) packet; + } + + private static DhcpOfferPacket assertOffer(@Nullable DhcpPacket packet) { + assertTrue(packet instanceof DhcpOfferPacket); + return (DhcpOfferPacket) packet; + } + + private DhcpPacket getPacket() throws Exception { + verify(mDeps, times(1)).sendPacket(any(), any(), any()); + return DhcpPacket.decodeFullPacket(mSentPacketCaptor.getValue(), ENCAP_BOOTP); + } + + private static Inet4Address parseAddr(@Nullable String inet4Addr) { + return (Inet4Address) parseNumericAddress(inet4Addr); + } +} diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java new file mode 100644 index 000000000000..3ca0564f24d6 --- /dev/null +++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java @@ -0,0 +1,220 @@ +/* + * 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.InetAddresses.parseNumericAddress; +import static android.net.NetworkUtils.inet4AddressToIntHTH; +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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.LinkAddress; +import android.net.NetworkUtils; +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.lang.reflect.Modifier; +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Collection; +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"))); + private static final boolean TEST_METERED = true; + + @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) + .setMetered(TEST_METERED); + } + + @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); + assertEquals(TEST_METERED, params.metered); + + 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(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(parseAddr("192.168.254.254")).build(); + } + + @Test + public void testFromParcelableObject() throws InvalidParameterException { + final DhcpServingParams params = mBuilder.build(); + final DhcpServingParamsParcel parcel = new DhcpServingParamsParcel(); + parcel.defaultRouters = toIntArray(TEST_DEFAULT_ROUTERS); + parcel.dhcpLeaseTimeSecs = TEST_LEASE_TIME_SECS; + parcel.dnsServers = toIntArray(TEST_DNS_SERVERS); + parcel.serverAddr = inet4AddressToIntHTH(TEST_SERVER_ADDR); + parcel.serverAddrPrefixLength = TEST_LINKADDR.getPrefixLength(); + parcel.linkMtu = TEST_MTU; + parcel.excludedAddrs = toIntArray(TEST_EXCLUDED_ADDRS); + parcel.metered = TEST_METERED; + final DhcpServingParams parceled = DhcpServingParams.fromParcelableObject(parcel); + + assertEquals(params.defaultRouters, parceled.defaultRouters); + assertEquals(params.dhcpLeaseTimeSecs, parceled.dhcpLeaseTimeSecs); + assertEquals(params.dnsServers, parceled.dnsServers); + assertEquals(params.serverAddr, parceled.serverAddr); + assertEquals(params.linkMtu, parceled.linkMtu); + assertEquals(params.excludedAddrs, parceled.excludedAddrs); + assertEquals(params.metered, parceled.metered); + + // Ensure that we do not miss any field if added in the future + final long numFields = Arrays.stream(DhcpServingParams.class.getDeclaredFields()) + .filter(f -> !Modifier.isStatic(f.getModifiers())) + .count(); + assertEquals(7, numFields); + } + + @Test(expected = InvalidParameterException.class) + public void testFromParcelableObject_NullArgument() throws InvalidParameterException { + DhcpServingParams.fromParcelableObject(null); + } + + private static int[] toIntArray(Collection addrs) { + return addrs.stream().mapToInt(NetworkUtils::inet4AddressToIntHTH).toArray(); + } + + 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); + } +} diff --git a/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java b/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java new file mode 100644 index 000000000000..07ad3123bc53 --- /dev/null +++ b/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 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 com.android.server.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.net.util.SharedLog; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SharedLogTest { + private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}"; + private static final String TIMESTAMP = "HH:MM:SS"; + + @Test + public void testBasicOperation() { + final SharedLog logTop = new SharedLog("top"); + logTop.mark("first post!"); + + final SharedLog logLevel2a = logTop.forSubComponent("twoA"); + final SharedLog logLevel2b = logTop.forSubComponent("twoB"); + logLevel2b.e("2b or not 2b"); + logLevel2b.e("No exception", null); + logLevel2b.e("Wait, here's one", new Exception("Test")); + logLevel2a.w("second post?"); + + final SharedLog logLevel3 = logLevel2a.forSubComponent("three"); + logTop.log("still logging"); + logLevel3.log("3 >> 2"); + logLevel2a.mark("ok: last post"); + + final String[] expected = { + " - MARK first post!", + " - [twoB] ERROR 2b or not 2b", + " - [twoB] ERROR No exception", + // No stacktrace in shared log, only in logcat + " - [twoB] ERROR Wait, here's one: Test", + " - [twoA] WARN second post?", + " - still logging", + " - [twoA.three] 3 >> 2", + " - [twoA] MARK ok: last post", + }; + // Verify the logs are all there and in the correct order. + verifyLogLines(expected, logTop); + + // In fact, because they all share the same underlying LocalLog, + // every subcomponent SharedLog's dump() is identical. + verifyLogLines(expected, logLevel2a); + verifyLogLines(expected, logLevel2b); + verifyLogLines(expected, logLevel3); + } + + private static void verifyLogLines(String[] expected, SharedLog log) { + final ByteArrayOutputStream ostream = new ByteArrayOutputStream(); + final PrintWriter pw = new PrintWriter(ostream, true); + log.dump(null, pw, null); + + final String dumpOutput = ostream.toString(); + assertTrue(dumpOutput != null); + assertTrue(!"".equals(dumpOutput)); + + final String[] lines = dumpOutput.split("\n"); + assertEquals(expected.length, lines.length); + + for (int i = 0; i < expected.length; i++) { + String got = lines[i]; + String want = expected[i]; + assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want)); + assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP), + got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP)); + } + } +} diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java index 9dfdddbea18a..eb5be77e4a33 100644 --- a/services/core/java/com/android/server/connectivity/Tethering.java +++ b/services/core/java/com/android/server/connectivity/Tethering.java @@ -1837,7 +1837,7 @@ public class Tethering extends BaseNetworkObserver { final TetherState tetherState = new TetherState( new IpServer(iface, mLooper, interfaceType, mLog, mNMService, mStatsService, makeControlCallback(), mConfig.enableLegacyDhcpServer, - mDeps.getIpServerDependencies())); + mDeps.getIpServerDependencies(mContext))); mTetherStates.put(iface, tetherState); tetherState.ipServer.start(); } diff --git a/services/core/java/com/android/server/connectivity/tethering/TetheringDependencies.java b/services/core/java/com/android/server/connectivity/tethering/TetheringDependencies.java index 7daf71dda73b..a42efe960ff9 100644 --- a/services/core/java/com/android/server/connectivity/tethering/TetheringDependencies.java +++ b/services/core/java/com/android/server/connectivity/tethering/TetheringDependencies.java @@ -60,8 +60,8 @@ public class TetheringDependencies { /** * Get dependencies to be used by IpServer. */ - public IpServer.Dependencies getIpServerDependencies() { - return new IpServer.Dependencies(); + public IpServer.Dependencies getIpServerDependencies(Context context) { + return new IpServer.Dependencies(context); } /** diff --git a/services/net/Android.bp b/services/net/Android.bp index e0ae68f20483..ae697b7f093a 100644 --- a/services/net/Android.bp +++ b/services/net/Android.bp @@ -2,3 +2,19 @@ java_library_static { name: "services.net", srcs: ["java/**/*.java"], } + +// TODO: move to networking module with DhcpClient and remove lib +java_library { + name: "dhcp-packet-lib", + srcs: [ + "java/android/net/dhcp/*Packet.java", + ] +} + +// TODO: move to networking module with IpNeighborMonitor/ConnectivityPacketTracker and remove lib +java_library { + name: "frameworks-net-shared-utils", + srcs: [ + "java/android/net/util/FdEventsReader.java", + ] +} \ No newline at end of file diff --git a/services/net/java/android/net/dhcp/DhcpLease.java b/services/net/java/android/net/dhcp/DhcpLease.java deleted file mode 100644 index 6849cfadc22a..000000000000 --- a/services/net/java/android/net/dhcp/DhcpLease.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 android.annotation.NonNull; -import android.annotation.Nullable; -import android.net.MacAddress; -import android.os.SystemClock; -import android.text.TextUtils; - -import com.android.internal.util.HexDump; - -import java.net.Inet4Address; -import java.util.Arrays; -import java.util.Objects; - -/** - * An IPv4 address assignment done through DHCPv4. - * @hide - */ -public class DhcpLease { - public static final long EXPIRATION_NEVER = Long.MAX_VALUE; - public static final String HOSTNAME_NONE = null; - - @Nullable - private final byte[] mClientId; - @NonNull - private final MacAddress mHwAddr; - @NonNull - private final Inet4Address mNetAddr; - /** - * Expiration time for the lease, to compare with {@link SystemClock#elapsedRealtime()}. - */ - private final long mExpTime; - @Nullable - private final String mHostname; - - public DhcpLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, - @NonNull Inet4Address netAddr, long expTime, @Nullable String hostname) { - mClientId = (clientId == null ? null : Arrays.copyOf(clientId, clientId.length)); - mHwAddr = hwAddr; - mNetAddr = netAddr; - mExpTime = expTime; - mHostname = hostname; - } - - /** - * Get the clientId associated with this lease, if any. - * - *

If the lease is not associated to a clientId, this returns null. - */ - @Nullable - public byte[] getClientId() { - if (mClientId == null) { - return null; - } - return Arrays.copyOf(mClientId, mClientId.length); - } - - @NonNull - public MacAddress getHwAddr() { - return mHwAddr; - } - - @Nullable - public String getHostname() { - return mHostname; - } - - @NonNull - public Inet4Address getNetAddr() { - return mNetAddr; - } - - public long getExpTime() { - return mExpTime; - } - - /** - * Push back the expiration time of this lease. If the provided time is sooner than the original - * expiration time, the lease time will not be updated. - * - *

The lease hostname is updated with the provided one if set. - * @return A {@link DhcpLease} with expiration time set to max(expTime, currentExpTime) - */ - public DhcpLease renewedLease(long expTime, @Nullable String hostname) { - return new DhcpLease(mClientId, mHwAddr, mNetAddr, Math.max(expTime, mExpTime), - (hostname == null ? mHostname : hostname)); - } - - /** - * Determine whether this lease matches a client with the specified parameters. - * @param clientId clientId of the client if any, or null otherwise. - * @param hwAddr Hardware address of the client. - */ - public boolean matchesClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) { - if (mClientId != null) { - return Arrays.equals(mClientId, clientId); - } else { - return clientId == null && mHwAddr.equals(hwAddr); - } - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof DhcpLease)) { - return false; - } - final DhcpLease other = (DhcpLease) obj; - return Arrays.equals(mClientId, other.mClientId) - && mHwAddr.equals(other.mHwAddr) - && mNetAddr.equals(other.mNetAddr) - && mExpTime == other.mExpTime - && TextUtils.equals(mHostname, other.mHostname); - } - - @Override - public int hashCode() { - return Objects.hash(mClientId, mHwAddr, mNetAddr, mHostname, mExpTime); - } - - static String clientIdToString(byte[] bytes) { - if (bytes == null) { - return "null"; - } - return HexDump.toHexString(bytes); - } - - static String inet4AddrToString(@Nullable Inet4Address addr) { - return (addr == null) ? "null" : addr.getHostAddress(); - } - - @Override - public String toString() { - return String.format("clientId: %s, hwAddr: %s, netAddr: %s, expTime: %d, hostname: %s", - clientIdToString(mClientId), mHwAddr.toString(), inet4AddrToString(mNetAddr), - mExpTime, mHostname); - } -} diff --git a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java deleted file mode 100644 index b3d0512ba447..000000000000 --- a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java +++ /dev/null @@ -1,544 +0,0 @@ -/* - * 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.inet4AddressToIntHTH; -import static android.net.NetworkUtils.intToInet4AddressHTH; -import static android.net.NetworkUtils.prefixLengthToV4NetmaskIntHTH; -import static android.net.dhcp.DhcpLease.EXPIRATION_NEVER; -import static android.net.dhcp.DhcpLease.inet4AddrToString; -import static android.net.util.NetworkConstants.IPV4_ADDR_BITS; - -import static java.lang.Math.min; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.net.IpPrefix; -import android.net.MacAddress; -import android.net.dhcp.DhcpServer.Clock; -import android.net.util.SharedLog; -import android.util.ArrayMap; - -import java.net.Inet4Address; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.function.Function; - -/** - * A repository managing IPv4 address assignments through DHCPv4. - * - *

This class is not thread-safe. All public methods should be called on a common thread or - * use some synchronization mechanism. - * - *

Methods are optimized for a small number of allocated leases, assuming that most of the time - * only 2~10 addresses will be allocated, which is the common case. Managing a large number of - * addresses is supported but will be slower: some operations have complexity in O(num_leases). - * @hide - */ -class DhcpLeaseRepository { - public static final byte[] CLIENTID_UNSPEC = null; - public static final Inet4Address INETADDR_UNSPEC = null; - - @NonNull - private final SharedLog mLog; - @NonNull - private final Clock mClock; - - @NonNull - private IpPrefix mPrefix; - @NonNull - private Set mReservedAddrs; - private int mSubnetAddr; - private int mSubnetMask; - private int mNumAddresses; - private long mLeaseTimeMs; - - /** - * Next timestamp when committed or declined leases should be checked for expired ones. This - * will always be lower than or equal to the time for the first lease to expire: it's OK not to - * update this when removing entries, but it must always be updated when adding/updating. - */ - private long mNextExpirationCheck = EXPIRATION_NEVER; - - static class DhcpLeaseException extends Exception { - DhcpLeaseException(String message) { - super(message); - } - } - - static class OutOfAddressesException extends DhcpLeaseException { - OutOfAddressesException(String message) { - super(message); - } - } - - static class InvalidAddressException extends DhcpLeaseException { - InvalidAddressException(String message) { - super(message); - } - } - - static class InvalidSubnetException extends DhcpLeaseException { - InvalidSubnetException(String message) { - super(message); - } - } - - /** - * Leases by IP address - */ - private final ArrayMap mCommittedLeases = new ArrayMap<>(); - - /** - * Map address -> expiration timestamp in ms. Addresses are guaranteed to be valid as defined - * by {@link #isValidAddress(Inet4Address)}, but are not necessarily otherwise available for - * assignment. - */ - private final LinkedHashMap mDeclinedAddrs = new LinkedHashMap<>(); - - DhcpLeaseRepository(@NonNull IpPrefix prefix, @NonNull Set reservedAddrs, - long leaseTimeMs, @NonNull SharedLog log, @NonNull Clock clock) { - updateParams(prefix, reservedAddrs, leaseTimeMs); - mLog = log; - mClock = clock; - } - - public void updateParams(@NonNull IpPrefix prefix, @NonNull Set reservedAddrs, - long leaseTimeMs) { - mPrefix = prefix; - mReservedAddrs = Collections.unmodifiableSet(new HashSet<>(reservedAddrs)); - mSubnetMask = prefixLengthToV4NetmaskIntHTH(prefix.getPrefixLength()); - mSubnetAddr = inet4AddressToIntHTH((Inet4Address) prefix.getAddress()) & mSubnetMask; - mNumAddresses = 1 << (IPV4_ADDR_BITS - prefix.getPrefixLength()); - mLeaseTimeMs = leaseTimeMs; - - cleanMap(mCommittedLeases); - cleanMap(mDeclinedAddrs); - } - - /** - * From a map keyed by {@link Inet4Address}, remove entries where the key is invalid (as - * specified by {@link #isValidAddress(Inet4Address)}), or is a reserved address. - */ - private void cleanMap(Map map) { - final Iterator> it = map.entrySet().iterator(); - while (it.hasNext()) { - final Inet4Address addr = it.next().getKey(); - if (!isValidAddress(addr) || mReservedAddrs.contains(addr)) { - it.remove(); - } - } - } - - /** - * Get a DHCP offer, to reply to a DHCPDISCOVER. Follows RFC2131 #4.3.1. - * - * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} - * @param relayAddr Internet address of the relay (giaddr), can be {@link Inet4Address#ANY} - * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC} - * @param hostname Client-provided hostname, or {@link DhcpLease#HOSTNAME_NONE} - * @throws OutOfAddressesException The server does not have any available address - * @throws InvalidSubnetException The lease was requested from an unsupported subnet - */ - @NonNull - public DhcpLease getOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, - @NonNull Inet4Address relayAddr, @Nullable Inet4Address reqAddr, - @Nullable String hostname) throws OutOfAddressesException, InvalidSubnetException { - final long currentTime = mClock.elapsedRealtime(); - final long expTime = currentTime + mLeaseTimeMs; - - removeExpiredLeases(currentTime); - checkValidRelayAddr(relayAddr); - - final DhcpLease currentLease = findByClient(clientId, hwAddr); - final DhcpLease newLease; - if (currentLease != null) { - newLease = currentLease.renewedLease(expTime, hostname); - mLog.log("Offering extended lease " + newLease); - // Do not update lease time in the map: the offer is not committed yet. - } else if (reqAddr != null && isValidAddress(reqAddr) && isAvailable(reqAddr)) { - newLease = new DhcpLease(clientId, hwAddr, reqAddr, expTime, hostname); - mLog.log("Offering requested lease " + newLease); - } else { - newLease = makeNewOffer(clientId, hwAddr, expTime, hostname); - mLog.log("Offering new generated lease " + newLease); - } - return newLease; - } - - private void checkValidRelayAddr(@Nullable Inet4Address relayAddr) - throws InvalidSubnetException { - // As per #4.3.1, addresses are assigned based on the relay address if present. This - // implementation only assigns addresses if the relayAddr is inside our configured subnet. - // This also applies when the client requested a specific address for consistency between - // requests, and with older behavior. - if (isIpAddrOutsidePrefix(mPrefix, relayAddr)) { - throw new InvalidSubnetException("Lease requested by relay from outside of subnet"); - } - } - - private static boolean isIpAddrOutsidePrefix(@NonNull IpPrefix prefix, - @Nullable Inet4Address addr) { - return addr != null && !addr.equals(Inet4Address.ANY) && !prefix.contains(addr); - } - - @Nullable - private DhcpLease findByClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) { - for (DhcpLease lease : mCommittedLeases.values()) { - if (lease.matchesClient(clientId, hwAddr)) { - return lease; - } - } - - // Note this differs from dnsmasq behavior, which would match by hwAddr if clientId was - // given but no lease keyed on clientId matched. This would prevent one interface from - // obtaining multiple leases with different clientId. - return null; - } - - /** - * Make a lease conformant to a client DHCPREQUEST or renew the client's existing lease, - * commit it to the repository and return it. - * - *

This method always succeeds and commits the lease if it does not throw, and has no side - * effects if it throws. - * - * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} - * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC} - * @param sidSet Whether the server identifier was set in the request - * @return The newly created or renewed lease - * @throws InvalidAddressException The client provided an address that conflicts with its - * current configuration, or other committed/reserved leases. - */ - @NonNull - public DhcpLease requestLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, - @NonNull Inet4Address clientAddr, @NonNull Inet4Address relayAddr, - @Nullable Inet4Address reqAddr, boolean sidSet, @Nullable String hostname) - throws InvalidAddressException, InvalidSubnetException { - final long currentTime = mClock.elapsedRealtime(); - removeExpiredLeases(currentTime); - checkValidRelayAddr(relayAddr); - final DhcpLease assignedLease = findByClient(clientId, hwAddr); - - final Inet4Address leaseAddr = reqAddr != null ? reqAddr : clientAddr; - if (assignedLease != null) { - if (sidSet && reqAddr != null) { - // Client in SELECTING state; remove any current lease before creating a new one. - mCommittedLeases.remove(assignedLease.getNetAddr()); - } else if (!assignedLease.getNetAddr().equals(leaseAddr)) { - // reqAddr null (RENEWING/REBINDING): client renewing its own lease for clientAddr. - // reqAddr set with sid not set (INIT-REBOOT): client verifying configuration. - // In both cases, throw if clientAddr or reqAddr does not match the known lease. - throw new InvalidAddressException("Incorrect address for client in " - + (reqAddr != null ? "INIT-REBOOT" : "RENEWING/REBINDING")); - } - } - - // In the init-reboot case, RFC2131 #4.3.2 says that the server must not reply if - // assignedLease == null, but dnsmasq will let the client use the requested address if - // available, when configured with --dhcp-authoritative. This is preferable to avoid issues - // if the server lost the lease DB: the client would not get a reply because the server - // does not know their lease. - // Similarly in RENEWING/REBINDING state, create a lease when possible if the - // client-provided lease is unknown. - final DhcpLease lease = - checkClientAndMakeLease(clientId, hwAddr, leaseAddr, hostname, currentTime); - mLog.logf("DHCPREQUEST assignedLease %s, reqAddr=%s, sidSet=%s: created/renewed lease %s", - assignedLease, inet4AddrToString(reqAddr), sidSet, lease); - return lease; - } - - /** - * Check that the client can request the specified address, make or renew the lease if yes, and - * commit it. - * - *

This method always succeeds and returns the lease if it does not throw, and has no - * side-effect if it throws. - * - * @return The newly created or renewed, committed lease - * @throws InvalidAddressException The client provided an address that conflicts with its - * current configuration, or other committed/reserved leases. - */ - private DhcpLease checkClientAndMakeLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, - @NonNull Inet4Address addr, @Nullable String hostname, long currentTime) - throws InvalidAddressException { - final long expTime = currentTime + mLeaseTimeMs; - final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null); - if (currentLease != null && !currentLease.matchesClient(clientId, hwAddr)) { - throw new InvalidAddressException("Address in use"); - } - - final DhcpLease lease; - if (currentLease == null) { - if (isValidAddress(addr) && !mReservedAddrs.contains(addr)) { - lease = new DhcpLease(clientId, hwAddr, addr, expTime, hostname); - } else { - throw new InvalidAddressException("Lease not found and address unavailable"); - } - } else { - lease = currentLease.renewedLease(expTime, hostname); - } - commitLease(lease); - return lease; - } - - private void commitLease(@NonNull DhcpLease lease) { - mCommittedLeases.put(lease.getNetAddr(), lease); - maybeUpdateEarliestExpiration(lease.getExpTime()); - } - - /** - * Delete a committed lease from the repository. - * - * @return true if a lease matching parameters was found. - */ - public boolean releaseLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, - @NonNull Inet4Address addr) { - final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null); - if (currentLease == null) { - mLog.w("Could not release unknown lease for " + inet4AddrToString(addr)); - return false; - } - if (currentLease.matchesClient(clientId, hwAddr)) { - mCommittedLeases.remove(addr); - mLog.log("Released lease " + currentLease); - return true; - } - mLog.w(String.format("Not releasing lease %s: does not match client (cid %s, hwAddr %s)", - currentLease, DhcpLease.clientIdToString(clientId), hwAddr)); - return false; - } - - public void markLeaseDeclined(@NonNull Inet4Address addr) { - if (mDeclinedAddrs.containsKey(addr) || !isValidAddress(addr)) { - mLog.logf("Not marking %s as declined: already declined or not assignable", - inet4AddrToString(addr)); - return; - } - final long expTime = mClock.elapsedRealtime() + mLeaseTimeMs; - mDeclinedAddrs.put(addr, expTime); - mLog.logf("Marked %s as declined expiring %d", inet4AddrToString(addr), expTime); - maybeUpdateEarliestExpiration(expTime); - } - - /** - * Get the list of currently valid committed leases in the repository. - */ - @NonNull - public List getCommittedLeases() { - removeExpiredLeases(mClock.elapsedRealtime()); - return new ArrayList<>(mCommittedLeases.values()); - } - - /** - * Get the set of addresses that have been marked as declined in the repository. - */ - @NonNull - public Set getDeclinedAddresses() { - removeExpiredLeases(mClock.elapsedRealtime()); - return new HashSet<>(mDeclinedAddrs.keySet()); - } - - /** - * Given the expiration time of a new committed lease or declined address, update - * {@link #mNextExpirationCheck} so it stays lower than or equal to the time for the first lease - * to expire. - */ - private void maybeUpdateEarliestExpiration(long expTime) { - if (expTime < mNextExpirationCheck) { - mNextExpirationCheck = expTime; - } - } - - /** - * Remove expired entries from a map keyed by {@link Inet4Address}. - * - * @param tag Type of lease in the map, for logging - * @param getExpTime Functor returning the expiration time for an object in the map. - * Must not return null. - * @return The lowest expiration time among entries remaining in the map - */ - private long removeExpired(long currentTime, @NonNull Map map, - @NonNull String tag, @NonNull Function getExpTime) { - final Iterator> it = map.entrySet().iterator(); - long firstExpiration = EXPIRATION_NEVER; - while (it.hasNext()) { - final Entry lease = it.next(); - final long expTime = getExpTime.apply(lease.getValue()); - if (expTime <= currentTime) { - mLog.logf("Removing expired %s lease for %s (expTime=%s, currentTime=%s)", - tag, lease.getKey(), expTime, currentTime); - it.remove(); - } else { - firstExpiration = min(firstExpiration, expTime); - } - } - return firstExpiration; - } - - /** - * Go through committed and declined leases and remove the expired ones. - */ - private void removeExpiredLeases(long currentTime) { - if (currentTime < mNextExpirationCheck) { - return; - } - - final long commExp = removeExpired( - currentTime, mCommittedLeases, "committed", DhcpLease::getExpTime); - final long declExp = removeExpired( - currentTime, mDeclinedAddrs, "declined", Function.identity()); - - mNextExpirationCheck = min(commExp, declExp); - } - - private boolean isAvailable(@NonNull Inet4Address addr) { - return !mReservedAddrs.contains(addr) && !mCommittedLeases.containsKey(addr); - } - - /** - * Get the 0-based index of an address in the subnet. - * - *

Given ordering of addresses 5.6.7.8 < 5.6.7.9 < 5.6.8.0, the index on a subnet is defined - * so that the first address is 0, the second 1, etc. For example on a /16, 192.168.0.0 -> 0, - * 192.168.0.1 -> 1, 192.168.1.0 -> 256 - * - */ - private int getAddrIndex(int addr) { - return addr & ~mSubnetMask; - } - - private int getAddrByIndex(int index) { - return mSubnetAddr | index; - } - - /** - * Get a valid address starting from the supplied one. - * - *

This only checks that the address is numerically valid for assignment, not whether it is - * already in use. The return value is always inside the configured prefix, even if the supplied - * address is not. - * - *

If the provided address is valid, it is returned as-is. Otherwise, the next valid - * address (with the ordering in {@link #getAddrIndex(int)}) is returned. - */ - private int getValidAddress(int addr) { - final int lastByteMask = 0xff; - int addrIndex = getAddrIndex(addr); // 0-based index of the address in the subnet - - // Some OSes do not handle addresses in .255 or .0 correctly: avoid those. - final int lastByte = getAddrByIndex(addrIndex) & lastByteMask; - if (lastByte == lastByteMask) { - // Avoid .255 address, and .0 address that follows - addrIndex = (addrIndex + 2) % mNumAddresses; - } else if (lastByte == 0) { - // Avoid .0 address - addrIndex = (addrIndex + 1) % mNumAddresses; - } - - // Do not use first or last address of range - if (addrIndex == 0 || addrIndex == mNumAddresses - 1) { - // Always valid and not end of range since prefixLength is at most 30 in serving params - addrIndex = 1; - } - return getAddrByIndex(addrIndex); - } - - /** - * Returns whether the address is in the configured subnet and part of the assignable range. - */ - private boolean isValidAddress(Inet4Address addr) { - final int intAddr = inet4AddressToIntHTH(addr); - return getValidAddress(intAddr) == intAddr; - } - - private int getNextAddress(int addr) { - final int addrIndex = getAddrIndex(addr); - final int nextAddress = getAddrByIndex((addrIndex + 1) % mNumAddresses); - return getValidAddress(nextAddress); - } - - /** - * Calculate a first candidate address for a client by hashing the hardware address. - * - *

This will be a valid address as checked by {@link #getValidAddress(int)}, but may be - * in use. - * - * @return An IPv4 address encoded as 32-bit int - */ - private int getFirstClientAddress(MacAddress hwAddr) { - // This follows dnsmasq behavior. Advantages are: clients will often get the same - // offers for different DISCOVER even if the lease was not yet accepted or has expired, - // and address generation will generally not need to loop through many allocated addresses - // until it finds a free one. - int hash = 0; - for (byte b : hwAddr.toByteArray()) { - hash += b + (b << 8) + (b << 16); - } - // This implementation will not always result in the same IPs as dnsmasq would give out in - // Android <= P, because it includes invalid and reserved addresses in mNumAddresses while - // the configured ranges for dnsmasq did not. - final int addrIndex = hash % mNumAddresses; - return getValidAddress(getAddrByIndex(addrIndex)); - } - - /** - * Create a lease that can be offered to respond to a client DISCOVER. - * - *

This method always succeeds and returns the lease if it does not throw. If no non-declined - * address is available, it will try to offer the oldest declined address if valid. - * - * @throws OutOfAddressesException The server has no address left to offer - */ - private DhcpLease makeNewOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, - long expTime, @Nullable String hostname) throws OutOfAddressesException { - int intAddr = getFirstClientAddress(hwAddr); - // 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 = intToInet4AddressHTH(intAddr); - if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) { - return new DhcpLease(clientId, hwAddr, addr, expTime, hostname); - } - intAddr = getNextAddress(intAddr); - } - - // Try freeing DECLINEd addresses if out of addresses. - final Iterator it = mDeclinedAddrs.keySet().iterator(); - while (it.hasNext()) { - final Inet4Address addr = it.next(); - it.remove(); - mLog.logf("Out of addresses in address pool: dropped declined addr %s", - inet4AddrToString(addr)); - // isValidAddress() is always verified for entries in mDeclinedAddrs. - // However declined addresses may have been requested (typically by the machine that was - // already using the address) after being declined. - if (isAvailable(addr)) { - return new DhcpLease(clientId, hwAddr, addr, expTime, hostname); - } - } - - throw new OutOfAddressesException("No address available for offer"); - } -} diff --git a/services/net/java/android/net/dhcp/DhcpPacket.java b/services/net/java/android/net/dhcp/DhcpPacket.java index 6ba7d94117b1..ce8b7e78d0f8 100644 --- a/services/net/java/android/net/dhcp/DhcpPacket.java +++ b/services/net/java/android/net/dhcp/DhcpPacket.java @@ -1,8 +1,5 @@ package android.net.dhcp; -import static android.net.util.NetworkConstants.IPV4_MAX_MTU; -import static android.net.util.NetworkConstants.IPV4_MIN_MTU; - import android.annotation.Nullable; import android.net.DhcpResults; import android.net.LinkAddress; @@ -37,6 +34,9 @@ import java.util.List; public abstract class DhcpPacket { protected static final String TAG = "DhcpPacket"; + // TODO: use NetworkStackConstants.IPV4_MIN_MTU once this class is moved to the network stack. + private static final int IPV4_MIN_MTU = 68; + // dhcpcd has a minimum lease of 20 seconds, but DhcpStateMachine would refuse to wake up the // CPU for anything shorter than 5 minutes. For sanity's sake, this must be higher than the // DHCP client timeout. diff --git a/services/net/java/android/net/dhcp/DhcpPacketListener.java b/services/net/java/android/net/dhcp/DhcpPacketListener.java deleted file mode 100644 index dce8b619494e..000000000000 --- a/services/net/java/android/net/dhcp/DhcpPacketListener.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 android.annotation.NonNull; -import android.annotation.Nullable; -import android.net.util.FdEventsReader; -import android.os.Handler; -import android.system.Os; - -import java.io.FileDescriptor; -import java.net.Inet4Address; -import java.net.InetSocketAddress; - -/** - * A {@link FdEventsReader} to receive and parse {@link DhcpPacket}. - * @hide - */ -abstract class DhcpPacketListener extends FdEventsReader { - static final class Payload { - protected final byte[] mBytes = new byte[DhcpPacket.MAX_LENGTH]; - protected Inet4Address mSrcAddr; - protected int mSrcPort; - } - - DhcpPacketListener(@NonNull Handler handler) { - super(handler, new Payload()); - } - - @Override - protected int recvBufSize(@NonNull Payload buffer) { - return buffer.mBytes.length; - } - - @Override - protected final void handlePacket(@NonNull Payload recvbuf, int length) { - if (recvbuf.mSrcAddr == null) { - return; - } - - try { - final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf.mBytes, length, - DhcpPacket.ENCAP_BOOTP); - onReceive(packet, recvbuf.mSrcAddr, recvbuf.mSrcPort); - } catch (DhcpPacket.ParseException e) { - logParseError(recvbuf.mBytes, length, e); - } - } - - @Override - protected int readPacket(@NonNull FileDescriptor fd, @NonNull Payload packetBuffer) - throws Exception { - final InetSocketAddress addr = new InetSocketAddress(); - final int read = Os.recvfrom( - fd, packetBuffer.mBytes, 0, packetBuffer.mBytes.length, 0 /* flags */, addr); - - // Buffers with null srcAddr will be dropped in handlePacket() - packetBuffer.mSrcAddr = inet4AddrOrNull(addr); - packetBuffer.mSrcPort = addr.getPort(); - return read; - } - - @Nullable - private static Inet4Address inet4AddrOrNull(@NonNull InetSocketAddress addr) { - return addr.getAddress() instanceof Inet4Address - ? (Inet4Address) addr.getAddress() - : null; - } - - protected abstract void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr, - int srcPort); - protected abstract void logParseError(@NonNull byte[] packet, int length, - @NonNull DhcpPacket.ParseException e); -} diff --git a/services/net/java/android/net/dhcp/DhcpServer.java b/services/net/java/android/net/dhcp/DhcpServer.java deleted file mode 100644 index 641bba2ed306..000000000000 --- a/services/net/java/android/net/dhcp/DhcpServer.java +++ /dev/null @@ -1,582 +0,0 @@ -/* - * 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.getBroadcastAddress; -import static android.net.NetworkUtils.getPrefixMaskAsInet4Address; -import static android.net.TrafficStats.TAG_SYSTEM_DHCP_SERVER; -import static android.net.dhcp.DhcpPacket.DHCP_CLIENT; -import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME; -import static android.net.dhcp.DhcpPacket.DHCP_SERVER; -import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP; -import static android.net.dhcp.DhcpPacket.INFINITE_LEASE; -import static android.system.OsConstants.AF_INET; -import static android.system.OsConstants.IPPROTO_UDP; -import static android.system.OsConstants.SOCK_DGRAM; -import static android.system.OsConstants.SOL_SOCKET; -import static android.system.OsConstants.SO_BINDTODEVICE; -import static android.system.OsConstants.SO_BROADCAST; -import static android.system.OsConstants.SO_REUSEADDR; - -import static java.lang.Integer.toUnsignedLong; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.net.MacAddress; -import android.net.NetworkUtils; -import android.net.TrafficStats; -import android.net.util.SharedLog; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; -import android.system.ErrnoException; -import android.system.Os; -import android.text.TextUtils; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.HexDump; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.net.Inet4Address; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.ArrayList; - -/** - * A DHCPv4 server. - * - *

This server listens for and responds to packets on a single interface. It considers itself - * authoritative for all leases on the subnet, which means that DHCP requests for unknown leases of - * unknown hosts receive a reply instead of being ignored. - * - *

The server is single-threaded (including send/receive operations): all internal operations are - * done on the provided {@link Looper}. Public methods are thread-safe and will schedule operations - * on the looper asynchronously. - * @hide - */ -public class DhcpServer { - private static final String REPO_TAG = "Repository"; - - // Lease time to transmit to client instead of a negative time in case a lease expired before - // the server could send it (if the server process is suspended for example). - private static final int EXPIRED_FALLBACK_LEASE_TIME_SECS = 120; - - private static final int CMD_START_DHCP_SERVER = 1; - private static final int CMD_STOP_DHCP_SERVER = 2; - private static final int CMD_UPDATE_PARAMS = 3; - - @NonNull - private final ServerHandler mHandler; - @NonNull - private final String mIfName; - @NonNull - private final DhcpLeaseRepository mLeaseRepo; - @NonNull - private final SharedLog mLog; - @NonNull - private final Dependencies mDeps; - @NonNull - private final Clock mClock; - @NonNull - private final DhcpPacketListener mPacketListener; - - @Nullable - private FileDescriptor mSocket; - @NonNull - private DhcpServingParams mServingParams; - - /** - * Clock to be used by DhcpServer to track time for lease expiration. - * - *

The clock should track time as may be measured by clients obtaining a lease. It does not - * need to be monotonous across restarts of the server as long as leases are cleared when the - * server is stopped. - */ - public static class Clock { - /** - * @see SystemClock#elapsedRealtime() - */ - public long elapsedRealtime() { - return SystemClock.elapsedRealtime(); - } - } - - /** - * Dependencies for the DhcpServer. Useful to be mocked in tests. - */ - public interface Dependencies { - /** - * Send a packet to the specified datagram socket. - * - * @param fd File descriptor of the socket. - * @param buffer Data to be sent. - * @param dst Destination address of the packet. - */ - void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer, - @NonNull InetAddress dst) throws ErrnoException, IOException; - - /** - * Create a DhcpLeaseRepository for the server. - * @param servingParams Parameters used to serve DHCP requests. - * @param log Log to be used by the repository. - * @param clock Clock that the repository must use to track time. - */ - DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams, - @NonNull SharedLog log, @NonNull Clock clock); - - /** - * Create a packet listener that will send packets to be processed. - */ - DhcpPacketListener makePacketListener(); - - /** - * Create a clock that the server will use to track time. - */ - Clock makeClock(); - - /** - * Add an entry to the ARP cache table. - * @param fd Datagram socket file descriptor that must use the new entry. - */ - void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr, - @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException; - } - - private class DependenciesImpl implements Dependencies { - @Override - public void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer, - @NonNull InetAddress dst) throws ErrnoException, IOException { - Os.sendto(fd, buffer, 0, dst, DhcpPacket.DHCP_CLIENT); - } - - @Override - public DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams, - @NonNull SharedLog log, @NonNull Clock clock) { - return new DhcpLeaseRepository( - DhcpServingParams.makeIpPrefix(servingParams.serverAddr), - servingParams.excludedAddrs, - servingParams.dhcpLeaseTimeSecs * 1000, log.forSubComponent(REPO_TAG), clock); - } - - @Override - public DhcpPacketListener makePacketListener() { - return new PacketListener(); - } - - @Override - public Clock makeClock() { - return new Clock(); - } - - @Override - public void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr, - @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException { - NetworkUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd); - } - } - - private static class MalformedPacketException extends Exception { - MalformedPacketException(String message, Throwable t) { - super(message, t); - } - } - - public DhcpServer(@NonNull Looper looper, @NonNull String ifName, - @NonNull DhcpServingParams params, @NonNull SharedLog log) { - this(looper, ifName, params, log, null); - } - - @VisibleForTesting - DhcpServer(@NonNull Looper looper, @NonNull String ifName, - @NonNull DhcpServingParams params, @NonNull SharedLog log, - @Nullable Dependencies deps) { - if (deps == null) { - deps = new DependenciesImpl(); - } - mHandler = new ServerHandler(looper); - mIfName = ifName; - mServingParams = params; - mLog = log; - mDeps = deps; - mClock = deps.makeClock(); - mPacketListener = deps.makePacketListener(); - mLeaseRepo = deps.makeLeaseRepository(mServingParams, mLog, mClock); - } - - /** - * Start listening for and responding to packets. - */ - public void start() { - mHandler.sendEmptyMessage(CMD_START_DHCP_SERVER); - } - - /** - * Update serving parameters. All subsequently received requests will be handled with the new - * parameters, and current leases that are incompatible with the new parameters are dropped. - */ - public void updateParams(@NonNull DhcpServingParams params) { - sendMessage(CMD_UPDATE_PARAMS, params); - } - - /** - * Stop listening for packets. - * - *

As the server is stopped asynchronously, some packets may still be processed shortly after - * calling this method. - */ - public void stop() { - mHandler.sendEmptyMessage(CMD_STOP_DHCP_SERVER); - } - - private void sendMessage(int what, @Nullable Object obj) { - mHandler.sendMessage(mHandler.obtainMessage(what, obj)); - } - - private class ServerHandler extends Handler { - ServerHandler(@NonNull Looper looper) { - super(looper); - } - - @Override - public void handleMessage(@NonNull Message msg) { - switch (msg.what) { - case CMD_UPDATE_PARAMS: - final DhcpServingParams params = (DhcpServingParams) msg.obj; - mServingParams = params; - mLeaseRepo.updateParams( - DhcpServingParams.makeIpPrefix(mServingParams.serverAddr), - params.excludedAddrs, - params.dhcpLeaseTimeSecs); - break; - case CMD_START_DHCP_SERVER: - // This is a no-op if the listener is already started - mPacketListener.start(); - break; - case CMD_STOP_DHCP_SERVER: - // This is a no-op if the listener was not started - mPacketListener.stop(); - break; - } - } - } - - @VisibleForTesting - void processPacket(@NonNull DhcpPacket packet, int srcPort) { - final String packetType = packet.getClass().getSimpleName(); - if (srcPort != DHCP_CLIENT) { - mLog.logf("Ignored packet of type %s sent from client port %d", packetType, srcPort); - return; - } - - mLog.log("Received packet of type " + packetType); - final Inet4Address sid = packet.mServerIdentifier; - if (sid != null && !sid.equals(mServingParams.serverAddr.getAddress())) { - mLog.log("Packet ignored due to wrong server identifier: " + sid); - return; - } - - try { - if (packet instanceof DhcpDiscoverPacket) { - processDiscover((DhcpDiscoverPacket) packet); - } else if (packet instanceof DhcpRequestPacket) { - processRequest((DhcpRequestPacket) packet); - } else if (packet instanceof DhcpReleasePacket) { - processRelease((DhcpReleasePacket) packet); - } else { - mLog.e("Unknown packet type: " + packet.getClass().getSimpleName()); - } - } catch (MalformedPacketException e) { - // Not an internal error: only logging exception message, not stacktrace - mLog.e("Ignored malformed packet: " + e.getMessage()); - } - } - - private void logIgnoredPacketInvalidSubnet(DhcpLeaseRepository.InvalidSubnetException e) { - // Not an internal error: only logging exception message, not stacktrace - mLog.e("Ignored packet from invalid subnet: " + e.getMessage()); - } - - private void processDiscover(@NonNull DhcpDiscoverPacket packet) - throws MalformedPacketException { - final DhcpLease lease; - final MacAddress clientMac = getMacAddr(packet); - try { - lease = mLeaseRepo.getOffer(packet.getExplicitClientIdOrNull(), clientMac, - packet.mRelayIp, packet.mRequestedIp, packet.mHostName); - } catch (DhcpLeaseRepository.OutOfAddressesException e) { - transmitNak(packet, "Out of addresses to offer"); - return; - } catch (DhcpLeaseRepository.InvalidSubnetException e) { - logIgnoredPacketInvalidSubnet(e); - return; - } - - transmitOffer(packet, lease, clientMac); - } - - private void processRequest(@NonNull DhcpRequestPacket packet) throws MalformedPacketException { - // If set, packet SID matches with this server's ID as checked in processPacket(). - final boolean sidSet = packet.mServerIdentifier != null; - final DhcpLease lease; - final MacAddress clientMac = getMacAddr(packet); - try { - lease = mLeaseRepo.requestLease(packet.getExplicitClientIdOrNull(), clientMac, - packet.mClientIp, packet.mRelayIp, packet.mRequestedIp, sidSet, - packet.mHostName); - } catch (DhcpLeaseRepository.InvalidAddressException e) { - transmitNak(packet, "Invalid requested address"); - return; - } catch (DhcpLeaseRepository.InvalidSubnetException e) { - logIgnoredPacketInvalidSubnet(e); - return; - } - - transmitAck(packet, lease, clientMac); - } - - private void processRelease(@NonNull DhcpReleasePacket packet) - throws MalformedPacketException { - final byte[] clientId = packet.getExplicitClientIdOrNull(); - final MacAddress macAddr = getMacAddr(packet); - // Don't care about success (there is no ACK/NAK); logging is already done in the repository - mLeaseRepo.releaseLease(clientId, macAddr, packet.mClientIp); - } - - private Inet4Address getAckOrOfferDst(@NonNull DhcpPacket request, @NonNull DhcpLease lease, - boolean broadcastFlag) { - // Unless relayed or broadcast, send to client IP if already configured on the client, or to - // the lease address if the client has no configured address - if (!isEmpty(request.mRelayIp)) { - return request.mRelayIp; - } else if (broadcastFlag) { - return (Inet4Address) Inet4Address.ALL; - } else if (!isEmpty(request.mClientIp)) { - return request.mClientIp; - } else { - return lease.getNetAddr(); - } - } - - /** - * Determine whether the broadcast flag should be set in the BOOTP packet flags. This does not - * apply to NAK responses, which should always have it set. - */ - private static boolean getBroadcastFlag(@NonNull DhcpPacket request, @NonNull DhcpLease lease) { - // No broadcast flag if the client already has a configured IP to unicast to. RFC2131 #4.1 - // has some contradictions regarding broadcast behavior if a client already has an IP - // configured and sends a request with both ciaddr (renew/rebind) and the broadcast flag - // set. Sending a unicast response to ciaddr matches previous behavior and is more - // efficient. - // If the client has no configured IP, broadcast if requested by the client or if the lease - // address cannot be used to send a unicast reply either. - return isEmpty(request.mClientIp) && (request.mBroadcast || isEmpty(lease.getNetAddr())); - } - - /** - * Get the hostname from a lease if non-empty and requested in the incoming request. - * @param request The incoming request. - * @return The hostname, or null if not requested or empty. - */ - @Nullable - private static String getHostnameIfRequested(@NonNull DhcpPacket request, - @NonNull DhcpLease lease) { - return request.hasRequestedParam(DHCP_HOST_NAME) && !TextUtils.isEmpty(lease.getHostname()) - ? lease.getHostname() - : null; - } - - private boolean transmitOffer(@NonNull DhcpPacket request, @NonNull DhcpLease lease, - @NonNull MacAddress clientMac) { - final boolean broadcastFlag = getBroadcastFlag(request, lease); - final int timeout = getLeaseTimeout(lease); - final Inet4Address prefixMask = - getPrefixMaskAsInet4Address(mServingParams.serverAddr.getPrefixLength()); - final Inet4Address broadcastAddr = getBroadcastAddress( - mServingParams.getServerInet4Addr(), mServingParams.serverAddr.getPrefixLength()); - final String hostname = getHostnameIfRequested(request, lease); - final ByteBuffer offerPacket = DhcpPacket.buildOfferPacket( - ENCAP_BOOTP, request.mTransId, broadcastFlag, mServingParams.getServerInet4Addr(), - request.mRelayIp, lease.getNetAddr(), request.mClientMac, timeout, prefixMask, - broadcastAddr, new ArrayList<>(mServingParams.defaultRouters), - new ArrayList<>(mServingParams.dnsServers), - mServingParams.getServerInet4Addr(), null /* domainName */, hostname, - mServingParams.metered, (short) mServingParams.linkMtu); - - return transmitOfferOrAckPacket(offerPacket, request, lease, clientMac, broadcastFlag); - } - - private boolean transmitAck(@NonNull DhcpPacket request, @NonNull DhcpLease lease, - @NonNull MacAddress clientMac) { - // TODO: replace DhcpPacket's build methods with real builders and use common code with - // transmitOffer above - final boolean broadcastFlag = getBroadcastFlag(request, lease); - final int timeout = getLeaseTimeout(lease); - final String hostname = getHostnameIfRequested(request, lease); - final ByteBuffer ackPacket = DhcpPacket.buildAckPacket(ENCAP_BOOTP, request.mTransId, - broadcastFlag, mServingParams.getServerInet4Addr(), request.mRelayIp, - lease.getNetAddr(), request.mClientIp, request.mClientMac, timeout, - mServingParams.getPrefixMaskAsAddress(), mServingParams.getBroadcastAddress(), - new ArrayList<>(mServingParams.defaultRouters), - new ArrayList<>(mServingParams.dnsServers), - mServingParams.getServerInet4Addr(), null /* domainName */, hostname, - mServingParams.metered, (short) mServingParams.linkMtu); - - return transmitOfferOrAckPacket(ackPacket, request, lease, clientMac, broadcastFlag); - } - - private boolean transmitNak(DhcpPacket request, String message) { - mLog.w("Transmitting NAK: " + message); - // Always set broadcast flag for NAK: client may not have a correct IP - final ByteBuffer nakPacket = DhcpPacket.buildNakPacket( - ENCAP_BOOTP, request.mTransId, mServingParams.getServerInet4Addr(), - request.mRelayIp, request.mClientMac, true /* broadcast */, message); - - final Inet4Address dst = isEmpty(request.mRelayIp) - ? (Inet4Address) Inet4Address.ALL - : request.mRelayIp; - return transmitPacket(nakPacket, DhcpNakPacket.class.getSimpleName(), dst); - } - - private boolean transmitOfferOrAckPacket(@NonNull ByteBuffer buf, @NonNull DhcpPacket request, - @NonNull DhcpLease lease, @NonNull MacAddress clientMac, boolean broadcastFlag) { - mLog.logf("Transmitting %s with lease %s", request.getClass().getSimpleName(), lease); - // Client may not yet respond to ARP for the lease address, which may be the destination - // address. Add an entry to the ARP cache to save future ARP probes and make sure the - // packet reaches its destination. - if (!addArpEntry(clientMac, lease.getNetAddr())) { - // Logging for error already done - return false; - } - final Inet4Address dst = getAckOrOfferDst(request, lease, broadcastFlag); - return transmitPacket(buf, request.getClass().getSimpleName(), dst); - } - - private boolean transmitPacket(@NonNull ByteBuffer buf, @NonNull String packetTypeTag, - @NonNull Inet4Address dst) { - try { - mDeps.sendPacket(mSocket, buf, dst); - } catch (ErrnoException | IOException e) { - mLog.e("Can't send packet " + packetTypeTag, e); - return false; - } - return true; - } - - private boolean addArpEntry(@NonNull MacAddress macAddr, @NonNull Inet4Address inetAddr) { - try { - mDeps.addArpEntry(inetAddr, macAddr, mIfName, mSocket); - return true; - } catch (IOException e) { - mLog.e("Error adding client to ARP table", e); - return false; - } - } - - /** - * Get the remaining lease time in seconds, starting from {@link Clock#elapsedRealtime()}. - * - *

This is an unsigned 32-bit integer, so it cannot be read as a standard (signed) Java int. - * The return value is only intended to be used to populate the lease time field in a DHCP - * response, considering that lease time is an unsigned 32-bit integer field in DHCP packets. - * - *

Lease expiration times are tracked internally with millisecond precision: this method - * returns a rounded down value. - */ - private int getLeaseTimeout(@NonNull DhcpLease lease) { - final long remainingTimeSecs = (lease.getExpTime() - mClock.elapsedRealtime()) / 1000; - if (remainingTimeSecs < 0) { - mLog.e("Processing expired lease " + lease); - return EXPIRED_FALLBACK_LEASE_TIME_SECS; - } - - if (remainingTimeSecs >= toUnsignedLong(INFINITE_LEASE)) { - return INFINITE_LEASE; - } - - return (int) remainingTimeSecs; - } - - /** - * Get the client MAC address from a packet. - * - * @throws MalformedPacketException The address in the packet uses an unsupported format. - */ - @NonNull - private MacAddress getMacAddr(@NonNull DhcpPacket packet) throws MalformedPacketException { - try { - return MacAddress.fromBytes(packet.getClientMac()); - } catch (IllegalArgumentException e) { - final String message = "Invalid MAC address in packet: " - + HexDump.dumpHexString(packet.getClientMac()); - throw new MalformedPacketException(message, e); - } - } - - private static boolean isEmpty(@Nullable Inet4Address address) { - return address == null || Inet4Address.ANY.equals(address); - } - - private class PacketListener extends DhcpPacketListener { - PacketListener() { - super(mHandler); - } - - @Override - protected void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr, - int srcPort) { - processPacket(packet, srcPort); - } - - @Override - protected void logError(@NonNull String msg, Exception e) { - mLog.e("Error receiving packet: " + msg, e); - } - - @Override - protected void logParseError(@NonNull byte[] packet, int length, - @NonNull DhcpPacket.ParseException e) { - mLog.e("Error parsing packet", e); - } - - @Override - protected FileDescriptor createFd() { - // TODO: have and use an API to set a socket tag without going through the thread tag - final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_DHCP_SERVER); - try { - mSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - Os.setsockoptInt(mSocket, SOL_SOCKET, SO_REUSEADDR, 1); - // SO_BINDTODEVICE actually takes a string. This works because the first member - // of struct ifreq is a NULL-terminated interface name. - // TODO: add a setsockoptString() - Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mIfName); - Os.setsockoptInt(mSocket, SOL_SOCKET, SO_BROADCAST, 1); - Os.bind(mSocket, Inet4Address.ANY, DHCP_SERVER); - NetworkUtils.protectFromVpn(mSocket); - - return mSocket; - } catch (IOException | ErrnoException e) { - mLog.e("Error creating UDP socket", e); - DhcpServer.this.stop(); - return null; - } finally { - TrafficStats.setThreadStatsTag(oldTag); - } - } - } -} diff --git a/services/net/java/android/net/dhcp/DhcpServingParams.java b/services/net/java/android/net/dhcp/DhcpServingParams.java deleted file mode 100644 index 2780814a2f33..000000000000 --- a/services/net/java/android/net/dhcp/DhcpServingParams.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * 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.NetworkUtils.intToInet4AddressHTH; -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.annotation.Nullable; -import android.net.IpPrefix; -import android.net.LinkAddress; -import android.net.NetworkUtils; - -import com.google.android.collect.Sets; - -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; - - /** - * Indicates whether the DHCP server should send the ANDROID_METERED vendor-specific option. - */ - public final boolean metered; - - /** - * 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, boolean metered) { - this.serverAddr = serverAddr; - this.defaultRouters = defaultRouters; - this.dnsServers = dnsServers; - this.excludedAddrs = excludedAddrs; - this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs; - this.linkMtu = linkMtu; - this.metered = metered; - } - - /** - * Create parameters from a stable AIDL-compatible parcel. - */ - public static DhcpServingParams fromParcelableObject(@NonNull DhcpServingParamsParcel parcel) - throws InvalidParameterException { - final LinkAddress serverAddr = new LinkAddress( - intToInet4AddressHTH(parcel.serverAddr), - parcel.serverAddrPrefixLength); - return new Builder() - .setServerAddr(serverAddr) - .setDefaultRouters(toInet4AddressSet(parcel.defaultRouters)) - .setDnsServers(toInet4AddressSet(parcel.dnsServers)) - .setExcludedAddrs(toInet4AddressSet(parcel.excludedAddrs)) - .setDhcpLeaseTimeSecs(parcel.dhcpLeaseTimeSecs) - .setLinkMtu(parcel.linkMtu) - .setMetered(parcel.metered) - .build(); - } - - private static Set toInet4AddressSet(@Nullable int[] addrs) { - if (addrs == null) { - return new HashSet<>(0); - } - - final HashSet res = new HashSet<>(); - for (int addr : addrs) { - res.add(intToInet4AddressHTH(addr)); - } - return res; - } - - @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 mServerAddr; - private Set mDefaultRouters; - private Set mDnsServers; - private Set mExcludedAddrs; - private long mDhcpLeaseTimeSecs; - private int mLinkMtu = MTU_UNSET; - private boolean mMetered; - - /** - * Set the server address and served prefix for the DHCP server. - * - *

This parameter is required. - */ - public Builder setServerAddr(@NonNull LinkAddress serverAddr) { - this.mServerAddr = 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.mDefaultRouters = defaultRouters; - 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 list of routers, - * but it must always be set explicitly before building the {@link DhcpServingParams}. - */ - public Builder setDefaultRouters(@NonNull Inet4Address... defaultRouters) { - return setDefaultRouters(Sets.newArraySet(defaultRouters)); - } - - /** - * Convenience method to build the parameters with no default router. - * - *

Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address. - */ - public Builder withNoDefaultRouter() { - return setDefaultRouters(); - } - - /** - * 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.mDnsServers = dnsServers; - return this; - } - - /** - * Set the DNS servers to be advertised to DHCP clients. - * - *

This may be an empty list of servers, but it must always be set explicitly before - * building the {@link DhcpServingParams}. - */ - public Builder setDnsServers(@NonNull Inet4Address... dnsServers) { - return setDnsServers(Sets.newArraySet(dnsServers)); - } - - /** - * Convenience method to build the parameters with no DNS server. - * - *

Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address. - */ - public Builder withNoDnsServer() { - return setDnsServers(); - } - - /** - * 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.mExcludedAddrs = excludedAddrs; - 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 Inet4Address... excludedAddrs) { - return setExcludedAddrs(Sets.newArraySet(excludedAddrs)); - } - - /** - * Set the lease time for leases assigned by the DHCP server. - * - *

This parameter is required. - */ - public Builder setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) { - this.mDhcpLeaseTimeSecs = 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.mLinkMtu = linkMtu; - return this; - } - - /** - * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option. - * - *

If not set, the default value is false. - */ - public Builder setMetered(boolean metered) { - this.mMetered = metered; - 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 (mServerAddr == null) { - throw new InvalidParameterException("Missing serverAddr"); - } - if (mDefaultRouters == null) { - throw new InvalidParameterException("Missing defaultRouters"); - } - if (mDnsServers == null) { - // Empty set is OK, but enforce explicitly setting it - throw new InvalidParameterException("Missing dnsServers"); - } - if (mDhcpLeaseTimeSecs <= 0 || mDhcpLeaseTimeSecs > toUnsignedLong(INFINITE_LEASE)) { - throw new InvalidParameterException("Invalid lease time: " + mDhcpLeaseTimeSecs); - } - if (mLinkMtu != MTU_UNSET && (mLinkMtu < IPV4_MIN_MTU || mLinkMtu > IPV4_MAX_MTU)) { - throw new InvalidParameterException("Invalid link MTU: " + mLinkMtu); - } - if (!mServerAddr.isIPv4()) { - throw new InvalidParameterException("serverAddr must be IPv4"); - } - if (mServerAddr.getPrefixLength() < MIN_PREFIX_LENGTH - || mServerAddr.getPrefixLength() > MAX_PREFIX_LENGTH) { - throw new InvalidParameterException("Prefix length is not in supported range"); - } - - final IpPrefix prefix = makeIpPrefix(mServerAddr); - for (Inet4Address addr : mDefaultRouters) { - if (!prefix.contains(addr)) { - throw new InvalidParameterException(String.format( - "Default router %s is not in server prefix %s", addr, mServerAddr)); - } - } - - final Set excl = new HashSet<>(); - if (mExcludedAddrs != null) { - excl.addAll(mExcludedAddrs); - } - excl.add((Inet4Address) mServerAddr.getAddress()); - excl.addAll(mDefaultRouters); - excl.addAll(mDnsServers); - - return new DhcpServingParams(mServerAddr, - Collections.unmodifiableSet(new HashSet<>(mDefaultRouters)), - Collections.unmodifiableSet(new HashSet<>(mDnsServers)), - Collections.unmodifiableSet(excl), - mDhcpLeaseTimeSecs, mLinkMtu, mMetered); - } - } - - /** - * Utility method to create an IpPrefix with the address and prefix length of a LinkAddress. - */ - @NonNull - static IpPrefix makeIpPrefix(@NonNull LinkAddress addr) { - return new IpPrefix(addr.getAddress(), addr.getPrefixLength()); - } -} diff --git a/services/net/java/android/net/ip/IpServer.java b/services/net/java/android/net/ip/IpServer.java index 493350d776f3..8b22f68286af 100644 --- a/services/net/java/android/net/ip/IpServer.java +++ b/services/net/java/android/net/ip/IpServer.java @@ -17,20 +17,26 @@ package android.net.ip; import static android.net.NetworkUtils.numericToInetAddress; -import static android.net.util.NetworkConstants.asByte; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; import static android.net.util.NetworkConstants.FF; import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH; +import static android.net.util.NetworkConstants.asByte; +import android.content.Context; import android.net.ConnectivityManager; import android.net.INetd; +import android.net.INetworkStackStatusCallback; import android.net.INetworkStatsService; import android.net.InterfaceConfiguration; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; +import android.net.NetworkStack; import android.net.RouteInfo; -import android.net.dhcp.DhcpServer; -import android.net.dhcp.DhcpServingParams; +import android.net.dhcp.DhcpServerCallbacks; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.DhcpServingParamsParcelExt; +import android.net.dhcp.IDhcpServer; import android.net.ip.RouterAdvertisementDaemon.RaParams; import android.net.util.InterfaceParams; import android.net.util.InterfaceSet; @@ -126,6 +132,10 @@ public class IpServer extends StateMachine { } public static class Dependencies { + private final Context mContext; + public Dependencies(Context context) { + mContext = context; + } public RouterAdvertisementDaemon getRouterAdvertisementDaemon(InterfaceParams ifParams) { return new RouterAdvertisementDaemon(ifParams); } @@ -138,9 +148,12 @@ public class IpServer extends StateMachine { return NetdService.getInstance(); } - public DhcpServer makeDhcpServer(Looper looper, String ifName, - DhcpServingParams params, SharedLog log) { - return new DhcpServer(looper, ifName, params, log); + /** + * Create a DhcpServer instance to be used by IpServer. + */ + public void makeDhcpServer(String ifName, DhcpServingParamsParcel params, + DhcpServerCallbacks cb) { + mContext.getSystemService(NetworkStack.class).makeDhcpServer(ifName, params, cb); } } @@ -197,7 +210,10 @@ public class IpServer extends StateMachine { // Advertisements (otherwise, we do not add them to mLinkProperties at all). private LinkProperties mLastIPv6LinkProperties; private RouterAdvertisementDaemon mRaDaemon; - private DhcpServer mDhcpServer; + + // To be accessed only on the handler thread + private int mDhcpServerStartIndex = 0; + private IDhcpServer mDhcpServer; private RaParams mLastRaParams; public IpServer( @@ -252,35 +268,109 @@ public class IpServer extends StateMachine { private boolean startIPv4() { return configureIPv4(true); } + /** + * Convenience wrapper around INetworkStackStatusCallback to run callbacks on the IpServer + * handler. + * + *

Different instances of this class can be created for each call to IDhcpServer methods, + * with different implementations of the callback, to differentiate handling of success/error in + * each call. + */ + private abstract class OnHandlerStatusCallback extends INetworkStackStatusCallback.Stub { + @Override + public void onStatusAvailable(int statusCode) { + getHandler().post(() -> callback(statusCode)); + } + + public abstract void callback(int statusCode); + } + + private class DhcpServerCallbacksImpl extends DhcpServerCallbacks { + private final int mStartIndex; + + private DhcpServerCallbacksImpl(int startIndex) { + mStartIndex = startIndex; + } + + @Override + public void onDhcpServerCreated(int statusCode, IDhcpServer server) throws RemoteException { + getHandler().post(() -> { + // We are on the handler thread: mDhcpServerStartIndex can be read safely. + if (mStartIndex != mDhcpServerStartIndex) { + // This start request is obsolete. When the |server| binder token goes out of + // scope, the garbage collector will finalize it, which causes the network stack + // process garbage collector to collect the server itself. + return; + } + + if (statusCode != STATUS_SUCCESS) { + mLog.e("Error obtaining DHCP server: " + statusCode); + handleError(); + return; + } + + mDhcpServer = server; + try { + mDhcpServer.start(new OnHandlerStatusCallback() { + @Override + public void callback(int startStatusCode) { + if (startStatusCode != STATUS_SUCCESS) { + mLog.e("Error starting DHCP server: " + startStatusCode); + handleError(); + } + } + }); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + }); + } + + private void handleError() { + mLastError = ConnectivityManager.TETHER_ERROR_DHCPSERVER_ERROR; + transitionTo(mInitialState); + } + } + private boolean startDhcp(Inet4Address addr, int prefixLen) { if (mUsingLegacyDhcp) { return true; } - final DhcpServingParams params; - try { - params = new DhcpServingParams.Builder() - .setDefaultRouters(addr) - .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS) - .setDnsServers(addr) - .setServerAddr(new LinkAddress(addr, prefixLen)) - .setMetered(true) - .build(); - // TODO: also advertise link MTU - } catch (DhcpServingParams.InvalidParameterException e) { - Log.e(TAG, "Invalid DHCP parameters", e); - return false; - } - - mDhcpServer = mDeps.makeDhcpServer(getHandler().getLooper(), mIfaceName, params, - mLog.forSubComponent("DHCP")); - mDhcpServer.start(); + final DhcpServingParamsParcel params; + params = new DhcpServingParamsParcelExt() + .setDefaultRouters(addr) + .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS) + .setDnsServers(addr) + .setServerAddr(new LinkAddress(addr, prefixLen)) + .setMetered(true); + // TODO: also advertise link MTU + + mDhcpServerStartIndex++; + mDeps.makeDhcpServer( + mIfaceName, params, new DhcpServerCallbacksImpl(mDhcpServerStartIndex)); return true; } private void stopDhcp() { + // Make all previous start requests obsolete so servers are not started later + mDhcpServerStartIndex++; + if (mDhcpServer != null) { - mDhcpServer.stop(); - mDhcpServer = null; + try { + mDhcpServer.stop(new OnHandlerStatusCallback() { + @Override + public void callback(int statusCode) { + if (statusCode != STATUS_SUCCESS) { + mLog.e("Error stopping DHCP server: " + statusCode); + mLastError = ConnectivityManager.TETHER_ERROR_DHCPSERVER_ERROR; + // Not much more we can do here + } + } + }); + mDhcpServer = null; + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } } } diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java index 3defe56939f5..c183b81362dc 100644 --- a/services/net/java/android/net/util/NetworkConstants.java +++ b/services/net/java/android/net/util/NetworkConstants.java @@ -16,9 +16,6 @@ package android.net.util; -import java.nio.ByteBuffer; - - /** * Networking protocol constants. * @@ -81,8 +78,6 @@ public final class NetworkConstants { * - 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/services/net/java/android/net/util/SharedLog.java b/services/net/java/android/net/util/SharedLog.java index 74bc1470293f..8b7b59d20978 100644 --- a/services/net/java/android/net/util/SharedLog.java +++ b/services/net/java/android/net/util/SharedLog.java @@ -32,6 +32,7 @@ import java.util.StringJoiner; * * All access to class methods other than dump() must be on the same thread. * + * TODO: this is a copy of SharedLog in the NetworkStack. Remove after Tethering is migrated. * @hide */ public class SharedLog { diff --git a/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java b/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java deleted file mode 100644 index ba0448c98387..000000000000 --- a/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java +++ /dev/null @@ -1,539 +0,0 @@ -/* - * 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.DhcpLease.HOSTNAME_NONE; -import static android.net.dhcp.DhcpLeaseRepository.CLIENTID_UNSPEC; -import static android.net.dhcp.DhcpLeaseRepository.INETADDR_UNSPEC; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.when; - -import static java.lang.String.format; -import static java.net.InetAddress.parseNumericAddress; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.net.IpPrefix; -import android.net.MacAddress; -import android.net.dhcp.DhcpServer.Clock; -import android.net.util.SharedLog; -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 org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -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 DhcpLeaseRepositoryTest { - private static final Inet4Address INET4_ANY = (Inet4Address) Inet4Address.ANY; - private static final Inet4Address TEST_DEF_ROUTER = parseAddr4("192.168.42.247"); - private static final Inet4Address TEST_SERVER_ADDR = parseAddr4("192.168.42.241"); - private static final Inet4Address TEST_RESERVED_ADDR = parseAddr4("192.168.42.243"); - private static final MacAddress TEST_MAC_1 = MacAddress.fromBytes( - new byte[] { 5, 4, 3, 2, 1, 0 }); - private static final MacAddress TEST_MAC_2 = MacAddress.fromBytes( - new byte[] { 0, 1, 2, 3, 4, 5 }); - private static final MacAddress TEST_MAC_3 = MacAddress.fromBytes( - new byte[] { 0, 1, 2, 3, 4, 6 }); - private static final Inet4Address TEST_INETADDR_1 = parseAddr4("192.168.42.248"); - private static final Inet4Address TEST_INETADDR_2 = parseAddr4("192.168.42.249"); - private static final String TEST_HOSTNAME_1 = "hostname1"; - private static final String TEST_HOSTNAME_2 = "hostname2"; - private static final IpPrefix TEST_IP_PREFIX = new IpPrefix(TEST_SERVER_ADDR, 22); - private static final long TEST_TIME = 100L; - private static final int TEST_LEASE_TIME_MS = 3_600_000; - private static final Set TEST_EXCL_SET = - Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - TEST_SERVER_ADDR, TEST_DEF_ROUTER, TEST_RESERVED_ADDR))); - - @NonNull - private SharedLog mLog; - @NonNull @Mock - private Clock mClock; - @NonNull - private DhcpLeaseRepository mRepo; - - private static Inet4Address parseAddr4(String inet4Addr) { - return (Inet4Address) parseNumericAddress(inet4Addr); - } - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mLog = new SharedLog("DhcpLeaseRepositoryTest"); - when(mClock.elapsedRealtime()).thenReturn(TEST_TIME); - mRepo = new DhcpLeaseRepository( - TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, mLog, mClock); - } - - /** - * Request a number of addresses through offer/request. Useful to test address exhaustion. - * @param nAddr Number of addresses to request. - */ - private void requestAddresses(byte nAddr) throws Exception { - final HashSet addrs = new HashSet<>(); - byte[] hwAddrBytes = new byte[] { 8, 4, 3, 2, 1, 0 }; - for (byte i = 0; i < nAddr; i++) { - hwAddrBytes[5] = i; - MacAddress newMac = MacAddress.fromBytes(hwAddrBytes); - final String hostname = "host_" + i; - final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, newMac, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, hostname); - - assertNotNull(lease); - assertEquals(newMac, lease.getHwAddr()); - assertEquals(hostname, lease.getHostname()); - assertTrue(format("Duplicate address allocated: %s in %s", lease.getNetAddr(), addrs), - addrs.add(lease.getNetAddr())); - - requestLeaseSelecting(newMac, lease.getNetAddr(), hostname); - } - } - - @Test - public void testAddressExhaustion() throws Exception { - // Use a /28 to quickly run out of addresses - mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS); - - // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses - requestAddresses((byte) 11); - - try { - mRepo.getOffer(null, TEST_MAC_2, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - fail("Should be out of addresses"); - } catch (DhcpLeaseRepository.OutOfAddressesException e) { - // Expected - } - } - - @Test - public void testUpdateParams_LeaseCleanup() throws Exception { - // Inside /28: - final Inet4Address reqAddrIn28 = parseAddr4("192.168.42.242"); - final Inet4Address declinedAddrIn28 = parseAddr4("192.168.42.245"); - - // Inside /28, but not available there (first address of the range) - final Inet4Address declinedFirstAddrIn28 = parseAddr4("192.168.42.240"); - - final DhcpLease reqAddrIn28Lease = requestLeaseSelecting(TEST_MAC_1, reqAddrIn28); - mRepo.markLeaseDeclined(declinedAddrIn28); - mRepo.markLeaseDeclined(declinedFirstAddrIn28); - - // Inside /22, but outside /28: - final Inet4Address reqAddrIn22 = parseAddr4("192.168.42.3"); - final Inet4Address declinedAddrIn22 = parseAddr4("192.168.42.4"); - - final DhcpLease reqAddrIn22Lease = requestLeaseSelecting(TEST_MAC_3, reqAddrIn22); - mRepo.markLeaseDeclined(declinedAddrIn22); - - // Address that will be reserved in the updateParams call below - final Inet4Address reservedAddr = parseAddr4("192.168.42.244"); - final DhcpLease reservedAddrLease = requestLeaseSelecting(TEST_MAC_2, reservedAddr); - - // Update from /22 to /28 and add another reserved address - Set newReserved = new HashSet<>(TEST_EXCL_SET); - newReserved.add(reservedAddr); - mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), newReserved, TEST_LEASE_TIME_MS); - - assertHasLease(reqAddrIn28Lease); - assertDeclined(declinedAddrIn28); - - assertNotDeclined(declinedFirstAddrIn28); - - assertNoLease(reqAddrIn22Lease); - assertNotDeclined(declinedAddrIn22); - - assertNoLease(reservedAddrLease); - } - - @Test - public void testGetOffer_StableAddress() throws Exception { - for (final MacAddress macAddr : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) { - final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - - // Same lease is offered twice - final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - assertEquals(lease, newLease); - } - } - - @Test - public void testUpdateParams_UsesNewPrefix() throws Exception { - final IpPrefix newPrefix = new IpPrefix(parseAddr4("192.168.123.0"), 24); - mRepo.updateParams(newPrefix, TEST_EXCL_SET, TEST_LEASE_TIME_MS); - - DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - assertTrue(newPrefix.contains(lease.getNetAddr())); - } - - @Test - public void testGetOffer_ExistingLease() throws Exception { - requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, TEST_HOSTNAME_1); - - DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - assertEquals(TEST_INETADDR_1, offer.getNetAddr()); - assertEquals(TEST_HOSTNAME_1, offer.getHostname()); - } - - @Test - public void testGetOffer_ClientIdHasExistingLease() throws Exception { - final byte[] clientId = new byte[] { 1, 2 }; - mRepo.requestLease(clientId, TEST_MAC_1, INET4_ANY /* clientAddr */, - INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1); - - // Different MAC, but same clientId - DhcpLease offer = mRepo.getOffer(clientId, TEST_MAC_2, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - assertEquals(TEST_INETADDR_1, offer.getNetAddr()); - assertEquals(TEST_HOSTNAME_1, offer.getHostname()); - } - - @Test - public void testGetOffer_DifferentClientId() throws Exception { - final byte[] clientId1 = new byte[] { 1, 2 }; - final byte[] clientId2 = new byte[] { 3, 4 }; - mRepo.requestLease(clientId1, TEST_MAC_1, INET4_ANY /* clientAddr */, - INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1); - - // Same MAC, different client ID - DhcpLease offer = mRepo.getOffer(clientId2, TEST_MAC_1, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - // Obtains a different address - assertNotEquals(TEST_INETADDR_1, offer.getNetAddr()); - assertEquals(HOSTNAME_NONE, offer.getHostname()); - assertEquals(TEST_MAC_1, offer.getHwAddr()); - } - - @Test - public void testGetOffer_RequestedAddress() throws Exception { - DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, - TEST_INETADDR_1 /* reqAddr */, TEST_HOSTNAME_1); - assertEquals(TEST_INETADDR_1, offer.getNetAddr()); - assertEquals(TEST_HOSTNAME_1, offer.getHostname()); - } - - @Test - public void testGetOffer_RequestedAddressInUse() throws Exception { - requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); - DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY /* relayAddr */, - TEST_INETADDR_1 /* reqAddr */, HOSTNAME_NONE); - assertNotEquals(TEST_INETADDR_1, offer.getNetAddr()); - } - - @Test - public void testGetOffer_RequestedAddressReserved() throws Exception { - DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, - TEST_RESERVED_ADDR /* reqAddr */, HOSTNAME_NONE); - assertNotEquals(TEST_RESERVED_ADDR, offer.getNetAddr()); - } - - @Test - public void testGetOffer_RequestedAddressInvalid() throws Exception { - final Inet4Address invalidAddr = parseAddr4("192.168.42.0"); - DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, - invalidAddr /* reqAddr */, HOSTNAME_NONE); - assertNotEquals(invalidAddr, offer.getNetAddr()); - } - - @Test - public void testGetOffer_RequestedAddressOutsideSubnet() throws Exception { - final Inet4Address invalidAddr = parseAddr4("192.168.254.2"); - DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, - invalidAddr /* reqAddr */, HOSTNAME_NONE); - assertNotEquals(invalidAddr, offer.getNetAddr()); - } - - @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class) - public void testGetOffer_RelayInInvalidSubnet() throws Exception { - mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* relayAddr */, - INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - } - - @Test - public void testRequestLease_SelectingTwice() throws Exception { - final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, - TEST_HOSTNAME_1); - - // Second request from same client for a different address - final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_2, - TEST_HOSTNAME_2); - - assertEquals(TEST_INETADDR_1, lease1.getNetAddr()); - assertEquals(TEST_HOSTNAME_1, lease1.getHostname()); - - assertEquals(TEST_INETADDR_2, lease2.getNetAddr()); - assertEquals(TEST_HOSTNAME_2, lease2.getHostname()); - - // First address freed when client requested a different one: another client can request it - final DhcpLease lease3 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1, HOSTNAME_NONE); - assertEquals(TEST_INETADDR_1, lease3.getNetAddr()); - } - - @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) - public void testRequestLease_SelectingInvalid() throws Exception { - requestLeaseSelecting(TEST_MAC_1, parseAddr4("192.168.254.5")); - } - - @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) - public void testRequestLease_SelectingInUse() throws Exception { - requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); - requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); - } - - @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) - public void testRequestLease_SelectingReserved() throws Exception { - requestLeaseSelecting(TEST_MAC_1, TEST_RESERVED_ADDR); - } - - @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class) - public void testRequestLease_SelectingRelayInInvalidSubnet() throws Exception { - mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* clientAddr */, - parseAddr4("192.168.128.1") /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, - true /* sidSet */, HOSTNAME_NONE); - } - - @Test - public void testRequestLease_InitReboot() throws Exception { - // Request address once - requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); - - final long newTime = TEST_TIME + 100; - when(mClock.elapsedRealtime()).thenReturn(newTime); - - // init-reboot (sidSet == false): verify configuration - final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_1); - assertEquals(TEST_INETADDR_1, lease.getNetAddr()); - assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); - } - - @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) - public void testRequestLease_InitRebootWrongAddr() throws Exception { - // Request address once - requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); - // init-reboot with different requested address - requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2); - } - - @Test - public void testRequestLease_InitRebootUnknownAddr() throws Exception { - // init-reboot with unknown requested address - final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2); - // RFC2131 says we should not reply to accommodate other servers, but since we are - // authoritative we allow creating the lease to avoid issues with lost lease DB (same as - // dnsmasq behavior) - assertEquals(TEST_INETADDR_2, lease.getNetAddr()); - } - - @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) - public void testRequestLease_InitRebootWrongSubnet() throws Exception { - requestLeaseInitReboot(TEST_MAC_1, parseAddr4("192.168.254.2")); - } - - @Test - public void testRequestLease_Renewing() throws Exception { - requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); - - final long newTime = TEST_TIME + 100; - when(mClock.elapsedRealtime()).thenReturn(newTime); - - final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); - - assertEquals(TEST_INETADDR_1, lease.getNetAddr()); - assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); - } - - @Test - public void testRequestLease_RenewingUnknownAddr() throws Exception { - final long newTime = TEST_TIME + 100; - when(mClock.elapsedRealtime()).thenReturn(newTime); - final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); - // Allows renewing an unknown address if available - assertEquals(TEST_INETADDR_1, lease.getNetAddr()); - assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); - } - - @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) - public void testRequestLease_RenewingAddrInUse() throws Exception { - requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); - requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); - } - - @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) - public void testRequestLease_RenewingInvalidAddr() throws Exception { - requestLeaseRenewing(TEST_MAC_1, parseAddr4("192.168.254.2")); - } - - @Test - public void testReleaseLease() throws Exception { - final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); - - assertHasLease(lease1); - assertTrue(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); - assertNoLease(lease1); - - final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); - assertEquals(TEST_INETADDR_1, lease2.getNetAddr()); - } - - @Test - public void testReleaseLease_UnknownLease() { - assertFalse(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); - } - - @Test - public void testReleaseLease_StableOffer() throws Exception { - for (MacAddress mac : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) { - final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, mac, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - - requestLeaseSelecting(mac, lease.getNetAddr()); - mRepo.releaseLease(CLIENTID_UNSPEC, mac, lease.getNetAddr()); - - // Same lease is offered after it was released - final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, mac, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - assertEquals(lease.getNetAddr(), newLease.getNetAddr()); - } - } - - @Test - public void testMarkLeaseDeclined() throws Exception { - final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - - mRepo.markLeaseDeclined(lease.getNetAddr()); - - // Same lease is not offered again - final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - assertNotEquals(lease.getNetAddr(), newLease.getNetAddr()); - } - - @Test - public void testMarkLeaseDeclined_UsedIfOutOfAddresses() throws Exception { - // Use a /28 to quickly run out of addresses - mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS); - - mRepo.markLeaseDeclined(TEST_INETADDR_1); - mRepo.markLeaseDeclined(TEST_INETADDR_2); - - // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses - requestAddresses((byte) 9); - - // Last 2 addresses: addresses marked declined should be used - final DhcpLease firstLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_1); - requestLeaseSelecting(TEST_MAC_1, firstLease.getNetAddr()); - - final DhcpLease secondLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, - INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_2); - requestLeaseSelecting(TEST_MAC_2, secondLease.getNetAddr()); - - // Now out of addresses - try { - mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_3, INET4_ANY /* relayAddr */, - INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); - fail("Repository should be out of addresses and throw"); - } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ } - - assertEquals(TEST_INETADDR_1, firstLease.getNetAddr()); - assertEquals(TEST_HOSTNAME_1, firstLease.getHostname()); - assertEquals(TEST_INETADDR_2, secondLease.getNetAddr()); - assertEquals(TEST_HOSTNAME_2, secondLease.getHostname()); - } - - private DhcpLease requestLease(@NonNull MacAddress macAddr, @NonNull Inet4Address clientAddr, - @Nullable Inet4Address reqAddr, @Nullable String hostname, boolean sidSet) - throws DhcpLeaseRepository.DhcpLeaseException { - return mRepo.requestLease(CLIENTID_UNSPEC, macAddr, clientAddr, INET4_ANY /* relayAddr */, - reqAddr, sidSet, hostname); - } - - /** - * Request a lease simulating a client in the SELECTING state. - */ - private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr, - @NonNull Inet4Address reqAddr, @Nullable String hostname) - throws DhcpLeaseRepository.DhcpLeaseException { - return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, hostname, - true /* sidSet */); - } - - /** - * Request a lease simulating a client in the SELECTING state. - */ - private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr, - @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException { - return requestLeaseSelecting(macAddr, reqAddr, HOSTNAME_NONE); - } - - /** - * Request a lease simulating a client in the INIT-REBOOT state. - */ - private DhcpLease requestLeaseInitReboot(@NonNull MacAddress macAddr, - @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException { - return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, HOSTNAME_NONE, - false /* sidSet */); - } - - /** - * Request a lease simulating a client in the RENEWING state. - */ - private DhcpLease requestLeaseRenewing(@NonNull MacAddress macAddr, - @NonNull Inet4Address clientAddr) throws DhcpLeaseRepository.DhcpLeaseException { - // Renewing: clientAddr filled in, no reqAddr - return requestLease(macAddr, clientAddr, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE, - true /* sidSet */); - } - - private void assertNoLease(DhcpLease lease) { - assertFalse("Leases contain " + lease, mRepo.getCommittedLeases().contains(lease)); - } - - private void assertHasLease(DhcpLease lease) { - assertTrue("Leases do not contain " + lease, mRepo.getCommittedLeases().contains(lease)); - } - - private void assertNotDeclined(Inet4Address addr) { - assertFalse("Address is declined: " + addr, mRepo.getDeclinedAddresses().contains(addr)); - } - - private void assertDeclined(Inet4Address addr) { - assertTrue("Address is not declined: " + addr, mRepo.getDeclinedAddresses().contains(addr)); - } -} diff --git a/tests/net/java/android/net/dhcp/DhcpServerTest.java b/tests/net/java/android/net/dhcp/DhcpServerTest.java deleted file mode 100644 index ab9bd84b05cb..000000000000 --- a/tests/net/java/android/net/dhcp/DhcpServerTest.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * 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.DhcpPacket.DHCP_CLIENT; -import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME; -import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP; -import static android.net.dhcp.DhcpPacket.INADDR_ANY; -import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import static java.net.InetAddress.parseNumericAddress; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.net.LinkAddress; -import android.net.MacAddress; -import android.net.dhcp.DhcpLeaseRepository.InvalidAddressException; -import android.net.dhcp.DhcpLeaseRepository.OutOfAddressesException; -import android.net.dhcp.DhcpServer.Clock; -import android.net.dhcp.DhcpServer.Dependencies; -import android.net.util.SharedLog; -import android.os.test.TestLooper; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.net.Inet4Address; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class DhcpServerTest { - private static final String PROP_DEXMAKER_SHARE_CLASSLOADER = "dexmaker.share_classloader"; - private static final String TEST_IFACE = "testiface"; - - private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2"); - private static final LinkAddress TEST_SERVER_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20); - private static final Set TEST_DEFAULT_ROUTERS = new HashSet<>( - Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124"))); - private static final Set TEST_DNS_SERVERS = new HashSet<>( - Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127"))); - private static final Set TEST_EXCLUDED_ADDRS = new HashSet<>( - Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201"))); - private static final long TEST_LEASE_TIME_SECS = 3600L; - private static final int TEST_MTU = 1500; - private static final String TEST_HOSTNAME = "testhostname"; - - private static final int TEST_TRANSACTION_ID = 123; - private static final byte[] TEST_CLIENT_MAC_BYTES = new byte [] { 1, 2, 3, 4, 5, 6 }; - private static final MacAddress TEST_CLIENT_MAC = MacAddress.fromBytes(TEST_CLIENT_MAC_BYTES); - private static final Inet4Address TEST_CLIENT_ADDR = parseAddr("192.168.0.42"); - - private static final long TEST_CLOCK_TIME = 1234L; - private static final int TEST_LEASE_EXPTIME_SECS = 3600; - private static final DhcpLease TEST_LEASE = new DhcpLease(null, TEST_CLIENT_MAC, - TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME, - null /* hostname */); - private static final DhcpLease TEST_LEASE_WITH_HOSTNAME = new DhcpLease(null, TEST_CLIENT_MAC, - TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME, TEST_HOSTNAME); - - @NonNull @Mock - private Dependencies mDeps; - @NonNull @Mock - private DhcpLeaseRepository mRepository; - @NonNull @Mock - private Clock mClock; - @NonNull @Mock - private DhcpPacketListener mPacketListener; - - @NonNull @Captor - private ArgumentCaptor mSentPacketCaptor; - @NonNull @Captor - private ArgumentCaptor mResponseDstAddrCaptor; - - @NonNull - private TestLooper mLooper; - @NonNull - private DhcpServer mServer; - - @Nullable - private String mPrevShareClassloaderProp; - - @Before - public void setUp() throws Exception { - // Allow mocking package-private classes - mPrevShareClassloaderProp = System.getProperty(PROP_DEXMAKER_SHARE_CLASSLOADER); - System.setProperty(PROP_DEXMAKER_SHARE_CLASSLOADER, "true"); - MockitoAnnotations.initMocks(this); - - when(mDeps.makeLeaseRepository(any(), any(), any())).thenReturn(mRepository); - when(mDeps.makeClock()).thenReturn(mClock); - when(mDeps.makePacketListener()).thenReturn(mPacketListener); - doNothing().when(mDeps) - .sendPacket(any(), mSentPacketCaptor.capture(), mResponseDstAddrCaptor.capture()); - when(mClock.elapsedRealtime()).thenReturn(TEST_CLOCK_TIME); - - final DhcpServingParams servingParams = new DhcpServingParams.Builder() - .setDefaultRouters(TEST_DEFAULT_ROUTERS) - .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS) - .setDnsServers(TEST_DNS_SERVERS) - .setServerAddr(TEST_SERVER_LINKADDR) - .setLinkMtu(TEST_MTU) - .setExcludedAddrs(TEST_EXCLUDED_ADDRS) - .build(); - - mLooper = new TestLooper(); - mServer = new DhcpServer(mLooper.getLooper(), TEST_IFACE, servingParams, - new SharedLog(DhcpServerTest.class.getSimpleName()), mDeps); - - mServer.start(); - mLooper.dispatchAll(); - } - - @After - public void tearDown() { - // Calling stop() several times is not an issue - mServer.stop(); - System.setProperty(PROP_DEXMAKER_SHARE_CLASSLOADER, - (mPrevShareClassloaderProp == null ? "" : mPrevShareClassloaderProp)); - } - - @Test - public void testStart() throws Exception { - verify(mPacketListener, times(1)).start(); - } - - @Test - public void testStop() throws Exception { - mServer.stop(); - mLooper.dispatchAll(); - verify(mPacketListener, times(1)).stop(); - } - - @Test - public void testDiscover() throws Exception { - // TODO: refactor packet construction to eliminate unnecessary/confusing/duplicate fields - when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC), - eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */)) - .thenReturn(TEST_LEASE); - - final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID, - (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES, - false /* broadcast */, INADDR_ANY /* srcIp */); - mServer.processPacket(discover, DHCP_CLIENT); - - assertResponseSentTo(TEST_CLIENT_ADDR); - final DhcpOfferPacket packet = assertOffer(getPacket()); - assertMatchesTestLease(packet); - } - - @Test - public void testDiscover_OutOfAddresses() throws Exception { - when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC), - eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */)) - .thenThrow(new OutOfAddressesException("Test exception")); - - final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID, - (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES, - false /* broadcast */, INADDR_ANY /* srcIp */); - mServer.processPacket(discover, DHCP_CLIENT); - - assertResponseSentTo(INADDR_BROADCAST); - final DhcpNakPacket packet = assertNak(getPacket()); - assertMatchesClient(packet); - } - - private DhcpRequestPacket makeRequestSelectingPacket() { - final DhcpRequestPacket request = new DhcpRequestPacket(TEST_TRANSACTION_ID, - (short) 0 /* secs */, INADDR_ANY /* clientIp */, INADDR_ANY /* relayIp */, - TEST_CLIENT_MAC_BYTES, false /* broadcast */); - request.mServerIdentifier = TEST_SERVER_ADDR; - request.mRequestedIp = TEST_CLIENT_ADDR; - return request; - } - - @Test - public void testRequest_Selecting_Ack() throws Exception { - when(mRepository.requestLease(isNull() /* clientId */, eq(TEST_CLIENT_MAC), - eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */, - eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, eq(TEST_HOSTNAME))) - .thenReturn(TEST_LEASE_WITH_HOSTNAME); - - final DhcpRequestPacket request = makeRequestSelectingPacket(); - request.mHostName = TEST_HOSTNAME; - request.mRequestedParams = new byte[] { DHCP_HOST_NAME }; - mServer.processPacket(request, DHCP_CLIENT); - - assertResponseSentTo(TEST_CLIENT_ADDR); - final DhcpAckPacket packet = assertAck(getPacket()); - assertMatchesTestLease(packet, TEST_HOSTNAME); - } - - @Test - public void testRequest_Selecting_Nak() throws Exception { - when(mRepository.requestLease(isNull(), eq(TEST_CLIENT_MAC), - eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */, - eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, isNull() /* hostname */)) - .thenThrow(new InvalidAddressException("Test error")); - - final DhcpRequestPacket request = makeRequestSelectingPacket(); - mServer.processPacket(request, DHCP_CLIENT); - - assertResponseSentTo(INADDR_BROADCAST); - final DhcpNakPacket packet = assertNak(getPacket()); - assertMatchesClient(packet); - } - - @Test - public void testRequest_Selecting_WrongClientPort() throws Exception { - final DhcpRequestPacket request = makeRequestSelectingPacket(); - mServer.processPacket(request, 50000); - - verify(mRepository, never()) - .requestLease(any(), any(), any(), any(), any(), anyBoolean(), any()); - verify(mDeps, never()).sendPacket(any(), any(), any()); - } - - @Test - public void testRelease() throws Exception { - final DhcpReleasePacket release = new DhcpReleasePacket(TEST_TRANSACTION_ID, - TEST_SERVER_ADDR, TEST_CLIENT_ADDR, - INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES); - mServer.processPacket(release, DHCP_CLIENT); - - verify(mRepository, times(1)) - .releaseLease(isNull(), eq(TEST_CLIENT_MAC), eq(TEST_CLIENT_ADDR)); - } - - /* TODO: add more tests once packet construction is refactored, including: - * - usage of giaddr - * - usage of broadcast bit - * - other request states (init-reboot/renewing/rebinding) - */ - - private void assertMatchesTestLease(@NonNull DhcpPacket packet, @Nullable String hostname) { - assertMatchesClient(packet); - assertFalse(packet.hasExplicitClientId()); - assertEquals(TEST_SERVER_ADDR, packet.mServerIdentifier); - assertEquals(TEST_CLIENT_ADDR, packet.mYourIp); - assertNotNull(packet.mLeaseTime); - assertEquals(TEST_LEASE_EXPTIME_SECS, (int) packet.mLeaseTime); - assertEquals(hostname, packet.mHostName); - } - - private void assertMatchesTestLease(@NonNull DhcpPacket packet) { - assertMatchesTestLease(packet, null); - } - - private void assertMatchesClient(@NonNull DhcpPacket packet) { - assertEquals(TEST_TRANSACTION_ID, packet.mTransId); - assertEquals(TEST_CLIENT_MAC, MacAddress.fromBytes(packet.mClientMac)); - } - - private void assertResponseSentTo(@NonNull Inet4Address addr) { - assertEquals(addr, mResponseDstAddrCaptor.getValue()); - } - - private static DhcpNakPacket assertNak(@Nullable DhcpPacket packet) { - assertTrue(packet instanceof DhcpNakPacket); - return (DhcpNakPacket) packet; - } - - private static DhcpAckPacket assertAck(@Nullable DhcpPacket packet) { - assertTrue(packet instanceof DhcpAckPacket); - return (DhcpAckPacket) packet; - } - - private static DhcpOfferPacket assertOffer(@Nullable DhcpPacket packet) { - assertTrue(packet instanceof DhcpOfferPacket); - return (DhcpOfferPacket) packet; - } - - private DhcpPacket getPacket() throws Exception { - verify(mDeps, times(1)).sendPacket(any(), any(), any()); - return DhcpPacket.decodeFullPacket(mSentPacketCaptor.getValue(), ENCAP_BOOTP); - } - - private static Inet4Address parseAddr(@Nullable String inet4Addr) { - return (Inet4Address) parseNumericAddress(inet4Addr); - } -} diff --git a/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java b/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java deleted file mode 100644 index 2ab224667b8a..000000000000 --- a/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * 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.inet4AddressToIntHTH; -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.NetworkUtils; -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.lang.reflect.Modifier; -import java.net.Inet4Address; -import java.util.Arrays; -import java.util.Collection; -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"))); - private static final boolean TEST_METERED = true; - - @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) - .setMetered(TEST_METERED); - } - - @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); - assertEquals(TEST_METERED, params.metered); - - 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(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(parseAddr("192.168.254.254")).build(); - } - - @Test - public void testFromParcelableObject() throws InvalidParameterException { - final DhcpServingParams params = mBuilder.build(); - final DhcpServingParamsParcel parcel = new DhcpServingParamsParcel(); - parcel.defaultRouters = toIntArray(TEST_DEFAULT_ROUTERS); - parcel.dhcpLeaseTimeSecs = TEST_LEASE_TIME_SECS; - parcel.dnsServers = toIntArray(TEST_DNS_SERVERS); - parcel.serverAddr = inet4AddressToIntHTH(TEST_SERVER_ADDR); - parcel.serverAddrPrefixLength = TEST_LINKADDR.getPrefixLength(); - parcel.linkMtu = TEST_MTU; - parcel.excludedAddrs = toIntArray(TEST_EXCLUDED_ADDRS); - parcel.metered = TEST_METERED; - final DhcpServingParams parceled = DhcpServingParams.fromParcelableObject(parcel); - - assertEquals(params.defaultRouters, parceled.defaultRouters); - assertEquals(params.dhcpLeaseTimeSecs, parceled.dhcpLeaseTimeSecs); - assertEquals(params.dnsServers, parceled.dnsServers); - assertEquals(params.serverAddr, parceled.serverAddr); - assertEquals(params.linkMtu, parceled.linkMtu); - assertEquals(params.excludedAddrs, parceled.excludedAddrs); - assertEquals(params.metered, parceled.metered); - - // Ensure that we do not miss any field if added in the future - final long numFields = Arrays.stream(DhcpServingParams.class.getDeclaredFields()) - .filter(f -> !Modifier.isStatic(f.getModifiers())) - .count(); - assertEquals(7, numFields); - } - - private static int[] toIntArray(Collection addrs) { - return addrs.stream().mapToInt(NetworkUtils::inet4AddressToIntHTH).toArray(); - } - - 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); - } -} diff --git a/tests/net/java/android/net/ip/IpServerTest.java b/tests/net/java/android/net/ip/IpServerTest.java index 017822896610..c3162af1868d 100644 --- a/tests/net/java/android/net/ip/IpServerTest.java +++ b/tests/net/java/android/net/ip/IpServerTest.java @@ -22,20 +22,26 @@ import static android.net.ConnectivityManager.TETHERING_WIFI; import static android.net.ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR; import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR; import static android.net.ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR; +import static android.net.NetworkUtils.intToInet4AddressHTH; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; import static android.net.ip.IpServer.STATE_AVAILABLE; import static android.net.ip.IpServer.STATE_TETHERED; import static android.net.ip.IpServer.STATE_UNAVAILABLE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -48,8 +54,9 @@ import android.net.LinkAddress; import android.net.LinkProperties; import android.net.MacAddress; import android.net.RouteInfo; -import android.net.dhcp.DhcpServer; -import android.net.dhcp.DhcpServingParams; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpServer; +import android.net.dhcp.IDhcpServerCallbacks; import android.net.util.InterfaceParams; import android.net.util.InterfaceSet; import android.net.util.SharedLog; @@ -82,16 +89,18 @@ public class IpServerTest { private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams( IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */); + private static final int MAKE_DHCPSERVER_TIMEOUT_MS = 1000; + @Mock private INetworkManagementService mNMService; @Mock private INetworkStatsService mStatsService; @Mock private IpServer.Callback mCallback; @Mock private InterfaceConfiguration mInterfaceConfiguration; @Mock private SharedLog mSharedLog; - @Mock private DhcpServer mDhcpServer; + @Mock private IDhcpServer mDhcpServer; @Mock private RouterAdvertisementDaemon mRaDaemon; @Mock private IpServer.Dependencies mDependencies; - @Captor private ArgumentCaptor mDhcpParamsCaptor; + @Captor private ArgumentCaptor mDhcpParamsCaptor; private final TestLooper mLooper = new TestLooper(); private final ArgumentCaptor mLinkPropertiesCaptor = @@ -112,8 +121,18 @@ public class IpServerTest { mLooper.dispatchAll(); reset(mNMService, mStatsService, mCallback); when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration); - when(mDependencies.makeDhcpServer( - any(), any(), mDhcpParamsCaptor.capture(), any())).thenReturn(mDhcpServer); + + doAnswer(inv -> { + final IDhcpServerCallbacks cb = inv.getArgument(2); + new Thread(() -> { + try { + cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer); + } catch (RemoteException e) { + fail(e.getMessage()); + } + }).run(); + return null; + }).when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(), any()); when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon); when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS); @@ -399,21 +418,20 @@ public class IpServerTest { initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */); dispatchTetherConnectionChanged(UPSTREAM_IFACE); - verify(mDependencies, never()).makeDhcpServer(any(), any(), any(), any()); + verify(mDependencies, never()).makeDhcpServer(any(), any(), any()); } - private void assertDhcpStarted(IpPrefix expectedPrefix) { - verify(mDependencies, times(1)).makeDhcpServer( - eq(mLooper.getLooper()), eq(IFACE_NAME), any(), eq(mSharedLog)); - verify(mDhcpServer, times(1)).start(); - final DhcpServingParams params = mDhcpParamsCaptor.getValue(); + private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception { + verify(mDependencies, times(1)).makeDhcpServer(eq(IFACE_NAME), any(), any()); + verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).start(any()); + final DhcpServingParamsParcel params = mDhcpParamsCaptor.getValue(); // Last address byte is random - assertTrue(expectedPrefix.contains(params.serverAddr.getAddress())); - assertEquals(expectedPrefix.getPrefixLength(), params.serverAddr.getPrefixLength()); - assertEquals(1, params.defaultRouters.size()); - assertEquals(params.serverAddr.getAddress(), params.defaultRouters.iterator().next()); - assertEquals(1, params.dnsServers.size()); - assertEquals(params.serverAddr.getAddress(), params.dnsServers.iterator().next()); + assertTrue(expectedPrefix.contains(intToInet4AddressHTH(params.serverAddr))); + assertEquals(expectedPrefix.getPrefixLength(), params.serverAddrPrefixLength); + assertEquals(1, params.defaultRouters.length); + assertEquals(params.serverAddr, params.defaultRouters[0]); + assertEquals(1, params.dnsServers.length); + assertEquals(params.serverAddr, params.dnsServers[0]); assertEquals(DHCP_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs); } @@ -458,7 +476,7 @@ public class IpServerTest { addr4 = addr; break; } - assertTrue("missing IPv4 address", addr4 != null); + assertNotNull("missing IPv4 address", addr4); // Assert the presence of the associated directly connected route. final RouteInfo directlyConnected = new RouteInfo(addr4, null, lp.getInterfaceName()); diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java index e6b43d286a3d..1ea83c2bbb6b 100644 --- a/tests/net/java/com/android/server/connectivity/TetheringTest.java +++ b/tests/net/java/com/android/server/connectivity/TetheringTest.java @@ -27,6 +27,7 @@ import static android.net.ConnectivityManager.TETHERING_USB; import static android.net.ConnectivityManager.TETHERING_WIFI; import static android.net.ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE; import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME; import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE; import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE; @@ -37,6 +38,7 @@ import static android.provider.Settings.Global.TETHER_ENABLE_LEGACY_DHCP_SERVER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Matchers.anyInt; @@ -47,6 +49,8 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -74,8 +78,9 @@ import android.net.NetworkInfo; import android.net.NetworkState; import android.net.NetworkUtils; import android.net.RouteInfo; -import android.net.dhcp.DhcpServer; -import android.net.dhcp.DhcpServingParams; +import android.net.dhcp.DhcpServerCallbacks; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpServer; import android.net.ip.IpServer; import android.net.ip.RouterAdvertisementDaemon; import android.net.util.InterfaceParams; @@ -86,7 +91,6 @@ import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Handler; import android.os.INetworkManagementService; -import android.os.Looper; import android.os.PersistableBundle; import android.os.RemoteException; import android.os.UserHandle; @@ -129,6 +133,8 @@ public class TetheringTest { private static final String TEST_USB_IFNAME = "test_rndis0"; private static final String TEST_WLAN_IFNAME = "test_wlan0"; + private static final int DHCPSERVER_START_TIMEOUT_MS = 1000; + @Mock private ApplicationInfo mApplicationInfo; @Mock private Context mContext; @Mock private INetworkManagementService mNMService; @@ -143,9 +149,11 @@ public class TetheringTest { @Mock private UpstreamNetworkMonitor mUpstreamNetworkMonitor; @Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator; @Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon; - @Mock private DhcpServer mDhcpServer; + @Mock private IDhcpServer mDhcpServer; @Mock private INetd mNetd; + private final MockIpServerDependencies mIpServerDependencies = + spy(new MockIpServerDependencies()); private final MockTetheringDependencies mTetheringDependencies = new MockTetheringDependencies(); @@ -185,6 +193,47 @@ public class TetheringTest { } } + public class MockIpServerDependencies extends IpServer.Dependencies { + MockIpServerDependencies() { + super(null); + } + + @Override + public RouterAdvertisementDaemon getRouterAdvertisementDaemon( + InterfaceParams ifParams) { + return mRouterAdvertisementDaemon; + } + + @Override + public InterfaceParams getInterfaceParams(String ifName) { + assertTrue("Non-mocked interface " + ifName, + ifName.equals(TEST_USB_IFNAME) + || ifName.equals(TEST_WLAN_IFNAME) + || ifName.equals(TEST_MOBILE_IFNAME)); + final String[] ifaces = new String[] { + TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_MOBILE_IFNAME }; + return new InterfaceParams(ifName, ArrayUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET, + MacAddress.ALL_ZEROS_ADDRESS); + } + + @Override + public INetd getNetdService() { + return mNetd; + } + + @Override + public void makeDhcpServer(String ifName, DhcpServingParamsParcel params, + DhcpServerCallbacks cb) { + new Thread(() -> { + try { + cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer); + } catch (RemoteException e) { + fail(e.getMessage()); + } + }).run(); + } + } + public class MockTetheringDependencies extends TetheringDependencies { StateMachine upstreamNetworkMonitorMasterSM; ArrayList ipv6CoordinatorNotifyList; @@ -216,35 +265,8 @@ public class TetheringTest { } @Override - public IpServer.Dependencies getIpServerDependencies() { - return new IpServer.Dependencies() { - @Override - public RouterAdvertisementDaemon getRouterAdvertisementDaemon( - InterfaceParams ifParams) { - return mRouterAdvertisementDaemon; - } - - @Override - public InterfaceParams getInterfaceParams(String ifName) { - final String[] ifaces = new String[] { - TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_MOBILE_IFNAME }; - final int index = ArrayUtils.indexOf(ifaces, ifName); - assertTrue("Non-mocked interface: " + ifName, index >= 0); - return new InterfaceParams(ifName, index + IFINDEX_OFFSET, - MacAddress.ALL_ZEROS_ADDRESS); - } - - @Override - public INetd getNetdService() { - return mNetd; - } - - @Override - public DhcpServer makeDhcpServer(Looper looper, String ifName, - DhcpServingParams params, SharedLog log) { - return mDhcpServer; - } - }; + public IpServer.Dependencies getIpServerDependencies(Context context) { + return mIpServerDependencies; } @Override @@ -546,7 +568,7 @@ public class TetheringTest { sendIPv6TetherUpdates(upstreamState); verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull()); - verify(mDhcpServer, times(1)).start(); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); } @Test @@ -557,7 +579,7 @@ public class TetheringTest { runUsbTethering(upstreamState); sendIPv6TetherUpdates(upstreamState); - verify(mDhcpServer, never()).start(); + verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any()); } @Test @@ -581,7 +603,7 @@ public class TetheringTest { verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); verify(mRouterAdvertisementDaemon, times(1)).start(); - verify(mDhcpServer, times(1)).start(); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); sendIPv6TetherUpdates(upstreamState); verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull()); @@ -595,7 +617,7 @@ public class TetheringTest { verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME); verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); - verify(mDhcpServer, times(1)).start(); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME); @@ -612,7 +634,7 @@ public class TetheringTest { runUsbTethering(upstreamState); verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); - verify(mDhcpServer, times(1)).start(); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); // Then 464xlat comes up @@ -636,7 +658,7 @@ public class TetheringTest { verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); // DHCP not restarted on downstream (still times(1)) - verify(mDhcpServer, times(1)).start(); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); } @Test -- cgit v1.2.3-59-g8ed1b