Skip to content

Instantly share code, notes, and snippets.

@thewh1teagle
Last active July 31, 2026 21:59
Show Gist options
  • Select an option

  • Save thewh1teagle/bdd3faf8990d1aa15d39bb92a7c9ac2e to your computer and use it in GitHub Desktop.

Select an option

Save thewh1teagle/bdd3faf8990d1aa15d39bb92a7c9ac2e to your computer and use it in GitHub Desktop.
Native CDC-NCM USB tethering for Nothing phones on macOS

Nothing NCM Tether

A fire-and-forget macOS shell command that makes a supported Nothing phone expose native CDC-NCM USB Ethernet. It installs nothing and runs no background process.

Quick start

Install ADB once, enable USB debugging on the unlocked phone, connect it, and accept its authorization prompt:

brew install android-platform-tools
git clone https://gist.github.com/bdd3faf8990d1aa15d39bb92a7c9ac2e.git nothing-ncm-tether
cd nothing-ncm-tether
bash build.sh
sh nothing-tether

With multiple Android devices connected, use sh nothing-tether SERIAL. Re-run it after rebooting the phone or whenever tethering does not return. Android keeps using its normal upstream—phone Wi-Fi or mobile data—and the script does not change Wi-Fi settings on either device.

What it does

The thin shell script performs exactly three ADB phases:

  1. Push ncmtether.jar to /data/local/tmp/.
  2. Save NCM as the unlocked USB mode and reset USB so usb0 is absent.
  3. Request Ethernet tethering through Android's shell identity, then enable NCM.

The ordering prevents Android's Ethernet client from claiming usb0 before the tethering service does. Plain svc usb setFunctions ncm is insufficient on the affected Nothing build because its NCM resource overlay targets the wrong package.

Everything is visible in nothing-tether and NcmTether.java. The JAR is compiled Android DEX, not a Mac executable. Run bash build.sh to reproduce it from source when Android SDK/build-tools 36.1 and a JDK are installed. Without those, build.sh keeps the checked-in precompiled JAR and still validates it.

No Go, root, sudo, proxy, VPN, macOS driver, or installation is involved.

#!/bin/bash
set -euo pipefail
root="$(cd "$(dirname "$0")" && pwd)"
sdk="${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}"
platform="${ANDROID_PLATFORM:-$sdk/platforms/android-36.1/android.jar}"
d8="${D8:-$sdk/build-tools/36.1.0/d8}"
studio_jdk="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
java_compiler="${JAVAC:-$studio_jdk/bin/javac}"
bridge="$root/ncmtether.jar"
[ -x "$java_compiler" ] || java_compiler="$(command -v javac 2>/dev/null || true)"
[ "$java_compiler" != "$studio_jdk/bin/javac" ] || export JAVA_HOME="$studio_jdk"
if [ -f "$platform" ] && [ -x "$d8" ] && [ -x "$java_compiler" ] && "$java_compiler" -version >/dev/null 2>&1; then
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/classes" "$work/dex"
"$java_compiler" -source 11 -target 11 -cp "$platform" -d "$work/classes" "$root/NcmTether.java"
"$d8" --lib "$platform" --output "$work/dex" "$work/classes"/*.class
touch -t 198001010000 "$work/dex/classes.dex"
(cd "$work/dex" && zip -X -q "$bridge" classes.dex)
echo "Rebuilt Android bridge from source."
else
[ -s "$bridge" ] || base64 -D < "$root/ncmtether.jar.b64" > "$bridge"
[ -s "$bridge" ] || { echo "Missing Android toolchain and precompiled bridge." >&2; exit 1; }
echo "Using precompiled Android bridge."
fi
shasum -a 256 "$bridge"
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
import android.content.Context;
import android.content.ContextWrapper;
import android.net.TetheringManager;
import android.os.Looper;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Complete source for the Android tethering bridge embedded by the Go CLI.
* It runs as the real ADB shell UID and requests Ethernet tethering.
*/
public final class NcmTether {
public static void main(String[] args) throws Exception {
Looper.prepareMainLooper();
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Object thread = activityThread.getMethod("systemMain").invoke(null);
Context system = (Context) activityThread.getMethod("getSystemContext").invoke(thread);
Context shell = system.createPackageContext("com.android.shell", 0);
TetheringManager manager = shell.getSystemService(TetheringManager.class);
if (manager == null) throw new IllegalStateException("No TetheringManager");
Context shellIdentity = new ContextWrapper(shell) {
@Override public String getOpPackageName() { return "com.android.shell"; }
@Override public String getAttributionTag() { return null; }
};
Field contextField = TetheringManager.class.getDeclaredField("mContext");
contextField.setAccessible(true);
contextField.set(manager, shellIdentity);
boolean stopping = args.length > 0 && "stop".equals(args[0]);
int type = Integer.parseInt(args[stopping ? 1 : 0]);
TetheringManager.TetheringRequest.Builder builder =
new TetheringManager.TetheringRequest.Builder(type);
Method setScope = builder.getClass().getMethod("setConnectivityScope", int.class);
setScope.invoke(builder, 1); // CONNECTIVITY_SCOPE_GLOBAL
Method exempt = builder.getClass().getMethod("setExemptFromEntitlementCheck", boolean.class);
exempt.invoke(builder, true);
Method showUi = builder.getClass().getMethod("setShouldShowEntitlementUi", boolean.class);
showUi.invoke(builder, false);
TetheringManager.TetheringRequest request = builder.build();
CountDownLatch done = new CountDownLatch(1);
int[] result = {-1};
if (stopping) {
manager.stopTethering(request, Runnable::run,
new TetheringManager.StopTetheringCallback() {
@Override public void onStopTetheringSucceeded() {
result[0] = 0;
done.countDown();
}
@Override public void onStopTetheringFailed(int error) {
result[0] = error;
done.countDown();
}
});
} else {
manager.startTethering(request, Runnable::run,
new TetheringManager.StartTetheringCallback() {
@Override public void onTetheringStarted() {
result[0] = 0;
done.countDown();
}
@Override public void onTetheringFailed(int error) {
result[0] = error;
done.countDown();
}
});
}
if (!done.await(15, TimeUnit.SECONDS)) System.exit(2);
System.exit(result[0] == 0 ? 0 : 1);
}
}
#!/bin/sh
set -e
bridge_jar="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/ncmtether.jar"
serial="${1:-$(adb get-serialno)}"
phone() { adb -s "$serial" "$@"; }
# 1. Put the auditable Android bridge on the phone.
phone push "$bridge_jar" /data/local/tmp/ncmtether.jar >/dev/null
# 2. Remove usb0 after saving NCM as the unlocked USB mode.
phone shell 'for type in 1 4 5; do CLASSPATH=/data/local/tmp/ncmtether.jar app_process /system/bin NcmTether stop $type >/dev/null 2>&1 || :; done
settings put global tether_force_usb_functions 1; svc usb setScreenUnlockedFunctions ncm; svc usb setFunctions' >/dev/null 2>&1 || :
sleep 1
phone wait-for-device
# 3. Request tethering before recreating usb0 as native NCM.
phone shell 'CLASSPATH=/data/local/tmp/ncmtether.jar app_process /system/bin NcmTether 5 && svc usb setFunctions ncm' >/dev/null 2>&1 || :
sleep 1
phone wait-for-device
echo "NCM tethering ready"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment