[Thread] add ThreadNetworkController APIs

The ThreadNetworkController class provides APIs for managing the Thread
network, for example, attach to a specific network, form a network with
given dataset or update/migrate an existing network.

Bug: 262683651
Test: atest CtsThreadNetworkTestCases
Change-Id: Ib3c267d2c81a8c3c7772ed3c9cd2092487cc941a
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index fcf9521..bd265e6 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -33,11 +33,35 @@
     min_sdk_version: "30",
     srcs: [":service-thread-sources"],
     libs: [
+        "framework-connectivity-pre-jarjar",
         "framework-connectivity-t-pre-jarjar",
         "service-connectivity-pre-jarjar",
     ],
     static_libs: [
         "net-utils-device-common",
+        "net-utils-device-common-netlink",
+        "ot-daemon-aidl-java",
+    ],
+    apex_available: ["com.android.tethering"],
+}
+
+cc_library_shared {
+    name: "libservice-thread-jni",
+    min_sdk_version: "30",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    srcs: [
+        "jni/**/*.cpp",
+    ],
+    shared_libs: [
+        "libbase",
+        "libcutils",
+        "liblog",
+        "libnativehelper",
     ],
     apex_available: ["com.android.tethering"],
 }
diff --git a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
new file mode 100644
index 0000000..a8909bc
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.IOperationReceiver;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** A {@link IOperationReceiver} wrapper which makes it easier to invoke the callbacks. */
+final class OperationReceiverWrapper {
+    private final IOperationReceiver mReceiver;
+
+    private static final Object sPendingReceiversLock = new Object();
+
+    @GuardedBy("sPendingReceiversLock")
+    private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
+
+    public OperationReceiverWrapper(IOperationReceiver receiver) {
+        this.mReceiver = receiver;
+
+        synchronized (sPendingReceiversLock) {
+            sPendingReceivers.add(this);
+        }
+    }
+
+    public static void onOtDaemonDied() {
+        synchronized (sPendingReceiversLock) {
+            for (OperationReceiverWrapper receiver : sPendingReceivers) {
+                try {
+                    receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+                } catch (RemoteException e) {
+                    // The client is dead, do nothing
+                }
+            }
+            sPendingReceivers.clear();
+        }
+    }
+
+    public void onSuccess() {
+        synchronized (sPendingReceiversLock) {
+            sPendingReceivers.remove(this);
+        }
+
+        try {
+            mReceiver.onSuccess();
+        } catch (RemoteException e) {
+            // The client is dead, do nothing
+        }
+    }
+
+    public void onError(int errorCode, String errorMessage, Object... messageArgs) {
+        synchronized (sPendingReceiversLock) {
+            sPendingReceivers.remove(this);
+        }
+
+        try {
+            mReceiver.onError(errorCode, String.format(errorMessage, messageArgs));
+        } catch (RemoteException e) {
+            // The client is dead, do nothing
+        }
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index e8b95bc..3470f27 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -1,31 +1,729 @@
 /*
  * Copyright (C) 2023 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
+ * 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
+ * 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.
+ * 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.thread;
 
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
 import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
+import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED;
+import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
+import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ErrorCode;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_DETACHED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REASSEMBLY_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REJECTED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_RESPONSE_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_UNSUPPORTED_CHANNEL;
+import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IStateCallback;
 import android.net.thread.IThreadNetworkController;
+import android.net.thread.PendingOperationalDataset;
 import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.DeviceRole;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
 
-/** Implementation of the {@link ThreadNetworkController} API. */
-public final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.IOtDaemon;
+import com.android.server.thread.openthread.IOtDaemonCallback;
+import com.android.server.thread.openthread.IOtStatusReceiver;
+import com.android.server.thread.openthread.Ipv6AddressInfo;
+import com.android.server.thread.openthread.OtDaemonState;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Implementation of the {@link ThreadNetworkController} API.
+ *
+ * <p>Threading model: This class is not Thread-safe and should only be accessed from the
+ * ThreadNetworkService class. Additional attention should be paid to handle the threading code
+ * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from
+ * `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
+ * HandlerThread except for arguments or permissions checking
+ */
+final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
+    private static final String TAG = "ThreadNetworkService";
+
+    // Below member fields can be accessed from both the binder and handler threads
+
+    private final Context mContext;
+    private final Handler mHandler;
+
+    // Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
+    // particular, the constructor does not run on the handler thread, so it must not touch any of
+    // the non-final fields, nor must it mutate any of the non-final fields inside these objects.
+
+    private final HandlerThread mHandlerThread;
+    private final NetworkProvider mNetworkProvider;
+    private final Supplier<IOtDaemon> mOtDaemonSupplier;
+    private final ConnectivityManager mConnectivityManager;
+    private final TunInterfaceController mTunIfController;
+    private final LinkProperties mLinkProperties = new LinkProperties();
+    private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+
+    private IOtDaemon mOtDaemon;
+    private NetworkAgent mNetworkAgent;
+
+    @VisibleForTesting
+    ThreadNetworkControllerService(
+            Context context,
+            HandlerThread handlerThread,
+            NetworkProvider networkProvider,
+            Supplier<IOtDaemon> otDaemonSupplier,
+            ConnectivityManager connectivityManager,
+            TunInterfaceController tunIfController) {
+        mContext = context;
+        mHandlerThread = handlerThread;
+        mHandler = new Handler(handlerThread.getLooper());
+        mNetworkProvider = networkProvider;
+        mOtDaemonSupplier = otDaemonSupplier;
+        mConnectivityManager = connectivityManager;
+        mTunIfController = tunIfController;
+    }
+
+    public static ThreadNetworkControllerService newInstance(Context context) {
+        HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
+        handlerThread.start();
+        NetworkProvider networkProvider =
+                new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
+
+        return new ThreadNetworkControllerService(
+                context,
+                handlerThread,
+                networkProvider,
+                () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
+                context.getSystemService(ConnectivityManager.class),
+                new TunInterfaceController(TUN_IF_NAME));
+    }
+
+    private static NetworkCapabilities newNetworkCapabilities() {
+        return new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build();
+    }
+
+    private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
+        try {
+            return InetAddress.getByAddress(addressInfo.address);
+        } catch (UnknownHostException e) {
+            // This is impossible unless the Thread daemon is critically broken
+            return null;
+        }
+    }
+
+    private static LinkAddress newLinkAddress(Ipv6AddressInfo addressInfo) {
+        long deprecationTimeMillis =
+                addressInfo.isPreferred
+                        ? LinkAddress.LIFETIME_PERMANENT
+                        : SystemClock.elapsedRealtime();
+
+        InetAddress address = addressInfoToInetAddress(addressInfo);
+
+        // flags and scope will be adjusted automatically depending on the address and
+        // its lifetimes.
+        return new LinkAddress(
+                address,
+                addressInfo.prefixLength,
+                0 /* flags */,
+                0 /* scope */,
+                deprecationTimeMillis,
+                LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
+    }
+
+    private void initializeOtDaemon() {
+        try {
+            getOtDaemon();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to initialize ot-daemon");
+        }
+    }
+
+    private IOtDaemon getOtDaemon() throws RemoteException {
+        if (mOtDaemon != null) {
+            return mOtDaemon;
+        }
+
+        IOtDaemon otDaemon = mOtDaemonSupplier.get();
+        if (otDaemon == null) {
+            throw new RemoteException("Internal error: failed to start OT daemon");
+        }
+        otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
+        otDaemon.initialize(mTunIfController.getTunFd());
+        otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+        mOtDaemon = otDaemon;
+        return mOtDaemon;
+    }
+
+    private void onOtDaemonDied() {
+        Log.w(TAG, "OT daemon became dead, clean up...");
+        OperationReceiverWrapper.onOtDaemonDied();
+        mOtDaemonCallbackProxy.onOtDaemonDied();
+        mOtDaemon = null;
+    }
+
+    public void initialize() {
+        mHandler.post(
+                () -> {
+                    Log.d(TAG, "Initializing Thread system service...");
+                    try {
+                        mTunIfController.createTunInterface();
+                    } catch (IOException e) {
+                        throw new IllegalStateException(
+                                "Failed to create Thread tunnel interface", e);
+                    }
+                    mLinkProperties.setInterfaceName(TUN_IF_NAME);
+                    mLinkProperties.setMtu(TunInterfaceController.MTU);
+                    mConnectivityManager.registerNetworkProvider(mNetworkProvider);
+
+                    initializeOtDaemon();
+                });
+    }
+
+    private void registerThreadNetwork() {
+        if (mNetworkAgent != null) {
+            return;
+        }
+        NetworkCapabilities netCaps = newNetworkCapabilities();
+        NetworkScore score =
+                new NetworkScore.Builder()
+                        .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build();
+        mNetworkAgent =
+                new NetworkAgent(
+                        mContext,
+                        mHandlerThread.getLooper(),
+                        TAG,
+                        netCaps,
+                        mLinkProperties,
+                        score,
+                        new NetworkAgentConfig.Builder().build(),
+                        mNetworkProvider) {};
+        mNetworkAgent.register();
+        mNetworkAgent.markConnected();
+        Log.i(TAG, "Registered Thread network");
+    }
+
+    private void unregisterThreadNetwork() {
+        if (mNetworkAgent == null) {
+            // unregisterThreadNetwork can be called every time this device becomes detached or
+            // disabled and the mNetworkAgent may not be created in this cases
+            return;
+        }
+
+        Log.d(TAG, "Unregistering Thread network agent");
+
+        mNetworkAgent.unregister();
+        mNetworkAgent = null;
+    }
+
+    private void updateTunInterfaceAddress(LinkAddress linkAddress, boolean isAdded) {
+        try {
+            if (isAdded) {
+                mTunIfController.addAddress(linkAddress);
+            } else {
+                mTunIfController.removeAddress(linkAddress);
+            }
+        } catch (IOException e) {
+            Log.e(
+                    TAG,
+                    String.format(
+                            "Failed to %s Thread tun interface address %s",
+                            (isAdded ? "add" : "remove"), linkAddress),
+                    e);
+        }
+    }
+
+    private void updateNetworkLinkProperties(LinkAddress linkAddress, boolean isAdded) {
+        if (isAdded) {
+            mLinkProperties.addLinkAddress(linkAddress);
+        } else {
+            mLinkProperties.removeLinkAddress(linkAddress);
+        }
+
+        // The Thread daemon can send link property updates before the networkAgent is
+        // registered
+        if (mNetworkAgent != null) {
+            mNetworkAgent.sendLinkProperties(mLinkProperties);
+        }
+    }
 
     @Override
     public int getThreadVersion() {
         return THREAD_VERSION_1_3;
     }
+
+    private void enforceAllCallingPermissionsGranted(String... permissions) {
+        for (String permission : permissions) {
+            mContext.enforceCallingPermission(
+                    permission, "Permission " + permission + " is missing");
+        }
+    }
+
+    @Override
+    public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
+        enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+
+        mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
+    }
+
+    @Override
+    public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
+        mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
+    }
+
+    @Override
+    public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
+            throws RemoteException {
+        enforceAllCallingPermissionsGranted(
+                permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
+    }
+
+    @Override
+    public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
+            throws RemoteException {
+        mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
+    }
+
+    private void checkOnHandlerThread() {
+        if (Looper.myLooper() != mHandlerThread.getLooper()) {
+            Log.wtf(TAG, "Must be on the handler thread!");
+        }
+    }
+
+    private IOtStatusReceiver newOtStatusReceiver(OperationReceiverWrapper receiver) {
+        return new IOtStatusReceiver.Stub() {
+            @Override
+            public void onSuccess() {
+                receiver.onSuccess();
+            }
+
+            @Override
+            public void onError(int otError, String message) {
+                receiver.onError(otErrorToAndroidError(otError), message);
+            }
+        };
+    }
+
+    @ErrorCode
+    private static int otErrorToAndroidError(int otError) {
+        // See external/openthread/include/openthread/error.h for OT error definition
+        switch (otError) {
+            case OT_ERROR_ABORT:
+                return ERROR_ABORTED;
+            case OT_ERROR_BUSY:
+                return ERROR_BUSY;
+            case OT_ERROR_DETACHED:
+            case OT_ERROR_INVALID_STATE:
+                return ERROR_FAILED_PRECONDITION;
+            case OT_ERROR_NO_BUFS:
+                return ERROR_RESOURCE_EXHAUSTED;
+            case OT_ERROR_PARSE:
+                return ERROR_RESPONSE_BAD_FORMAT;
+            case OT_ERROR_REASSEMBLY_TIMEOUT:
+            case OT_ERROR_RESPONSE_TIMEOUT:
+                return ERROR_TIMEOUT;
+            case OT_ERROR_REJECTED:
+                return ERROR_REJECTED_BY_PEER;
+            case OT_ERROR_UNSUPPORTED_CHANNEL:
+                return ERROR_UNSUPPORTED_CHANNEL;
+            default:
+                return ERROR_INTERNAL_ERROR;
+        }
+    }
+
+    @Override
+    public void join(
+            @NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
+        enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+        mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
+    }
+
+    private void joinInternal(
+            @NonNull ActiveOperationalDataset activeDataset,
+            @NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            // The otDaemon.join() will leave first if this device is currently attached
+            getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
+        } catch (RemoteException e) {
+            Log.e(TAG, "otDaemon.join failed", e);
+            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        }
+    }
+
+    @Override
+    public void scheduleMigration(
+            @NonNull PendingOperationalDataset pendingDataset,
+            @NonNull IOperationReceiver receiver) {
+        enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+        mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
+    }
+
+    public void scheduleMigrationInternal(
+            @NonNull PendingOperationalDataset pendingDataset,
+            @NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon()
+                    .scheduleMigration(
+                            pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
+        } catch (RemoteException e) {
+            Log.e(TAG, "otDaemon.scheduleMigration failed", e);
+            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        }
+    }
+
+    @Override
+    public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
+        enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
+    }
+
+    private void leaveInternal(@NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon().leave(newOtStatusReceiver(receiver));
+        } catch (RemoteException e) {
+            // Oneway AIDL API should never throw?
+            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        }
+    }
+
+    private void handleThreadInterfaceStateChanged(boolean isUp) {
+        try {
+            mTunIfController.setInterfaceUp(isUp);
+            Log.d(TAG, "Thread network interface becomes " + (isUp ? "up" : "down"));
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to handle Thread interface state changes", e);
+        }
+    }
+
+    private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
+        if (ThreadNetworkController.isAttached(deviceRole)) {
+            Log.d(TAG, "Attached to the Thread network");
+            registerThreadNetwork();
+        } else {
+            Log.d(TAG, "Detached from the Thread network");
+            unregisterThreadNetwork();
+        }
+    }
+
+    private void handleAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
+        checkOnHandlerThread();
+        InetAddress address = addressInfoToInetAddress(addressInfo);
+        if (address.isMulticastAddress()) {
+            Log.i(TAG, "Ignoring multicast address " + address.getHostAddress());
+            return;
+        }
+
+        LinkAddress linkAddress = newLinkAddress(addressInfo);
+        Log.d(TAG, (isAdded ? "Adding" : "Removing") + " address " + linkAddress);
+
+        updateTunInterfaceAddress(linkAddress, isAdded);
+        updateNetworkLinkProperties(linkAddress, isAdded);
+    }
+
+    private static final class CallbackMetadata {
+        private static long gId = 0;
+
+        // The unique ID
+        final long id;
+
+        final IBinder.DeathRecipient deathRecipient;
+
+        CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
+            this.id = allocId();
+            this.deathRecipient = deathRecipient;
+        }
+
+        private static long allocId() {
+            if (gId == Long.MAX_VALUE) {
+                gId = 0;
+            }
+            return gId++;
+        }
+    }
+
+    /**
+     * Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
+     * mHandlerThread}.
+     */
+    private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
+        private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();
+        private final Map<IOperationalDatasetCallback, CallbackMetadata> mOpDatasetCallbacks =
+                new HashMap<>();
+
+        private OtDaemonState mState;
+        private ActiveOperationalDataset mActiveDataset;
+        private PendingOperationalDataset mPendingDataset;
+
+        public void registerStateCallback(IStateCallback callback) {
+            checkOnHandlerThread();
+            if (mStateCallbacks.containsKey(callback)) {
+                return;
+            }
+
+            IBinder.DeathRecipient deathRecipient =
+                    () -> mHandler.post(() -> unregisterStateCallback(callback));
+            CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+            mStateCallbacks.put(callback, callbackMetadata);
+            try {
+                callback.asBinder().linkToDeath(deathRecipient, 0);
+            } catch (RemoteException e) {
+                mStateCallbacks.remove(callback);
+                // This is thrown when the client is dead, do nothing
+            }
+
+            try {
+                getOtDaemon().registerStateCallback(this, callbackMetadata.id);
+            } catch (RemoteException e) {
+                // oneway operation should never fail
+            }
+        }
+
+        public void unregisterStateCallback(IStateCallback callback) {
+            checkOnHandlerThread();
+            if (!mStateCallbacks.containsKey(callback)) {
+                return;
+            }
+            callback.asBinder().unlinkToDeath(mStateCallbacks.remove(callback).deathRecipient, 0);
+        }
+
+        public void registerDatasetCallback(IOperationalDatasetCallback callback) {
+            checkOnHandlerThread();
+            if (mOpDatasetCallbacks.containsKey(callback)) {
+                return;
+            }
+
+            IBinder.DeathRecipient deathRecipient =
+                    () -> mHandler.post(() -> unregisterDatasetCallback(callback));
+            CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+            mOpDatasetCallbacks.put(callback, callbackMetadata);
+            try {
+                callback.asBinder().linkToDeath(deathRecipient, 0);
+            } catch (RemoteException e) {
+                mOpDatasetCallbacks.remove(callback);
+            }
+
+            try {
+                getOtDaemon().registerStateCallback(this, callbackMetadata.id);
+            } catch (RemoteException e) {
+                // oneway operation should never fail
+            }
+        }
+
+        public void unregisterDatasetCallback(IOperationalDatasetCallback callback) {
+            checkOnHandlerThread();
+            if (!mOpDatasetCallbacks.containsKey(callback)) {
+                return;
+            }
+            callback.asBinder()
+                    .unlinkToDeath(mOpDatasetCallbacks.remove(callback).deathRecipient, 0);
+        }
+
+        public void onOtDaemonDied() {
+            checkOnHandlerThread();
+            if (mState == null) {
+                return;
+            }
+
+            // If this device is already STOPPED or DETACHED, do nothing
+            if (!ThreadNetworkController.isAttached(mState.deviceRole)) {
+                return;
+            }
+
+            // The Thread device role is considered DETACHED when the OT daemon process is dead
+            handleDeviceRoleChanged(DEVICE_ROLE_DETACHED);
+            for (IStateCallback callback : mStateCallbacks.keySet()) {
+                try {
+                    callback.onDeviceRoleChanged(DEVICE_ROLE_DETACHED);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        @Override
+        public void onStateChanged(OtDaemonState newState, long listenerId) {
+            mHandler.post(() -> onStateChangedInternal(newState, listenerId));
+        }
+
+        private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
+            checkOnHandlerThread();
+            onInterfaceStateChanged(newState.isInterfaceUp);
+            onDeviceRoleChanged(newState.deviceRole, listenerId);
+            onPartitionIdChanged(newState.partitionId, listenerId);
+            mState = newState;
+
+            ActiveOperationalDataset newActiveDataset;
+            try {
+                if (newState.activeDatasetTlvs.length != 0) {
+                    newActiveDataset =
+                            ActiveOperationalDataset.fromThreadTlvs(newState.activeDatasetTlvs);
+                } else {
+                    newActiveDataset = null;
+                }
+                onActiveOperationalDatasetChanged(newActiveDataset, listenerId);
+                mActiveDataset = newActiveDataset;
+            } catch (IllegalArgumentException e) {
+                // Is unlikely that OT will generate invalid Operational Dataset
+                Log.w(TAG, "Ignoring invalid Active Operational Dataset changes", e);
+            }
+
+            PendingOperationalDataset newPendingDataset;
+            try {
+                if (newState.pendingDatasetTlvs.length != 0) {
+                    newPendingDataset =
+                            PendingOperationalDataset.fromThreadTlvs(newState.pendingDatasetTlvs);
+                } else {
+                    newPendingDataset = null;
+                }
+                onPendingOperationalDatasetChanged(newPendingDataset, listenerId);
+                mPendingDataset = newPendingDataset;
+            } catch (IllegalArgumentException e) {
+                Log.w(TAG, "Ignoring invalid Pending Operational Dataset changes", e);
+            }
+        }
+
+        private void onInterfaceStateChanged(boolean isUp) {
+            checkOnHandlerThread();
+            if (mState == null || mState.isInterfaceUp != isUp) {
+                handleThreadInterfaceStateChanged(isUp);
+            }
+        }
+
+        private void onDeviceRoleChanged(@DeviceRole int deviceRole, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = (mState == null || mState.deviceRole != deviceRole);
+            if (hasChange) {
+                handleDeviceRoleChanged(deviceRole);
+            }
+
+            for (var callbackEntry : mStateCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onDeviceRoleChanged(deviceRole);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        private void onPartitionIdChanged(long partitionId, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = (mState == null || mState.partitionId != partitionId);
+
+            for (var callbackEntry : mStateCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onPartitionIdChanged(partitionId);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        private void onActiveOperationalDatasetChanged(
+                ActiveOperationalDataset activeDataset, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = !Objects.equals(mActiveDataset, activeDataset);
+
+            for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onActiveOperationalDatasetChanged(activeDataset);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        private void onPendingOperationalDatasetChanged(
+                PendingOperationalDataset pendingDataset, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = !Objects.equals(mPendingDataset, pendingDataset);
+            for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onPendingOperationalDatasetChanged(pendingDataset);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        @Override
+        public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
+            mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
+        }
+    }
 }
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index c6d47df..cc694a1 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.thread;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.net.thread.IThreadNetworkController;
 import android.net.thread.IThreadNetworkManager;
@@ -29,16 +30,12 @@
  * Implementation of the Thread network service. This is the entry point of Android Thread feature.
  */
 public class ThreadNetworkService extends IThreadNetworkManager.Stub {
-    private final ThreadNetworkControllerService mControllerService;
+    private final Context mContext;
+    @Nullable private ThreadNetworkControllerService mControllerService;
 
     /** Creates a new {@link ThreadNetworkService} object. */
     public ThreadNetworkService(Context context) {
-        this(context, new ThreadNetworkControllerService());
-    }
-
-    private ThreadNetworkService(
-            Context context, ThreadNetworkControllerService controllerService) {
-        mControllerService = controllerService;
+        mContext = context;
     }
 
     /**
@@ -48,12 +45,16 @@
      */
     public void onBootPhase(int phase) {
         if (phase == SystemService.PHASE_BOOT_COMPLETED) {
-            // TODO: initialize ThreadNetworkManagerService
+            mControllerService = ThreadNetworkControllerService.newInstance(mContext);
+            mControllerService.initialize();
         }
     }
 
     @Override
     public List<IThreadNetworkController> getAllThreadNetworkControllers() {
+        if (mControllerService == null) {
+            return Collections.emptyList();
+        }
         return Collections.singletonList(mControllerService);
     }
 }
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
new file mode 100644
index 0000000..ac65b11
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import android.net.LinkAddress;
+import android.net.util.SocketUtils;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/** Controller for virtual/tunnel network interfaces. */
+public class TunInterfaceController {
+    private static final String TAG = "TunIfController";
+    static final int MTU = 1280;
+
+    static {
+        System.loadLibrary("service-thread-jni");
+    }
+
+    private final String mIfName;
+    private ParcelFileDescriptor mParcelTunFd;
+    private FileDescriptor mNetlinkSocket;
+    private static int sNetlinkSeqNo = 0;
+
+    /** Creates a new {@link TunInterfaceController} instance for given interface. */
+    public TunInterfaceController(String interfaceName) {
+        this.mIfName = interfaceName;
+    }
+
+    /**
+     * Creates the tunnel interface.
+     *
+     * @throws IOException if failed to create the interface
+     */
+    public void createTunInterface() throws IOException {
+        mParcelTunFd = ParcelFileDescriptor.adoptFd(nativeCreateTunInterface(mIfName, MTU));
+        try {
+            mNetlinkSocket = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_ROUTE);
+        } catch (ErrnoException e) {
+            throw new IOException("Failed to create netlink socket", e);
+        }
+    }
+
+    public void destroyTunInterface() {
+        try {
+            mParcelTunFd.close();
+            SocketUtils.closeSocket(mNetlinkSocket);
+        } catch (IOException e) {
+            // Should never fail
+        }
+        mParcelTunFd = null;
+        mNetlinkSocket = null;
+    }
+
+    /** Returns the FD of the tunnel interface. */
+    public ParcelFileDescriptor getTunFd() {
+        return mParcelTunFd;
+    }
+
+    private native int nativeCreateTunInterface(String interfaceName, int mtu) throws IOException;
+
+    /** Sets the interface up or down according to {@code isUp}. */
+    public void setInterfaceUp(boolean isUp) throws IOException {
+        nativeSetInterfaceUp(mIfName, isUp);
+    }
+
+    private native void nativeSetInterfaceUp(String interfaceName, boolean isUp) throws IOException;
+
+    /** Adds a new address to the interface. */
+    public void addAddress(LinkAddress address) throws IOException {
+        Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
+
+        long validLifetimeSeconds;
+        long preferredLifetimeSeconds;
+
+        if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
+                || address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
+            validLifetimeSeconds = 0xffffffffL;
+        } else {
+            validLifetimeSeconds =
+                    Math.max(
+                            (address.getDeprecationTime() - SystemClock.elapsedRealtime()) / 1000L,
+                            0L);
+        }
+
+        if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
+                || address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
+            preferredLifetimeSeconds = 0xffffffffL;
+        } else {
+            preferredLifetimeSeconds =
+                    Math.max(
+                            (address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
+                            0L);
+        }
+
+        byte[] message =
+                RtNetlinkAddressMessage.newRtmNewAddressMessage(
+                        sNetlinkSeqNo,
+                        address.getAddress(),
+                        (short) address.getPrefixLength(),
+                        address.getFlags(),
+                        (byte) address.getScope(),
+                        Os.if_nametoindex(mIfName),
+                        validLifetimeSeconds,
+                        preferredLifetimeSeconds);
+        try {
+            Os.write(mNetlinkSocket, message, 0, message.length);
+        } catch (ErrnoException e) {
+            throw new IOException("Failed to send netlink message", e);
+        }
+    }
+
+    /** Removes an address from the interface. */
+    public void removeAddress(LinkAddress address) throws IOException {
+        // TODO(b/263222068): remove address with netlink
+    }
+}
diff --git a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
new file mode 100644
index 0000000..ed39fab
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#define LOG_TAG "jniThreadTun"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/if_tun.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
+#include <string>
+
+#include <private/android_filesystem_config.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint com_android_server_thread_TunInterfaceController_createTunInterface(
+        JNIEnv* env, jobject clazz, jstring interfaceName, jint mtu) {
+    ScopedUtfChars ifName(env, interfaceName);
+
+    int fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC);
+    if (fd == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)",
+                             strerror(errno));
+        return -1;
+    }
+
+    struct ifreq ifr = {
+            .ifr_flags = IFF_TUN | IFF_NO_PI | static_cast<short>(IFF_TUN_EXCL),
+    };
+    strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
+
+    if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr)) != 0) {
+        close(fd);
+        jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
+                             strerror(errno));
+        return -1;
+    }
+
+    int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
+    if (inet6 == -1) {
+        close(fd);
+        jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
+                             strerror(errno));
+        return -1;
+    }
+    ifr.ifr_mtu = mtu;
+    if (ioctl(inet6, SIOCSIFMTU, &ifr) != 0) {
+        close(fd);
+        close(inet6);
+        jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFMTU) failed (%s)",
+                             strerror(errno));
+        return -1;
+    }
+
+    close(inet6);
+    return fd;
+}
+
+static void com_android_server_thread_TunInterfaceController_setInterfaceUp(
+        JNIEnv* env, jobject clazz, jstring interfaceName, jboolean isUp) {
+    struct ifreq ifr;
+    ScopedUtfChars ifName(env, interfaceName);
+
+    ifr.ifr_flags = isUp ? IFF_UP : 0;
+    strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
+
+    int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
+    if (inet6 == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
+                             strerror(errno));
+    }
+
+    if (ioctl(inet6, SIOCSIFFLAGS, &ifr) != 0) {
+        close(inet6);
+        jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFFLAGS) failed (%s)",
+                             strerror(errno));
+    }
+
+    close(inet6);
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativeCreateTunInterface",
+         "(Ljava/lang/String;I)I",
+         (void*)com_android_server_thread_TunInterfaceController_createTunInterface},
+        {"nativeSetInterfaceUp",
+         "(Ljava/lang/String;Z)V",
+         (void*)com_android_server_thread_TunInterfaceController_setInterfaceUp},
+};
+
+int register_com_android_server_thread_TunInterfaceController(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/server/thread/TunInterfaceController",
+                                    gMethods, NELEM(gMethods));
+}
+
+};  // namespace android
diff --git a/thread/service/jni/onload.cpp b/thread/service/jni/onload.cpp
new file mode 100644
index 0000000..5081664
--- /dev/null
+++ b/thread/service/jni/onload.cpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "jni.h"
+#include "utils/Log.h"
+
+namespace android {
+int register_com_android_server_thread_TunInterfaceController(JNIEnv* env);
+}
+
+using namespace android;
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
+    JNIEnv* env = NULL;
+
+    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+        ALOGE("GetEnv failed!");
+        return -1;
+    }
+    ALOG_ASSERT(env != NULL, "Could not retrieve the env!");
+
+    register_com_android_server_thread_TunInterfaceController(env);
+    return JNI_VERSION_1_4;
+}