diff --git a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java b/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
new file mode 100644
index 0000000000000000000000000000000000000000..f2b0cfbd5a430ff91c57367b6bb6bfc81b33028b
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
@@ -0,0 +1,154 @@
+/**
+ * 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.networkstack.tethering.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.internal.util.State;
+
+import java.util.List;
+
+/**
+ * An implementation of a state machine, meant to be called synchronously.
+ *
+ * This class implements a finite state automaton based on the same State
+ * class as StateMachine.
+ * All methods of this class must be called on only one thread.
+ */
+public class SyncStateMachine {
+    @NonNull private final String mName;
+    @NonNull private final Thread mMyThread;
+    private final boolean mDbg;
+
+    // mCurrentState is the current state. mDestState is the target state that mCurrentState will
+    // transition to. The value of mDestState can be changed when a state processes a message and
+    // calls #transitionTo, but it cannot be changed during the state transition. When the state
+    // transition is complete, mDestState will be set to mCurrentState. Both mCurrentState and
+    // mDestState only be null before state machine starts and must only be touched on mMyThread.
+    @Nullable private State mCurrentState;
+    @Nullable private State mDestState;
+
+    /**
+     * A information class about a state and its parent. Used to maintain the state hierarchy.
+     */
+    public static class StateInfo {
+        /** The state who owns this StateInfo. */
+        public final State state;
+        /** The parent state. */
+        public final State parent;
+        // True when the state has been entered and on the stack.
+        private boolean mActive;
+
+        public StateInfo(@NonNull final State child, @Nullable final State parent) {
+            this.state = child;
+            this.parent = parent;
+        }
+    }
+
+    /**
+     * The constructor.
+     *
+     * @param name of this machine.
+     * @param thread the running thread of this machine. It must either be the thread on which this
+     * constructor is called, or a thread that is not started yet.
+     */
+    public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread) {
+        this(name, thread, false /* debug */);
+    }
+
+    /**
+     * The constructor.
+     *
+     * @param name of this machine.
+     * @param thread the running thread of this machine. It must either be the thread on which this
+     * constructor is called, or a thread that is not started yet.
+     * @param dbg whether to print debug logs.
+     */
+    public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread,
+            final boolean dbg) {
+        mMyThread = thread;
+        // Machine can either be setup from machine thread or before machine thread started.
+        ensureCorrectOrNotStartedThread();
+
+        mName = name;
+        mDbg = dbg;
+    }
+
+    /**
+     * Add all of states to the state machine. Different StateInfos which have same state but have
+     * different parents are not allowed. A state can not have multiple parent states.
+     * This can only be called once either from mMyThread or before mMyThread started.
+     */
+    public final void addAllStates(@NonNull final List<StateInfo> stateInfos) {
+        ensureCorrectOrNotStartedThread();
+
+        if (mCurrentState != null) {
+            throw new IllegalStateException("State only can be added before started");
+        }
+    }
+
+    /**
+     * Start the state machine. The initial state can't be child state.
+     */
+    public final void start(@NonNull final State initialState) {
+        ensureCorrectThread();
+
+        mCurrentState = initialState;
+        mDestState = initialState;
+    }
+
+    /**
+     * Process the message synchronously then perform state transition.
+     */
+    public final void processMessage(int what, int arg1, int arg2, @Nullable Object obj) {
+        ensureCorrectThread();
+    }
+
+    /**
+     * Transition to destination state. Upon returning from processMessage the automaton will
+     * transition to the given destination state.
+     *
+     * This function can NOT be called inside the State enter and exit function. The transition
+     * target is always defined and can never be changed mid-way of state transition.
+     *
+     * @param destState will be the state to transition to.
+     */
+    public final void transitionTo(@NonNull final State destState) {
+        if (mDbg) Log.d(mName, "transitionTo " + destState);
+        ensureCorrectThread();
+
+        if (mDestState == mCurrentState) {
+            mDestState = destState;
+        } else {
+            throw new IllegalStateException("Destination already specified");
+        }
+    }
+
+    private void ensureCorrectThread() {
+        if (!mMyThread.equals(Thread.currentThread())) {
+            throw new IllegalStateException("Called from wrong thread");
+        }
+    }
+
+    private void ensureCorrectOrNotStartedThread() {
+        if (!mMyThread.isAlive()) return;
+
+        ensureCorrectThread();
+    }
+}