Provide RRO configuration to send DHCP client hostname option.

Add a configurable option in the RRO which controls whether or not to
send the hostname set in the Settings->About phone->Device name. The
option in RRO is false by default, that means DHCP Request still not
include any hostname by default. Once the option is overlaid and enabled,
the device name after transliteration will be wrote into hostname option.

Bug: 131783527
Test: atest NetworkStackTests NetworkStackIntegrationTests
Test: manual test, create empty APK to overlay the RRO configuration.

Change-Id: I9af0b0d9e7bb526d3a3c1003bb99d0a3d69b1e9e
diff --git a/res/values/config.xml b/res/values/config.xml
index 13ab04e..37a6976 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -46,4 +46,7 @@
     <!-- Set to true if NetworkMonitor needs to load the resource by neighbor mcc when device
          doesn't have a SIM card inserted. -->
     <bool name="config_no_sim_card_uses_neighbor_mcc">false</bool>
+
+    <!-- Configuration for including DHCP client hostname option -->
+    <bool name="config_dhcp_client_hostname">false</bool>
 </resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index 4727215..57c9e7a 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -24,6 +24,16 @@
             <item type="bool" name="config_no_sim_card_uses_neighbor_mcc"/>
             <!-- Configuration value for DhcpResults -->
             <item type="array" name="config_default_dns_servers"/>
+            <!-- Configuration for including DHCP client hostname option.
+            If this option is true, client hostname set in Settings.Global.DEVICE_NAME will be
+            included in DHCPDISCOVER/DHCPREQUEST, otherwise, the DHCP hostname option will not
+            be sent. RFC952 and RFC1123 stipulates an valid hostname should be only comprised of
+            'a-z', 'A-Z' and '-', and the length should be up to 63 octets or less (RFC1035#2.3.4),
+            platform will perform best-effort transliteration for other characters. Anything that
+            could be used to identify the device uniquely is not recommended, e.g. user's name,
+            random number and etc.
+            -->
+            <item type="bool" name="config_dhcp_client_hostname"/>
         </policy>
     </overlayable>
 </resources>
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 982d8ce..b1df996 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -68,6 +68,7 @@
 import android.net.metrics.DhcpClientEvent;
 import android.net.metrics.DhcpErrorEvent;
 import android.net.metrics.IpConnectivityLog;
+import android.net.util.HostnameTransliterator;
 import android.net.util.InterfaceParams;
 import android.net.util.NetworkStackUtils;
 import android.net.util.PacketReader;
@@ -76,6 +77,7 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.SystemClock;
+import android.provider.Settings;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.EventLog;
@@ -306,6 +308,8 @@
     private final NetworkStackIpMemoryStore mIpMemoryStore;
     @Nullable
     private DhcpPacketHandler mDhcpPacketHandler;
+    @Nullable
+    private final String mHostname;
 
     // Milliseconds SystemClock timestamps used to record transition times to DhcpBoundState.
     private long mLastInitEnterTime;
@@ -350,6 +354,22 @@
         }
 
         /**
+         * Get the configuration from RRO to check whether or not to send hostname option in
+         * DHCPDISCOVER/DHCPREQUEST message.
+         */
+        public boolean getSendHostnameOption(final Context context) {
+            return context.getResources().getBoolean(R.bool.config_dhcp_client_hostname);
+        }
+
+        /**
+         * Get the device name from system settings.
+         */
+        public String getDeviceName(final Context context) {
+            return Settings.Global.getString(context.getContentResolver(),
+                    Settings.Global.DEVICE_NAME);
+        }
+
+        /**
          * Get a IpMemoryStore instance.
          */
         public NetworkStackIpMemoryStore getIpMemoryStore() {
@@ -450,6 +470,11 @@
         mRenewAlarm = makeWakeupMessage("RENEW", CMD_RENEW_DHCP);
         mRebindAlarm = makeWakeupMessage("REBIND", CMD_REBIND_DHCP);
         mExpiryAlarm = makeWakeupMessage("EXPIRY", CMD_EXPIRE_DHCP);
+
+        // Transliterate hostname read from system settings if RRO option is enabled.
+        final boolean sendHostname = deps.getSendHostnameOption(context);
+        mHostname = sendHostname ? new HostnameTransliterator().transliterate(
+                deps.getDeviceName(mContext)) : null;
     }
 
     public void registerForPreDhcpNotification() {
@@ -641,7 +666,7 @@
     private boolean sendDiscoverPacket() {
         final ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
                 DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
-                DO_UNICAST, REQUESTED_PARAMS, isDhcpRapidCommitEnabled());
+                DO_UNICAST, REQUESTED_PARAMS, isDhcpRapidCommitEnabled(), mHostname);
         return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
     }
 
@@ -655,7 +680,7 @@
         final ByteBuffer packet = DhcpPacket.buildRequestPacket(
                 encap, mTransactionId, getSecs(), clientAddress,
                 DO_UNICAST, mHwAddr, requestedAddress,
-                serverAddress, REQUESTED_PARAMS, null);
+                serverAddress, REQUESTED_PARAMS, mHostname);
         String serverStr = (serverAddress != null) ? serverAddress.getHostAddress() : null;
         String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
                              " request=" + requestedAddress.getHostAddress() +
@@ -744,7 +769,7 @@
         mDhcpLease = results;
         if (mDhcpLease.dnsServers.isEmpty()) {
             // supplement customized dns servers
-            String[] dnsServersList =
+            final String[] dnsServersList =
                     mContext.getResources().getStringArray(R.array.config_default_dns_servers);
             for (final String dnsServer : dnsServersList) {
                 try {
@@ -1259,7 +1284,7 @@
             final Layer2PacketParcelable l2Packet = new Layer2PacketParcelable();
             final ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
                     DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
-                    DO_UNICAST, REQUESTED_PARAMS, true /* rapid commit */);
+                    DO_UNICAST, REQUESTED_PARAMS, true /* rapid commit */, mHostname);
 
             l2Packet.dstMacAddress = MacAddress.fromBytes(DhcpPacket.ETHER_BROADCAST);
             l2Packet.payload = packet.array();
diff --git a/src/android/net/dhcp/DhcpPacket.java b/src/android/net/dhcp/DhcpPacket.java
index c5700b3..9eca0b5 100644
--- a/src/android/net/dhcp/DhcpPacket.java
+++ b/src/android/net/dhcp/DhcpPacket.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2019 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 com.android.server.util.NetworkStackConstants.IPV4_ADDR_ALL;
@@ -15,6 +31,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.networkstack.apishim.ShimUtils;
+
 import java.io.UnsupportedEncodingException;
 import java.net.Inet4Address;
 import java.net.UnknownHostException;
@@ -350,7 +368,6 @@
     // Set in unit tests, to ensure that the test does not break when run on different devices and
     // on different releases.
     static String testOverrideVendorId = null;
-    static String testOverrideHostname = null;
 
     protected DhcpPacket(int transId, short secs, Inet4Address clientIp, Inet4Address yourIp,
                          Inet4Address nextIp, Inet4Address relayIp,
@@ -724,9 +741,16 @@
         return "android-dhcp-" + Build.VERSION.RELEASE;
     }
 
-    private String getHostname() {
-        if (testOverrideHostname != null) return testOverrideHostname;
-        return SystemProperties.get("net.hostname");
+    /**
+     * Get the DHCP client hostname after transliteration.
+     */
+    @VisibleForTesting
+    public String getHostname() {
+        if (mHostName == null
+                && !ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)) {
+            return SystemProperties.get("net.hostname");
+        }
+        return mHostName;
     }
 
     /**
@@ -1328,10 +1352,11 @@
      */
     public static ByteBuffer buildDiscoverPacket(int encap, int transactionId,
             short secs, byte[] clientMac, boolean broadcast, byte[] expectedParams,
-            boolean rapidCommit) {
+            boolean rapidCommit, String hostname) {
         DhcpPacket pkt = new DhcpDiscoverPacket(transactionId, secs, INADDR_ANY /* relayIp */,
                 clientMac, broadcast, INADDR_ANY /* srcIp */, rapidCommit);
         pkt.mRequestedParams = expectedParams;
+        pkt.mHostName = hostname;
         return pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT);
     }
 
diff --git a/src/android/net/util/HostnameTransliterator.java b/src/android/net/util/HostnameTransliterator.java
new file mode 100644
index 0000000..cf126d1
--- /dev/null
+++ b/src/android/net/util/HostnameTransliterator.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2019 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.icu.text.Transliterator;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Transliterator to display a human-readable DHCP client hostname.
+ */
+public class HostnameTransliterator {
+    private static final String TAG = "HostnameTransliterator";
+
+    // Maximum length of hostname to be encoded in the DHCP message. Following RFC1035#2.3.4
+    // and this transliterator converts the device name to a single label, so the label length
+    // limit applies to the whole hostname.
+    private static final int MAX_DNS_LABEL_LENGTH = 63;
+
+    @Nullable
+    private final Transliterator mTransliterator;
+
+    public HostnameTransliterator() {
+        final Enumeration<String> availableIDs = Transliterator.getAvailableIDs();
+        final Set<String> actualIds = new HashSet<>(Collections.list(availableIDs));
+        final StringBuilder rules = new StringBuilder();
+        if (actualIds.contains("Any-ASCII")) {
+            rules.append(":: Any-ASCII; ");
+        } else if (actualIds.contains("Any-Latin") && actualIds.contains("Latin-ASCII")) {
+            rules.append(":: Any-Latin; :: Latin-ASCII; ");
+        } else {
+            Log.e(TAG, "ICU Transliterator doesn't include supported ID");
+            mTransliterator = null;
+            return;
+        }
+        mTransliterator = Transliterator.createFromRules("", rules.toString(),
+                Transliterator.FORWARD);
+    }
+
+    @VisibleForTesting
+    public HostnameTransliterator(Transliterator transliterator) {
+        mTransliterator = transliterator;
+    }
+
+    // RFC952 and RFC1123 stipulates an valid hostname should be:
+    // 1. Only contain the alphabet (A-Z, a-z), digits (0-9), minus sign (-).
+    // 2. No blank or space characters are permitted as part of a name.
+    // 3. The first character must be an alpha character or digit.
+    // 4. The last character must not be a minus sign (-).
+    private String maybeRemoveRedundantSymbols(@NonNull String string) {
+        String result = string.replaceAll("[^a-zA-Z0-9-]", "-");
+        result = result.replaceAll("-+", "-");
+        if (result.startsWith("-")) {
+            result = result.replaceFirst("-", "");
+        }
+        if (result.endsWith("-")) {
+            result = result.substring(0, result.length() - 1);
+        }
+        return result;
+    }
+
+    /**
+     *  Transliterate the device name to valid hostname that could be human-readable string.
+     */
+    public String transliterate(@NonNull String deviceName) {
+        if (deviceName == null) return null;
+        if (mTransliterator == null) {
+            if (!deviceName.matches("\\p{ASCII}*")) return null;
+            deviceName = maybeRemoveRedundantSymbols(deviceName);
+            if (TextUtils.isEmpty(deviceName)) return null;
+            return deviceName.length() > MAX_DNS_LABEL_LENGTH
+                    ? deviceName.substring(0, MAX_DNS_LABEL_LENGTH) : deviceName;
+        }
+
+        String hostname = maybeRemoveRedundantSymbols(mTransliterator.transliterate(deviceName));
+        if (TextUtils.isEmpty(hostname)) return null;
+        return hostname.length() > MAX_DNS_LABEL_LENGTH
+                ? hostname.substring(0, MAX_DNS_LABEL_LENGTH) : hostname;
+    }
+}
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
index 2ec2efa..33556a5 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
@@ -54,6 +54,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.argThat;
@@ -212,6 +213,8 @@
     private static final int TEST_DEFAULT_MTU = 1500;
     private static final int TEST_MIN_MTU = 1280;
     private static final byte[] SERVER_MAC = new byte[] { 0x00, 0x1A, 0x11, 0x22, 0x33, 0x44 };
+    private static final String TEST_HOST_NAME = "AOSP on Crosshatch";
+    private static final String TEST_HOST_NAME_TRANSLITERATION = "AOSP-on-Crosshatch";
 
     private static class TapPacketReader extends PacketReader {
         private final ParcelFileDescriptor mTapFd;
@@ -260,6 +263,8 @@
         // Can't use SparseIntArray, it doesn't have an easy way to know if a key is not present.
         private HashMap<String, Integer> mIntConfigProperties = new HashMap<>();
         private DhcpClient mDhcpClient;
+        private boolean mIsHostnameConfigurationEnabled;
+        private String mHostname;
 
         public void setDhcpLeaseCacheEnabled(final boolean enable) {
             mIsDhcpLeaseCacheEnabled = enable;
@@ -273,6 +278,11 @@
             mIsDhcpIpConflictDetectEnabled = enable;
         }
 
+        public void setHostnameConfiguration(final boolean enable, final String hostname) {
+            mIsHostnameConfigurationEnabled = enable;
+            mHostname = hostname;
+        }
+
         @Override
         public INetd getNetd(Context context) {
             return mNetd;
@@ -319,6 +329,16 @@
                 public PowerManager.WakeLock getWakeLock(final PowerManager powerManager) {
                     return mTimeoutWakeLock;
                 }
+
+                @Override
+                public boolean getSendHostnameOption(final Context context) {
+                    return mIsHostnameConfigurationEnabled;
+                }
+
+                @Override
+                public String getDeviceName(final Context context) {
+                    return mIsHostnameConfigurationEnabled ? mHostname : null;
+                }
             };
         }
 
@@ -515,7 +535,9 @@
 
     private void startIpClientProvisioning(final boolean isDhcpLeaseCacheEnabled,
             final boolean shouldReplyRapidCommitAck, final boolean isPreconnectionEnabled,
-            final boolean isDhcpIpConflictDetectEnabled) throws RemoteException {
+            final boolean isDhcpIpConflictDetectEnabled,
+            final boolean isHostnameConfigurationEnabled, final String hostname)
+            throws RemoteException {
         ProvisioningConfiguration.Builder builder = new ProvisioningConfiguration.Builder()
                 .withoutIpReachabilityMonitor()
                 .withoutIPv6();
@@ -524,6 +546,7 @@
         mDependencies.setDhcpLeaseCacheEnabled(isDhcpLeaseCacheEnabled);
         mDependencies.setDhcpRapidCommitEnabled(shouldReplyRapidCommitAck);
         mDependencies.setDhcpIpConflictDetectEnabled(isDhcpIpConflictDetectEnabled);
+        mDependencies.setHostnameConfiguration(isHostnameConfigurationEnabled, hostname);
         mIpc.setL2KeyAndGroupHint(TEST_L2KEY, TEST_GROUPHINT);
         mIpc.startProvisioning(builder.build());
         verify(mCb).setNeighborDiscoveryOffload(true);
@@ -533,6 +556,15 @@
         verify(mCb, never()).onProvisioningFailure(any());
     }
 
+    private void startIpClientProvisioning(final boolean isDhcpLeaseCacheEnabled,
+            final boolean isDhcpRapidCommitEnabled, final boolean isPreconnectionEnabled,
+            final boolean isDhcpIpConflictDetectEnabled)
+            throws RemoteException {
+        startIpClientProvisioning(isDhcpLeaseCacheEnabled, isDhcpRapidCommitEnabled,
+                isPreconnectionEnabled, isDhcpIpConflictDetectEnabled,
+                false /* isHostnameConfigurationEnabled */, null /* hostname */);
+    }
+
     private void assertIpMemoryStoreNetworkAttributes(final Integer leaseTimeSec,
             final long startTime, final int mtu) {
         final ArgumentCaptor<NetworkAttributes> networkAttributes =
@@ -560,16 +592,33 @@
         verify(mIpMemoryStore, never()).storeNetworkAttributes(any(), any(), any());
     }
 
+    private void assertHostname(final boolean isHostnameConfigurationEnabled,
+            final String hostname, final String hostnameAfterTransliteration,
+            final List<DhcpPacket> packetList) throws Exception {
+        for (DhcpPacket packet : packetList) {
+            if (!isHostnameConfigurationEnabled || hostname == null) {
+                assertNull(packet.getHostname());
+            } else {
+                assertEquals(packet.getHostname(), hostnameAfterTransliteration);
+            }
+        }
+    }
+
     // Helper method to complete DHCP 2-way or 4-way handshake
-    private void performDhcpHandshake(final boolean isSuccessLease,
+    private List<DhcpPacket> performDhcpHandshake(final boolean isSuccessLease,
             final Integer leaseTimeSec, final boolean isDhcpLeaseCacheEnabled,
             final boolean shouldReplyRapidCommitAck, final int mtu,
-            final boolean isDhcpIpConflictDetectEnabled) throws Exception {
+            final boolean isDhcpIpConflictDetectEnabled,
+            final boolean isHostnameConfigurationEnabled, final String hostname)
+            throws Exception {
+        final List<DhcpPacket> packetList = new ArrayList<DhcpPacket>();
         startIpClientProvisioning(isDhcpLeaseCacheEnabled, shouldReplyRapidCommitAck,
-                false /* isPreconnectionEnabled */, isDhcpIpConflictDetectEnabled);
+                false /* isPreconnectionEnabled */, isDhcpIpConflictDetectEnabled,
+                isHostnameConfigurationEnabled, hostname);
 
         DhcpPacket packet;
         while ((packet = getNextDhcpPacket()) != null) {
+            packetList.add(packet);
             if (packet instanceof DhcpDiscoverPacket) {
                 if (shouldReplyRapidCommitAck) {
                     sendResponse(buildDhcpAckPacket(packet, leaseTimeSec, (short) mtu,
@@ -587,9 +636,21 @@
                 fail("invalid DHCP packet");
             }
             // wait for reply to DHCPOFFER packet if disabling rapid commit option
-            if (shouldReplyRapidCommitAck || !(packet instanceof DhcpDiscoverPacket)) return;
+            if (shouldReplyRapidCommitAck || !(packet instanceof DhcpDiscoverPacket)) {
+                return packetList;
+            }
         }
         fail("No DHCPREQUEST received on interface");
+        return packetList;
+    }
+
+    private List<DhcpPacket> performDhcpHandshake(final boolean isSuccessLease,
+            final Integer leaseTimeSec, final boolean isDhcpLeaseCacheEnabled,
+            final boolean isDhcpRapidCommitEnabled, final int mtu,
+            final boolean isDhcpIpConflictDetectEnabled) throws Exception {
+        return performDhcpHandshake(isSuccessLease, leaseTimeSec, isDhcpLeaseCacheEnabled,
+                isDhcpRapidCommitEnabled, mtu, isDhcpIpConflictDetectEnabled,
+                false /* isHostnameConfigurationEnabled */, null /* hostname */);
     }
 
     private DhcpPacket getNextDhcpPacket() throws ParseException {
@@ -1345,4 +1406,44 @@
                 true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
                 false /* shouldResponseArpReply */);
     }
+
+    @Test
+    public void testHostname_enableConfig() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
+                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
+                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
+                false /* isDhcpIpConflictDetectEnabled */,
+                true /* isHostnameConfigurationEnabled */, TEST_HOST_NAME /* hostname */);
+        assertEquals(2, sentPackets.size());
+        assertHostname(true, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test
+    public void testHostname_disableConfig() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
+                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
+                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
+                false /* isDhcpIpConflictDetectEnabled */,
+                false /* isHostnameConfigurationEnabled */, TEST_HOST_NAME);
+        assertEquals(2, sentPackets.size());
+        assertHostname(false, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
+
+    @Test
+    public void testHostname_enableConfigWithNullHostname() throws Exception {
+        final long currentTime = System.currentTimeMillis();
+        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
+                TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
+                false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
+                false /* isDhcpIpConflictDetectEnabled */,
+                true /* isHostnameConfigurationEnabled */, null /* hostname */);
+        assertEquals(2, sentPackets.size());
+        assertHostname(true, null /* hostname */, null /* hostnameAfterTransliteration */,
+                sentPackets);
+        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
+    }
 }
diff --git a/tests/unit/src/android/net/dhcp/DhcpPacketTest.java b/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
index fcaf655..090631b 100644
--- a/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
@@ -92,7 +92,6 @@
     @Before
     public void setUp() {
         DhcpPacket.testOverrideVendorId = "android-dhcp-???";
-        DhcpPacket.testOverrideHostname = "android-01234567890abcde";
     }
 
     class TestDhcpPacket extends DhcpPacket {
@@ -923,17 +922,19 @@
 
     @Test
     public void testDiscoverPacket() throws Exception {
-        short secs = 7;
-        int transactionId = 0xdeadbeef;
-        byte[] hwaddr = {
+        final short secs = 7;
+        final int transactionId = 0xdeadbeef;
+        final byte[] hwaddr = {
                 (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a
         };
+        final String testHostname = "android-01234567890abcde";
 
         ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
                 DhcpPacket.ENCAP_L2, transactionId, secs, hwaddr,
-                false /* do unicast */, DhcpClient.REQUESTED_PARAMS, false /* rapid commit */);
+                false /* do unicast */, DhcpClient.REQUESTED_PARAMS, false /* rapid commit */,
+                testHostname);
 
-        byte[] headers = new byte[] {
+        final byte[] headers = new byte[] {
             // Ethernet header.
             (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
             (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a,
@@ -958,7 +959,7 @@
             (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b,
             (byte) 0xb1, (byte) 0x7a
         };
-        byte[] options = new byte[] {
+        final byte[] options = new byte[] {
             // Magic cookie 0x63825363.
             (byte) 0x63, (byte) 0x82, (byte) 0x53, (byte) 0x63,
             // Message type DISCOVER.
@@ -993,16 +994,17 @@
             // Our packets are always of even length. TODO: find out why and possibly fix it.
             (byte) 0x00
         };
-        byte[] expected = new byte[DhcpPacket.MIN_PACKET_LENGTH_L2 + options.length];
+        final byte[] expected = new byte[DhcpPacket.MIN_PACKET_LENGTH_L2 + options.length];
         assertTrue((expected.length & 1) == 0);
+        assertEquals(DhcpPacket.MIN_PACKET_LENGTH_L2,
+                headers.length + 10 /* client hw addr padding */ + 64 /* sname */ + 128 /* file */);
         System.arraycopy(headers, 0, expected, 0, headers.length);
         System.arraycopy(options, 0, expected, DhcpPacket.MIN_PACKET_LENGTH_L2, options.length);
 
-        byte[] actual = new byte[packet.limit()];
+        final byte[] actual = new byte[packet.limit()];
         packet.get(actual);
-        String msg =
-                "Expected:\n  " + Arrays.toString(expected) +
-                "\nActual:\n  " + Arrays.toString(actual);
+        String msg = "Expected:\n  " + Arrays.toString(expected) + "\nActual:\n  "
+                + Arrays.toString(actual);
         assertTrue(msg, Arrays.equals(expected, actual));
     }
 
diff --git a/tests/unit/src/android/net/util/HostnameTransliteratorTest.java b/tests/unit/src/android/net/util/HostnameTransliteratorTest.java
new file mode 100644
index 0000000..e6c43d1
--- /dev/null
+++ b/tests/unit/src/android/net/util/HostnameTransliteratorTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Tests for HostnameTransliterator class.
+ *
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class HostnameTransliteratorTest {
+    private static final String TEST_HOST_NAME_EN = "AOSP on Crosshatch";
+    private static final String TEST_HOST_NAME_EN_TRANSLITERATION = "AOSP-on-Crosshatch";
+    private static final String TEST_HOST_NAME_JP = "AOSP on ほげほげ";
+    private static final String TEST_HOST_NAME_JP_TRANSLITERATION = "AOSP-on-hogehoge";
+    private static final String TEST_HOST_NAME_CN = "AOSP on 安卓";
+    private static final String TEST_HOST_NAME_CN_TRANSLITERATION = "AOSP-on-an-zhuo";
+    private static final String TEST_HOST_NAME_UNICODE = "AOSP on àéö→ūçŗ̊œ";
+    private static final String TEST_HOST_NAME_UNICODE_TRANSLITERATION = "AOSP-on-aeo-ucroe";
+    private static final String TEST_HOST_NAME_LONG =
+            "AOSP on Ccccccrrrrrroooooosssssshhhhhhaaaaaattttttcccccchhhhhh3abcd";
+    private static final String TEST_HOST_NAME_LONG_TRANSLITERATION =
+            "AOSP-on-Ccccccrrrrrroooooosssssshhhhhhaaaaaattttttcccccchhhhhh3";
+    private static final String TEST_HOST_NAME_LONG_TRUNCATED =
+            "AOSP-on-Ccccccrrrrrroooooosssssshhhhhhaaaaaattttttcccccchhhhhh3";
+
+    @NonNull
+    private HostnameTransliterator mTransliterator;
+
+    @Before
+    public void setUp() throws Exception {
+        mTransliterator = new HostnameTransliterator();
+        assertNotNull(mTransliterator);
+    }
+
+    @Test
+    public void testNullHostname() {
+        assertNull(mTransliterator.transliterate(null));
+    }
+
+    private void assertHostnameTransliteration(final String hostnameAftertransliteration,
+            final String hostname) {
+        assertEquals(hostnameAftertransliteration, mTransliterator.transliterate(hostname));
+    }
+
+    @Test
+    public void testEmptyHostname() {
+        assertHostnameTransliteration(null, "");
+    }
+
+    @Test
+    public void testHostnameOnlyTabs() {
+        assertHostnameTransliteration(null, "\t\t");
+    }
+
+    @Test
+    public void testHostnameOnlySpaces() {
+        assertHostnameTransliteration(null, "    ");
+    }
+
+    @Test
+    public void testHostnameOnlyUnsupportedAsciiSymbols() {
+        final String symbol = new String(new byte[] { 0x00, 0x1b /* ESC */, 0x7f /* DEL */,
+                0x10 /* backspace */}, StandardCharsets.US_ASCII);
+        assertHostnameTransliteration(null, symbol);
+    }
+
+    @Test
+    public void testHostnameMixedAsciiSymbols() {
+        final String symbol = new String(new byte[] { 0x00, 'a', 0x1b /* ESC */, 0x7f /* DEL */,
+                'b', 0x10 /* backspace */}, StandardCharsets.US_ASCII);
+        assertHostnameTransliteration("a-b", symbol);
+    }
+
+    @Test
+    public void testHostnames() {
+        assertHostnameTransliteration(TEST_HOST_NAME_EN_TRANSLITERATION, TEST_HOST_NAME_EN);
+        assertHostnameTransliteration(TEST_HOST_NAME_JP_TRANSLITERATION, TEST_HOST_NAME_JP);
+        assertHostnameTransliteration(TEST_HOST_NAME_CN_TRANSLITERATION, TEST_HOST_NAME_CN);
+        assertHostnameTransliteration(TEST_HOST_NAME_UNICODE_TRANSLITERATION,
+                TEST_HOST_NAME_UNICODE);
+    }
+
+    @Test
+    public void testHostnameWithMinusSign() {
+        assertHostnameTransliteration(TEST_HOST_NAME_EN_TRANSLITERATION, "-AOSP on Crosshatch");
+        assertHostnameTransliteration(TEST_HOST_NAME_EN_TRANSLITERATION, "AOSP on Crosshatch-");
+        assertHostnameTransliteration(TEST_HOST_NAME_EN_TRANSLITERATION, "---AOSP on Crosshatch");
+        assertHostnameTransliteration(TEST_HOST_NAME_EN_TRANSLITERATION, "AOSP on Crosshatch---");
+        assertHostnameTransliteration(TEST_HOST_NAME_EN_TRANSLITERATION, "AOSP---on---Crosshatch");
+    }
+
+    @Test
+    public void testLongHostname() {
+        assertHostnameTransliteration(TEST_HOST_NAME_LONG_TRANSLITERATION, TEST_HOST_NAME_LONG);
+    }
+
+    @Test
+    public void testNonAsciiHostname() {
+        mTransliterator = new HostnameTransliterator(null);
+        assertHostnameTransliteration(null, TEST_HOST_NAME_UNICODE);
+    }
+
+    @Test
+    public void testAsciiLongHostname() {
+        mTransliterator = new HostnameTransliterator(null);
+        assertHostnameTransliteration(TEST_HOST_NAME_LONG_TRUNCATED, TEST_HOST_NAME_LONG);
+    }
+}