Develop

USB networking (NCM)

Updated: Jun 16, 2026

Overview

USB NCM (Network Control Model) exposes the USB connection as a standard network interface. This guide explains how a third-party (3P) app can open a direct USB network link between a Meta Quest headset and a connected host computer (for example, a PC), what permissions it needs, how to handle the user-consent flow, and how to tell whether the user has plugged in a USB 2 or a USB 3 cable.
On Quest, the USB network appears as a local-only network with the TRANSPORT_USB transport. Key properties:
  • It is a local link only — there is no internet routing over it. The link is IPv6 link-local; use mDNS or link-local addresses to find the peer.
  • The app does not need any special Meta or Horizon OS permission. Access is gated at runtime by a user-consent dialog, which enables NCM access for all apps.
  • The network is requested through the standard AOSP ConnectivityManager API — there is no Meta-specific SDK to integrate.
  • Requesting this network takes precedence over other networking on the USB interface, so you must always release the request as soon as you are done with it.
There are three things a 3P app typically needs to do, each covered below:
  1. Submit a NetworkRequest for the USB (NCM) network.
  2. Detect when the user has not approved the request (and understand the foreground requirement).
  3. Read the negotiated USB speed and alert the user if a slow (USB 2) cable is connected.

Permissions

No Meta-specific permission is required. The relevant standard Android permissions are:
PermissionRequired?Why
android.permission.CHANGE_NETWORK_STATE
Yes
Needed by ConnectivityManager.requestNetwork().
android.permission.ACCESS_NETWORK_STATE
Yes
Needed to observe network, capability, and link-property callbacks.
android.permission.INTERNET
Yes (if you open sockets)
Required to create TCP/UDP sockets over the link.
android.permission.CHANGE_WIFI_MULTICAST_STATE
If using mDNS
Needed for NsdManager discovery of peers on the link.

Submit a NetworkRequest for the USB network

Build a NetworkRequest that asks for the TRANSPORT_USB transport and explicitly drops the INTERNET and TRUSTED capabilities (the USB link is local-only, so the default capabilities would never be satisfied). Then call ConnectivityManager.requestNetwork() with a NetworkCallback.
static final NetworkRequest NCM_REQUEST =
    new NetworkRequest.Builder()
        .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
        .addTransportType(NetworkCapabilities.TRANSPORT_USB)
        .build();

ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);

void requestNetwork() {
    cm.requestNetwork(NCM_REQUEST, mCallback, new Handler(Looper.getMainLooper()));
}

private final ConnectivityManager.NetworkCallback mCallback =
    new ConnectivityManager.NetworkCallback() {
        @Override
        public void onAvailable(Network network) {
            // The USB-NCM network is up and ready to use.
        }

        @Override
        public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) {
            // Reports link bandwidth — used to detect USB 2 vs USB 3.
        }

        @Override
        public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
            // lp.getInterfaceName() is the NCM interface; lp.getLinkAddresses()
            // contains the IPv6 link-local address to bind/connect with.
        }

        @Override
        public void onLost(Network network) {
            // The network went away (cable unplugged, USB mode changed, etc.).
        }

        @Override
        public void onUnavailable() {
            // The request could not be satisfied — most commonly the user
            // denied the consent dialog.
        }
    };
  • Scope traffic to the network. The USB link is local-only, so bind your sockets to the provided Network (or use the IPv6 link-local source address from LinkProperties) rather than relying on the default route.
  • Always release. Call cm.unregisterNetworkCallback(callback) when you no longer need the link (see Release the network). Holding the request preempts other USB networking.

Detect when the user has not approved the request

The first time an app requests the USB-NCM network, the system shows a consent dialog. The outcome is delivered entirely through your NetworkCallback:
  • ApprovedonAvailable(), followed by onCapabilitiesChanged() and onLinkPropertiesChanged().
  • Denied (or the request otherwise cannot be satisfied) — onUnavailable().
Important: When onUnavailable() fires, your callback has already been unregistered and the network request released, as if you had called unregisterNetworkCallback(). To let the user retry, reset your UI state.
@Override
public void onUnavailable() {
    // The system reports onUnavailable when the request can't be satisfied,
    // most commonly when the user denies the USB-NCM consent dialog. At this
    // point the network request is already released. Update the UI to reflect that.
    if (mRequested) {
        mRequested = false;
    }
    mStatusText.setText(R.string.network_status_unavailable);
}
The system only shows the consent dialog when the requesting app is in the foreground (visible to the user). This is a deliberate anti-abuse measure — a background app cannot pop a USB-networking prompt.
When a request arrives from a background app, the system waits for the app to come to the foreground before showing the dialog. If the app does not become foreground within 30 seconds, the request is dropped and your callback receives onUnavailable().
Guidance:
  • Call requestNetwork() while your activity is visible. This is the simplest, most reliable path.
  • If you must initiate from the background (for example, test automation or a service), start a foreground service and bring your UI to the foreground so the dialog can be shown before the 30-second window expires.
  • Once the user has granted consent, subsequent requests can be satisfied without re-prompting.

Identify the USB speed (USB 2 vs USB 3) and alert the user

USB 2 and USB 3 cables differ enormously in throughput (~480 Mbps vs. ~5 Gbps), and users frequently plug in a charge-only or USB 2 cable, or use a USB 2 port on the host computer by mistake.
The platform surfaces the negotiated USB link speed to 3P apps through the standard NetworkCapabilities link-bandwidth fields on the USB network. The system reads the live USB gadget speed and stamps it onto the NCM network’s capabilities as both upstream and downstream link bandwidth (in Kbps). Your app reads it in onCapabilitiesChanged():
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) {
    int downKbps = caps.getLinkDownstreamBandwidthKbps();
    int upKbps   = caps.getLinkUpstreamBandwidthKbps();
    // downKbps / upKbps reflect the negotiated USB cable/port speed.
}
Map the reported bandwidth to a cable class:
USB negotiated speedReported bandwidthCable class
High-Speed (USB 2.0)
~480,000 Kbps (480 Mbps)
USB 2
SuperSpeed / 5G (USB 3.x Gen 1)
~5,120,000 Kbps (5 Gbps)
USB 3
A simple way to warn the user when a slow cable is connected:
private static final int USB3_MIN_KBPS = 1_000_000; // 1 Gbps threshold

@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) {
    int downKbps = caps.getLinkDownstreamBandwidthKbps();
    if (downKbps > 0 && downKbps < USB3_MIN_KBPS) {
        // ~480 Mbps => USB 2 cable/port. Prompt the user to switch to a
        // USB 3 cable for high-bandwidth transfers.
        showUseUsb3CableHint(downKbps);
    }
}

Release the network

Release the request as soon as you are done. This restores normal USB networking behavior for other components.
void releaseNetwork() {
    cm.unregisterNetworkCallback(mCallback);
}
Also unregister in lifecycle teardown (for example, onDestroy()) so a backgrounded or finishing app does not keep holding the USB link.