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 <noreply@anthropic.com>
This commit is contained in:
commit
4d30b9d55a
2 changed files with 233 additions and 0 deletions
41
README.md
Normal file
41
README.md
Normal file
|
|
@ -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).
|
||||||
192
omatether.sh
Executable file
192
omatether.sh
Executable file
|
|
@ -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 <install|pair|unpair|status|priority|uninstall|help>
|
||||||
|
#
|
||||||
|
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 <<EOF
|
||||||
|
# Installed by omatether — iPhone USB tethering.
|
||||||
|
# Matches by driver so it wins over 20-ethernet.network's Name=eth* glob
|
||||||
|
# (networkd applies the lexically-first matching file).
|
||||||
|
[Match]
|
||||||
|
Driver=ipheth
|
||||||
|
|
||||||
|
[Link]
|
||||||
|
# Never block boot / network-online.target waiting for a phone.
|
||||||
|
RequiredForOnline=no
|
||||||
|
|
||||||
|
[Network]
|
||||||
|
DHCP=yes
|
||||||
|
|
||||||
|
# DNS is global-static via systemd-resolved on Omarchy; match the
|
||||||
|
# UseDNS=no convention of the stock 20-*.network files.
|
||||||
|
[DHCPv4]
|
||||||
|
UseDNS=no
|
||||||
|
RouteMetric=${metric}
|
||||||
|
|
||||||
|
[IPv6AcceptRA]
|
||||||
|
UseDNS=no
|
||||||
|
RouteMetric=${metric}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_networkd() {
|
||||||
|
as_root networkctl reload
|
||||||
|
local iface
|
||||||
|
if iface=$(tether_iface); then
|
||||||
|
as_root networkctl reconfigure "$iface" || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_install() {
|
||||||
|
say "Installing iPhone USB tethering support…"
|
||||||
|
as_root pacman -S --needed --noconfirm usbmuxd libimobiledevice
|
||||||
|
ok "usbmuxd + libimobiledevice installed (usbmuxd starts on demand via udev)"
|
||||||
|
|
||||||
|
write_netfile "$METRIC_FALLBACK"
|
||||||
|
reload_networkd
|
||||||
|
ok "networkd profile written: $NETFILE (fallback priority, metric $METRIC_FALLBACK)"
|
||||||
|
|
||||||
|
# Nudge udev in case the phone was already plugged in before install.
|
||||||
|
as_root udevadm trigger -s usb --action=add 2>/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 <high|low>"
|
||||||
|
;;
|
||||||
|
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
|
||||||
Loading…
Add table
Reference in a new issue