omatether/omatether.sh
28allday b0374f87e7 Fix SIGPIPE under pipefail in status (awk-exit and head -1 pipelines)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:27:21 +01:00

193 lines
5.7 KiB
Bash
Executable file

#!/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
udid=$(idevice_id -l 2>/dev/null | head -1 || true)
if [[ -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}' || true)
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