commit 4d30b9d55a9690a71e41b7a1fef6d017315ea3fd Author: 28allday Date: Fri Jul 3 19:26:31 2026 +0100 omatether v0.1.0 — iPhone USB tethering for Omarchy/Arch usbmuxd pairing + ipheth + systemd-networkd profile (Driver=ipheth match, fallback metric 750, RequiredForOnline=no). Commands: install/pair/unpair/ status/priority/uninstall. Co-Authored-By: Claude Fable 5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..bca0d54 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# omatether + +iPhone USB tethering for **Omarchy / Arch** (systemd-networkd + iwd stack). +Plug an iPhone in over USB and use its internet connection — the phone's +Wi-Fi if it's on one, cellular otherwise. Fully headless, no GUI required. + +## How it works + +- `usbmuxd` handles the iOS pairing/trust handshake (udev-activated, no service to enable) +- the kernel's `ipheth` driver exposes the phone as a normal network interface +- a `systemd-networkd` profile (`10-iphone-tether.network`) matches by + `Driver=ipheth` and DHCPs it, at **fallback priority** (route metric 750) + so it never hijacks ethernet (100) or Wi-Fi (600) + +## Install + +```bash +./omatether.sh install # packages + networkd profile +./omatether.sh pair # unlock phone, tap Trust +``` + +Then on the phone: **Settings → Personal Hotspot → Allow Others to Join: ON**. +The connection comes up automatically whenever the phone is plugged in. + +## Commands + +| Command | What it does | +|---|---| +| `install` | Install usbmuxd/libimobiledevice, write networkd profile | +| `pair` / `unpair` | Manage the Trust pairing | +| `status` | Device, interface, IP, route, live connectivity test | +| `priority high` | Route all traffic via the phone even when ethernet is up | +| `priority low` | Back to fallback-only (default) | +| `uninstall` | Remove the networkd profile | + +## Notes + +- iOS shares whatever uplink it has: on Wi-Fi it shares the Wi-Fi; otherwise cellular. +- The tether interface usually appears as `eth0`; the profile matches by driver, not name. +- `RequiredForOnline=no` — an unplugged phone never blocks boot. +- DNS follows the Omarchy convention (`UseDNS=no`; global resolvers via systemd-resolved). diff --git a/omatether.sh b/omatether.sh new file mode 100755 index 0000000..5f44592 --- /dev/null +++ b/omatether.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# +# omatether — iPhone USB tethering for Omarchy / Arch (systemd-networkd + iwd) +# +# Plug an iPhone in over USB and use its internet connection (the phone's +# Wi-Fi, or cellular if it isn't on Wi-Fi). Works headless — no GUI needed. +# +# Usage: omatether.sh +# +set -euo pipefail + +NETFILE="/etc/systemd/network/10-iphone-tether.network" + +# Route metrics: Omarchy ships ethernet=100, wifi=600, wwan=700. +METRIC_FALLBACK=750 # tether used only when nothing better is up (default) +METRIC_PREFERRED=50 # tether beats ethernet/wifi while plugged in + +RED=$'\e[31m'; GRN=$'\e[32m'; YLW=$'\e[33m'; RST=$'\e[0m' +say() { printf '%s\n' "$*"; } +ok() { printf '%s\n' "${GRN}✓${RST} $*"; } +warn() { printf '%s\n' "${YLW}!${RST} $*"; } +die() { printf '%s\n' "${RED}✗${RST} $*" >&2; exit 1; } + +as_root() { + if [[ $EUID -eq 0 ]]; then "$@"; else sudo "$@"; fi +} + +# Find the network interface backed by the ipheth driver, if any. +tether_iface() { + local dev drv + for dev in /sys/class/net/*; do + drv=$(readlink -f "$dev/device/driver" 2>/dev/null) || continue + [[ ${drv##*/} == ipheth ]] && { basename "$dev"; return 0; } + done + return 1 +} + +write_netfile() { + local metric=$1 + as_root tee "$NETFILE" >/dev/null </dev/null || true + + say "" + say "Next: unlock the iPhone, then run: $0 pair" +} + +cmd_pair() { + command -v idevicepair >/dev/null || die "libimobiledevice not installed — run: $0 install" + idevice_id -l 2>/dev/null | grep -q . || die "No iPhone detected over USB. Plug it in and try again." + + say "Pairing — unlock the iPhone and tap ${GRN}Trust${RST} when prompted…" + local out + if out=$(idevicepair pair 2>&1); then + ok "$out" + else + case $out in + *passcode*) die "Phone is locked. Unlock it, then re-run: $0 pair" ;; + *user\ denied*|*denied\ the\ trust*) die "Trust was declined on the phone. Re-run and tap Trust." ;; + *Please\ accept*) warn "Trust dialog is showing on the phone — tap Trust, then re-run: $0 pair"; exit 1 ;; + *) die "$out" ;; + esac + fi + + say "" + say "Now enable the hotspot on the phone:" + say " Settings → Personal Hotspot → Allow Others to Join: ON" + say "The connection comes up automatically. Check with: $0 status" +} + +cmd_unpair() { + idevicepair unpair && ok "Unpaired." +} + +cmd_status() { + local iface udid name + + if udid=$(idevice_id -l 2>/dev/null | head -1) && [[ -n $udid ]]; then + name=$(ideviceinfo -k DeviceName 2>/dev/null || echo "unknown") + ok "iPhone connected over USB: $name" + if idevicepair validate >/dev/null 2>&1; then + ok "Paired (trusted)" + else + warn "Not paired — run: $0 pair" + fi + else + warn "No iPhone detected over USB" + fi + + if iface=$(tether_iface); then + local state addr + state=$(networkctl status "$iface" 2>/dev/null | awk '/State:/{print $2; exit}') + ok "Tether interface: $iface ($state)" + addr=$(ip -4 -br addr show "$iface" | awk '{print $3}') + if [[ -n $addr ]]; then + ok "IPv4: $addr" + ip route show default dev "$iface" | sed 's/^/ /' + if curl -sf --interface "$iface" --max-time 5 -o /dev/null https://www.google.com; then + ok "Internet via $iface works" + else + warn "No internet through $iface yet (is Personal Hotspot on?)" + fi + else + warn "No IP yet — enable Personal Hotspot on the phone (Allow Others to Join)" + fi + else + warn "No tether interface — phone unplugged, untrusted, or hotspot off" + fi +} + +cmd_priority() { + [[ -f $NETFILE ]] || die "Not installed — run: $0 install" + case ${1:-} in + high) + write_netfile "$METRIC_PREFERRED" + reload_networkd + ok "Tether now PREFERRED (metric $METRIC_PREFERRED) — traffic routes via the phone when plugged in" + ;; + low) + write_netfile "$METRIC_FALLBACK" + reload_networkd + ok "Tether now FALLBACK (metric $METRIC_FALLBACK) — ethernet/wifi win when available" + ;; + *) + local cur + cur=$(awk -F= '/^RouteMetric/{print $2; exit}' "$NETFILE") + say "Current metric: $cur (ethernet=100, wifi=600)" + say "Usage: $0 priority " + ;; + esac +} + +cmd_uninstall() { + as_root rm -f "$NETFILE" + as_root networkctl reload + ok "Removed $NETFILE (usbmuxd/libimobiledevice left installed)" +} + +case ${1:-help} in + install) cmd_install ;; + pair) cmd_pair ;; + unpair) cmd_unpair ;; + status) cmd_status ;; + priority) shift; cmd_priority "${1:-}" ;; + uninstall) cmd_uninstall ;; + help|-h|--help) + sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//' + ;; + *) die "Unknown command: $1 (try: $0 help)" ;; +esac