Merge "Merge "Move DhcpServer to NetworkStack app" am: 94e5b22985 am: d495691bd5 am: 4bdc06e174"
diff --git a/Android.bp b/Android.bp
index 93c94e3..f6d49e9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -872,7 +872,10 @@
     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 23e4ec0..436b4a1 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -2485,6 +2485,8 @@
     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 29f8828..be0dc07 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 0000000..51032d8
--- /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 82a4e31..d4a0ec63 100644
--- a/core/java/android/net/NetworkStack.java
+++ b/core/java/android/net/NetworkStack.java
@@ -25,9 +25,12 @@
 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 NetworkStack() { }
 
+    /**
+     * Create a DHCP server according to the specified parameters.
+     *
+     * <p>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 0000000..bb56876
--- /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 0000000..559433b
--- /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 0000000..7ab4dcd
--- /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 55bb517..4688848 100644
--- a/packages/NetworkStack/Android.bp
+++ b/packages/NetworkStack/Android.bp
@@ -22,6 +22,10 @@
     srcs: [
         "src/**/*.java",
     ],
+    static_libs: [
+        "dhcp-packet-lib",
+        "frameworks-net-shared-utils",
+    ]
 }
 
 // Updatable network stack packaged as an application
diff --git a/services/net/java/android/net/dhcp/DhcpLease.java b/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java
similarity index 100%
rename from services/net/java/android/net/dhcp/DhcpLease.java
rename to packages/NetworkStack/src/android/net/dhcp/DhcpLease.java
diff --git a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java b/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java
similarity index 99%
rename from services/net/java/android/net/dhcp/DhcpLeaseRepository.java
rename to packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java
index b3d0512..0d298de 100644
--- a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java
@@ -21,7 +21,8 @@
 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 com.android.server.util.NetworkStackConstants.IPV4_ADDR_BITS;
 
 import static java.lang.Math.min;
 
diff --git a/services/net/java/android/net/dhcp/DhcpPacketListener.java b/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java
similarity index 100%
rename from services/net/java/android/net/dhcp/DhcpPacketListener.java
rename to packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java
diff --git a/services/net/java/android/net/dhcp/DhcpServer.java b/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java
similarity index 85%
rename from services/net/java/android/net/dhcp/DhcpServer.java
rename to packages/NetworkStack/src/android/net/dhcp/DhcpServer.java
index 641bba2..14e2936 100644
--- a/services/net/java/android/net/dhcp/DhcpServer.java
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java
@@ -23,7 +23,8 @@
 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.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;
@@ -32,21 +33,28 @@
 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;
@@ -70,7 +78,7 @@
  * on the looper asynchronously.
  * @hide
  */
-public class DhcpServer {
+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
@@ -82,7 +90,7 @@
     private static final int CMD_UPDATE_PARAMS = 3;
 
     @NonNull
-    private final ServerHandler mHandler;
+    private final HandlerThread mHandlerThread;
     @NonNull
     private final String mIfName;
     @NonNull
@@ -93,10 +101,14 @@
     private final Dependencies mDeps;
     @NonNull
     private final Clock mClock;
-    @NonNull
-    private final DhcpPacketListener mPacketListener;
 
     @Nullable
+    private volatile ServerHandler mHandler;
+
+    // Accessed only on the handler thread
+    @Nullable
+    private DhcpPacketListener mPacketListener;
+    @Nullable
     private FileDescriptor mSocket;
     @NonNull
     private DhcpServingParams mServingParams;
@@ -156,6 +168,12 @@
          */
         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 {
@@ -189,6 +207,11 @@
                 @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 {
@@ -197,41 +220,62 @@
         }
     }
 
-    public DhcpServer(@NonNull Looper looper, @NonNull String ifName,
+    public DhcpServer(@NonNull String ifName,
             @NonNull DhcpServingParams params, @NonNull SharedLog log) {
-        this(looper, ifName, params, log, null);
+        this(new HandlerThread(DhcpServer.class.getSimpleName() + "." + ifName),
+                ifName, params, log, null);
     }
 
     @VisibleForTesting
-    DhcpServer(@NonNull Looper looper, @NonNull String ifName,
+    DhcpServer(@NonNull HandlerThread handlerThread, @NonNull String ifName,
             @NonNull DhcpServingParams params, @NonNull SharedLog log,
             @Nullable Dependencies deps) {
         if (deps == null) {
             deps = new DependenciesImpl();
         }
-        mHandler = new ServerHandler(looper);
+        mHandlerThread = handlerThread;
         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.
+     *
+     * <p>It is not legal to call this method more than once; in particular the server cannot be
+     * restarted after being stopped.
      */
-    public void start() {
-        mHandler.sendEmptyMessage(CMD_START_DHCP_SERVER);
+    @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.
      */
-    public void updateParams(@NonNull DhcpServingParams params) {
-        sendMessage(CMD_UPDATE_PARAMS, params);
+    @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));
     }
 
     /**
@@ -240,11 +284,17 @@
      * <p>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);
+    @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));
     }
 
@@ -255,23 +305,42 @@
 
         @Override
         public void handleMessage(@NonNull Message msg) {
+            final INetworkStackStatusCallback cb;
             switch (msg.what) {
                 case CMD_UPDATE_PARAMS:
-                    final DhcpServingParams params = (DhcpServingParams) msg.obj;
+                    final Pair<DhcpServingParams, INetworkStackStatusCallback> pair =
+                            (Pair<DhcpServingParams, INetworkStackStatusCallback>) 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:
-                    // This is a no-op if the listener is already started
+                    mPacketListener = mDeps.makePacketListener();
                     mPacketListener.start();
+                    cb = (INetworkStackStatusCallback) msg.obj;
                     break;
                 case CMD_STOP_DHCP_SERVER:
-                    // This is a no-op if the listener was not started
-                    mPacketListener.stop();
+                    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);
+                }
             }
         }
     }
@@ -572,7 +641,7 @@
                 return mSocket;
             } catch (IOException | ErrnoException e) {
                 mLog.e("Error creating UDP socket", e);
-                DhcpServer.this.stop();
+                DhcpServer.this.stop(null);
                 return null;
             } finally {
                 TrafficStats.setThreadStatsTag(oldTag);
diff --git a/services/net/java/android/net/dhcp/DhcpServingParams.java b/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java
similarity index 95%
rename from services/net/java/android/net/dhcp/DhcpServingParams.java
rename to packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java
index 2780814a..f38888a 100644
--- a/services/net/java/android/net/dhcp/DhcpServingParams.java
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java
@@ -18,9 +18,10 @@
 
 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 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;
 
@@ -107,9 +108,13 @@
 
     /**
      * Create parameters from a stable AIDL-compatible parcel.
+     * @throws InvalidParameterException The parameters parcelable is null or invalid.
      */
-    public static DhcpServingParams fromParcelableObject(@NonNull DhcpServingParamsParcel parcel)
+    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);
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 0000000..74bc147
--- /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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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 5afaf58..7fea1e0 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 @@
     }
 
     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 0000000..bb5900c
--- /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 0000000..733f873
--- /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 0000000..bd7ff2a
--- /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 0000000..8b8474f
--- /dev/null
+++ b/packages/NetworkStack/tests/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.server.networkstack.tests">
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.server.networkstack.tests"
+        android:label="Networking service tests">
+    </instrumentation>
+</manifest>
\ 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 0000000..6b08b57
--- /dev/null
+++ b/packages/NetworkStack/tests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Tests for NetworkStack">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="NetworkStackTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="framework-base-presubmit" />
+    <option name="test-tag" value="NetworkStackTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.server.networkstack.tests" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
similarity index 99%
rename from tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java
rename to packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
index ba0448c..51d50d9 100644
--- a/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
@@ -16,6 +16,7 @@
 
 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;
@@ -29,7 +30,6 @@
 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;
diff --git a/tests/net/java/android/net/dhcp/DhcpServerTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java
similarity index 89%
rename from tests/net/java/android/net/dhcp/DhcpServerTest.java
rename to packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java
index ab9bd84..d4c1e2e 100644
--- a/tests/net/java/android/net/dhcp/DhcpServerTest.java
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java
@@ -16,11 +16,13 @@
 
 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;
@@ -33,14 +35,14 @@
 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 static java.net.InetAddress.parseNumericAddress;
-
 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;
@@ -48,9 +50,11 @@
 import android.net.dhcp.DhcpServer.Clock;
 import android.net.dhcp.DhcpServer.Dependencies;
 import android.net.util.SharedLog;
-import android.os.test.TestLooper;
+import android.os.HandlerThread;
 import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
 
 import org.junit.After;
 import org.junit.Before;
@@ -67,10 +71,10 @@
 import java.util.HashSet;
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
 @SmallTest
+@RunWithLooper
 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");
@@ -113,18 +117,25 @@
     private ArgumentCaptor<Inet4Address> mResponseDstAddrCaptor;
 
     @NonNull
-    private TestLooper mLooper;
+    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 {
-        // 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);
@@ -143,20 +154,22 @@
                 .setExcludedAddrs(TEST_EXCLUDED_ADDRS)
                 .build();
 
-        mLooper = new TestLooper();
-        mServer = new DhcpServer(mLooper.getLooper(), TEST_IFACE, servingParams,
+        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();
-        mLooper.dispatchAll();
+        mServer.start(mAssertSuccessCallback);
+        mLooper.processAllMessages();
     }
 
     @After
-    public void tearDown() {
-        // Calling stop() several times is not an issue
-        mServer.stop();
-        System.setProperty(PROP_DEXMAKER_SHARE_CLASSLOADER,
-                (mPrevShareClassloaderProp == null ? "" : mPrevShareClassloaderProp));
+    public void tearDown() throws Exception {
+        mServer.stop(mAssertSuccessCallback);
+        mLooper.processMessages(1);
+        verify(mPacketListener, times(1)).stop();
+        verify(mHandlerThread, times(1)).quitSafely();
     }
 
     @Test
@@ -165,13 +178,6 @@
     }
 
     @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),
diff --git a/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java
similarity index 96%
rename from tests/net/java/android/net/dhcp/DhcpServingParamsTest.java
rename to packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java
index 2ab2246..3ca0564 100644
--- a/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java
@@ -16,6 +16,7 @@
 
 package android.net.dhcp;
 
+import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.NetworkUtils.inet4AddressToIntHTH;
 import static android.net.dhcp.DhcpServingParams.MTU_UNSET;
 
@@ -23,8 +24,6 @@
 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;
@@ -195,6 +194,11 @@
         assertEquals(7, numFields);
     }
 
+    @Test(expected = InvalidParameterException.class)
+    public void testFromParcelableObject_NullArgument() throws InvalidParameterException {
+        DhcpServingParams.fromParcelableObject(null);
+    }
+
     private static int[] toIntArray(Collection<Inet4Address> addrs) {
         return addrs.stream().mapToInt(NetworkUtils::inet4AddressToIntHTH).toArray();
     }
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 0000000..07ad3123
--- /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 9dfdddb..eb5be77 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 @@
         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 7daf71d..a42efe9 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 @@
     /**
      * 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 e0ae68f..ae697b7 100644
--- a/services/net/Android.bp
+++ b/services/net/Android.bp
@@ -2,3 +2,19 @@
     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/DhcpPacket.java b/services/net/java/android/net/dhcp/DhcpPacket.java
index 6ba7d94..ce8b7e7 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 @@
 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/ip/IpServer.java b/services/net/java/android/net/ip/IpServer.java
index 493350d..8b22f68 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 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 @@
             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 @@
     // 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 @@
 
     private boolean startIPv4() { return configureIPv4(true); }
 
+    /**
+     * Convenience wrapper around INetworkStackStatusCallback to run callbacks on the IpServer
+     * handler.
+     *
+     * <p>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;
-        }
+        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
 
-        mDhcpServer = mDeps.makeDhcpServer(getHandler().getLooper(), mIfaceName, params,
-                mLog.forSubComponent("DHCP"));
-        mDhcpServer.start();
+        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 3defe56..c183b81 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 @@
      *     - 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 74bc147..8b7b59d 100644
--- a/services/net/java/android/net/util/SharedLog.java
+++ b/services/net/java/android/net/util/SharedLog.java
@@ -32,6 +32,7 @@
  *
  * 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/ip/IpServerTest.java b/tests/net/java/android/net/ip/IpServerTest.java
index 0178228..c3162af 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.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.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 @@
     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<DhcpServingParams> mDhcpParamsCaptor;
+    @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
 
     private final TestLooper mLooper = new TestLooper();
     private final ArgumentCaptor<LinkProperties> mLinkPropertiesCaptor =
@@ -112,8 +121,18 @@
         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 @@
         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 @@
             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 e6b43d2..1ea83c2 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_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 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.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.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.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 @@
     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 @@
     @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 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<IpServer> ipv6CoordinatorNotifyList;
@@ -216,35 +265,8 @@
         }
 
         @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 @@
 
         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 @@
         runUsbTethering(upstreamState);
         sendIPv6TetherUpdates(upstreamState);
 
-        verify(mDhcpServer, never()).start();
+        verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any());
     }
 
     @Test
@@ -581,7 +603,7 @@
         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 @@
 
         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 @@
         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 @@
         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