Turn your iPad Air M1 into a second display for Linux Mint via USB
The idea is to re-use my daily iPad Air M1 as a second display. I regularly use Linux Mint so the guide will focus around it. If using Windows you can use Spacedesk's USB driver and if using Mac... Official Sidecar!
The easiest way of displaying a screen onto a device is by using the combo Sunshine + Moonlight. Sunshine is the server and Moonlight is the client. This combo is optimized to run games from your big gaming machine onto a light client (i.e. Android TV), so latency has to be low. That's why it uses GPU enc/decoding to transmit the image via network.
Installation and configuration of the combo via WiFi is pretty straight forward and wont be covered here. Just install both apps, configure Sunshine, and the "server" will appear automagically on the client (in my case the iPad).
... the extra mile is the edge cases! What if I don't have a wifi becouse im travelling (i.e. train trips with no cell towers)?, or if I have to be carefull with my tether device's battery? Or if I want to reduce latency at minimum? Can I just create a bridge over a usb cable? Indeed it can be done, and again... doing so with a cellular iPad is also straight forward, as the Cellular iPad does have usb-tethering available from the settings page... but if the iPad is an Wifi only device (like mine)? This is what I want to explore on this guide.
Up to iOS 26.1 (my current iOS version), all iDevices support multiple USB modes including CDC-NCM (networking). When activated via usbmuxd, the iPad exposes USB network interfaces on Linux with zero iPadOS UI changes - just plug & automatic DHCP negotiation creates the network bridge.
Repo packages lack NCM mode support, so custom usbmuxd compilation is required (Linux Mint/Ubuntu).
First install from apt to bootstrap systemd services:
sudo apt update
sudo apt install usbmuxd libimobiledevice-utils libplist-utilsAnd the dependencies to build the custom version:
sudo apt install \
autoconf automake libtool pkg-config \
libplist-dev libssl-dev libusb-1.0-0-dev \
libreadline-dev libncurses5-dev \
git cmake build-essential python3 \
libfuse-dev # Opcional para ifuseThen build custom version from libimobiledevice:
# Compile libimobiledevice stack from source (cascade /usr/local) - for Mint 22.2
# Order: libplist -> libimobiledevice-glue -> libusbmuxd -> libtatsu -> libimobiledevice -> usbmuxd
# PKG_CONFIG_PATH ensures each step finds previous libs
mkdir -p ~/src/imd && cd ~/src/imd
# 0) Install build dependencies
sudo apt update
sudo apt install -y git build-essential pkg-config autoconf automake libtool-bin \
libusb-1.0-0-dev libssl-dev udev libcurl4-openssl-dev
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
# 1) libplist
git clone https://github.com/libimobiledevice/libplist.git
cd libplist
./autogen.sh --prefix=/usr/local
make -j"$(nproc)"
sudo make install
sudo ldconfig
cd ..
# 2) libimobiledevice-glue
git clone https://github.com/libimobiledevice/libimobiledevice-glue.git
cd libimobiledevice-glue
./autogen.sh --prefix=/usr/local
make -j"$(nproc)"
sudo make install
sudo ldconfig
cd ..
# 3) libusbmuxd
git clone https://github.com/libimobiledevice/libusbmuxd.git
cd libusbmuxd
./autogen.sh --prefix=/usr/local
make -j"$(nproc)"
sudo make install
sudo ldconfig
cd ..
# 4) libtatsu (new dependency for recent libimobiledevice)
git clone https://github.com/libimobiledevice/libtatsu.git
cd libtatsu
./autogen.sh --prefix=/usr/local
make -j"$(nproc)"
sudo make install
sudo ldconfig
cd ..
# 5) libimobiledevice (tools like idevicepair/ideviceinfo)
git clone https://github.com/libimobiledevice/libimobiledevice.git
cd libimobiledevice
./autogen.sh --prefix=/usr/local
make -j"$(nproc)"
sudo make install
sudo ldconfig
cd ..
# 6) usbmuxd (the daemon)
git clone https://github.com/libimobiledevice/usbmuxd.git
cd usbmuxd
./autogen.sh --prefix=/usr/local --sysconfdir=/etc --localstatedir=/var --runstatedir=/run
make -j"$(nproc)"
sudo make install
sudo ldconfigEdit service for custom binary + NCM mode:
sudo systemctl edit --full usbmuxd[Unit]
Description=Socket daemon for the usbmux protocol used by Apple devices
Documentation=man:usbmuxd(8)
[Service]
Environment=USBMUXD_DEFAULT_DEVICE_MODE=3
ExecStart=/usr/local/sbin/usbmuxd --user usbmux --systemd
PIDFile=/run/usbmuxd.pidsudo systemctl daemon-reload
sudo systemctl enable --now usbmuxdWhy the NetworkManager bridge is essential: At this point you have USB link-layer connectivity (L2), but no IP addresses or routable network (L3). The iPad exposes raw CDC-NCM interfaces without any DHCP server or IP configuration - and since iPadOS shows zero UI changes, you can't manually assign IPs either. NetworkManager's shared mode provides the missing DHCP server + NAT routing to create the actual IP network bridge between your Linux Mint host and iPad.
Connect iPad via USB-C. Check dmesg | grep -i Apple - two CDC-NCM interfaces appear:
- "Apple Tethering" (
enx...d0): Main data networking interface for Linux↔iPad traffic. - "Apple Private" (
enx...d1, MACfe:7d:5e:22:5c:e0): iOS internal services - mark unmanaged.
- Identify Tethering iface:
ip link | grep -E '^[0-9]+: enx'
dmesg | grep -i 'Apple Tethering'- Create connection:
IF=enxa2b40fe978d0
nmcli con down iPad-USB-Tethering 2>/dev/null || true
nmcli con del iPad-USB-Tethering 2>/dev/null || true
nmcli con add con-name iPad-USB-Tethering type ethernet ifname "$IF" ipv4.method shared ipv6.method shared
nmcli con up iPad-USB-Tethering- Block "Apple Private":
sudo tee /etc/NetworkManager/conf.d/99-unmanaged-ipad-private.conf >/dev/null <<'EOF'
[keyfile]
unmanaged-devices=mac:fe:7d:5e:22:5c:e0
EOF
sudo systemctl restart NetworkManageriPad gets IP (10.42.0.x), host 10.42.0.1. Manually add USB IP in Moonlight iPad client. Minimal latency, perfect for offline travel.
Aquí tienes una reformulación más profesional y técnica, ideal para documentación (tipo README.md en GitHub) o un artículo de blog.
He mejorado la gramática, el tono y la claridad, separando la configuración "básica" de la "avanzada" (el script).
To extend your desktop rather than mirroring the main screen, you need a separate video output. The most reliable method on Linux (specifically X11) is using a hardware HDMI Dummy Plug. This forces the GPU to render a second independent screen that Sunshine can capture.[1]
Once the dummy plug is connected to your laptop/GPU:
- Identify the output name (e.g.,
DP-3,HDMI-1) usingxrandr. - In the Sunshine Web UI, go to Configuration > Audio/Video.
- Set Output Name to your dummy plug's ID.
This will provide a standard 1080p extended display.
To utilize the full potential of your client device (e.g., an iPad's 4:3 aspect ratio), you need to force a custom resolution that matches the client, rather than the default 1080p of the dummy plug.
Since dummy plugs often lack specific EDID modes for tablets, use the following script to inject a custom modeline into Xrandr automatically.
Recommendation: Add this script to the Prep Commands in your Sunshine application settings so it runs automatically every time you connect.
#!/bin/bash
# Configuration
DEVICE="DP-3"
MAIN_DISPLAY="eDP-1"
# Defaults
# Note: Defaults set to "logical" resolution (approx 1/2 of retina) for better scaling
W=${SUNSHINE_CLIENT_WIDTH:-1180}
H=${SUNSHINE_CLIENT_HEIGHT:-820}
R=${SUNSHINE_CLIENT_FPS:-60}
# Mode name
MODE_NAME="${W}x${H}_${R}"
echo "Sunshine: Attempting to configure $DEVICE to $MODE_NAME..."
# 1. Generate parameters with cvt
CVT_OUTPUT=$(cvt "$W" "$H" "$R")
# Extract everything after "Modeline "
MODELINE_PARAMS=$(echo "$CVT_OUTPUT" | sed -n 's/^.*Modeline "\([^"]*\)"/\1/p' | sed 's/^[^ ]* //')
# If sed failed (because cvt output is different), use the brute force method:
if [ -z "$MODELINE_PARAMS" ]; then
# Alternative parsing attempt
MODELINE_PARAMS=$(echo "$CVT_OUTPUT" | grep Modeline | cut -d' ' -f3-)
fi
echo " -> Calculated params: $MODELINE_PARAMS"
# 2. Register the mode (only if it doesn't exist)
# Note: --newmode needs the name "ModeName" followed by the params
if ! xrandr --query | grep -q " $MODE_NAME "; then
echo " -> Creating mode in system..."
xrandr --newmode "$MODE_NAME" $MODELINE_PARAMS
else
echo " -> Mode $MODE_NAME already exists in system."
fi
# 3. Add mode to connector (if not already present)
# First check if the connector already has this mode available
if ! xrandr --query | grep -A 20 "^$DEVICE" | grep -q " $MODE_NAME "; then
echo " -> Adding mode to $DEVICE..."
xrandr --addmode "$DEVICE" "$MODE_NAME"
fi
# 4. Activate output
echo " -> Activating output..."
xrandr --output "$DEVICE" --mode "$MODE_NAME" --auto
# Verify success
if xrandr --query | grep "^$DEVICE connected" | grep "$MODE_NAME"; then
echo "Success: $DEVICE is active at $MODE_NAME"
exit 0
else
echo "Error: Could not activate mode."
exit 1
fi