wren-linux / bin / tether
#!/bin/bash
# tether: written by Wren Linux parts/tether.sh
# Uses a phone's mobile data through its USB cable. Plug the phone in and turn on
# USB tethering on it (Android: Settings > Network > Hotspot & tethering; iPhone:
# tap Trust, then Personal Hotspot, with wrenctl install phone-tethering), then:
# tether finds the phone, shows it, asks, and connects
# tether --list only shows what it finds
# tether --watch sway starts this: while you are offline, a phone appearing
# opens a window that runs tether, once per plug-in
# It never connects unless you say so, since any USB device can claim to be a network
# card, and whoever runs that network chooses your route and may choose your DNS. It
# only looks at USB devices with the drivers phones use (USB network adapters use some of
# them too, so read what it shows you). With NetworkManager running it asks that; without
# it, the DHCP client there is, with your sudo password. Either way it sends no name for
# this computer, and leaves secure DNS (parts/secure-dns.sh) alone.
set -uo pipefail
# (dhcpcd and dhclient are in sbin, which a normal user's PATH doesn't have; a test
# gives its own pretend programs)
export PATH=${WREN_TETHER_PATH:-/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}
# (a test points these elsewhere)
SYS=${WREN_SYS:-/sys}
RESOLV=${WREN_RESOLV:-/etc/resolv.conf}
ETC=${WREN_ETC:-/etc}
COOLDOWN=${WREN_TETHER_COOLDOWN:-30}
DRIVERS='rndis_host cdc_ether cdc_ncm ipheth'
say() { printf '%s\n' "$*"; }
die() { printf 'tether: %s\n' "$*" >&2; exit 1; }
# driver_of IF: its driver, if it is a USB network device with no radio
driver_of() {
local d=$SYS/class/net/$1
[ -e "$d/device/driver" ] && [ ! -e "$d/wireless" ] || return 1
# a USB network device's parent chain goes through a USB interface
[ "$(basename "$(readlink -f "$d/device/subsystem")")" = usb ] || return 1
basename "$(readlink -f "$d/device/driver")"
}
# phones: the interfaces with a phone's driver, one per line: NAME DRIVER
phones() {
local d n drv
for d in "$SYS"/class/net/*; do
[ -e "$d" ] || continue
n=${d##*/}
[[ $n =~ ^[A-Za-z0-9_.-]{1,15}$ ]] || continue
drv=$(driver_of "$n") || continue
case " $DRIVERS " in *" $drv "*) printf '%s %s\n' "$n" "$drv" ;; esac
done
}
# online: a default route exists
online() { [ -n "$(ip -4 route show default 2>/dev/null)" ] || [ -n "$(ip -6 route show default 2>/dev/null)" ]; }
secure_dns_on() { [ ! -L "$RESOLV" ] && grep -qsE '^# Written by Wren Linux (parts|extras)/secure-dns\.sh' "$RESOLV"; }
nm_running() { command -v nmcli >/dev/null && [ "$(nmcli -t -f RUNNING general 2>/dev/null)" = running ]; }
has_ipv4() { ip -4 -o addr show dev "$1" scope global 2>/dev/null | grep -q inet; }
# connect_nm IF: a connection for this one device that doesn't start by itself, and
# sends no name (its own setting, so that holds without parts/random-mac.sh). One
# from an earlier plug-in goes first: a phone's device name can change each time.
connect_nm() {
local con
while IFS= read -r con; do
case $con in tether-*) nmcli connection delete "$con" >/dev/null 2>&1 || true ;; esac
done < <(nmcli -g NAME connection show 2>/dev/null)
nmcli connection add type ethernet ifname "$1" con-name "tether-$1" connection.autoconnect no \
ipv4.dhcp-send-hostname no ipv6.dhcp-send-hostname no ipv6.ip6-privacy 2 >/dev/null &&
nmcli connection up "tether-$1" >/dev/null
}
# connect_dhcp IF: as root, the first client there is. Left to Debian's settings,
# dhcpcd and dhclient send this computer's name (seen in a DHCP server's log), so
# each gets a copy of those without that line, and nothing else changed: what else a
# client asks a network for can tell it apart, so it should ask what Debian's do. busybox
# sends no name.
connect_dhcp() {
local ifc=$1 opts=()
sudo ip link set "$ifc" up || return 1
if command -v dhcpcd >/dev/null; then
# (while secure DNS is on, resolv.conf stays pointed at it)
if secure_dns_on; then opts+=(--nohook resolv.conf); fi
{ grep -vE '^[[:space:]]*hostname[[:space:]]*$' "$ETC/dhcpcd.conf" 2>/dev/null; echo noipv4ll; } |
sudo tee /run/wren-tether-dhcpcd.conf >/dev/null
sudo dhcpcd -4 -w -t 30 -f /run/wren-tether-dhcpcd.conf "${opts[@]}" "$ifc"
elif command -v dhclient >/dev/null; then
# (dhclient's own script reads the same "make_resolv_conf" hook secure DNS sets)
{ grep -vE '^[[:space:]]*send host-name' "$ETC/dhcp/dhclient.conf" 2>/dev/null || true; } |
sudo tee /run/wren-tether-dhclient.conf >/dev/null
sudo dhclient -4 -1 -cf /run/wren-tether-dhclient.conf "$ifc"
elif command -v busybox >/dev/null; then
sudo tee /run/wren-tether-udhcpc.sh >/dev/null <<'SCRIPT'
#!/bin/sh
case $1 in
deconfig) ip -4 addr flush dev "$interface"; ip link set "$interface" up ;;
bound | renew)
ip -4 addr flush dev "$interface"
ip addr add "$ip/${mask:-24}" dev "$interface"
[ -z "$router" ] || ip route replace default via "${router%% *}" dev "$interface"
# (not while secure DNS points it at Unbound, or something else owns the file)
if [ -n "$dns" ] && [ ! -L /etc/resolv.conf ] &&
! grep -qE '^# Written by Wren Linux (parts|extras)/secure-dns\.sh' /etc/resolv.conf 2>/dev/null; then
for d in $dns; do echo "nameserver $d"; done >/etc/resolv.conf
fi ;;
esac
SCRIPT
sudo chmod 755 /run/wren-tether-udhcpc.sh
sudo busybox udhcpc -i "$ifc" -s /run/wren-tether-udhcpc.sh -n -q -t 10
else
die "there is no DHCP client here (dhcpcd, dhclient or busybox). Install one: sudo apt install dhcpcd-base"
fi
}
connect() {
if nm_running && [ "$(nmcli -t -f GENERAL.STATE device show "$1" 2>/dev/null)" != "GENERAL.STATE:10 (unmanaged)" ]; then
connect_nm "$1"
else
connect_dhcp "$1"
fi
}
ask_and_connect() {
local found n=0 name driver reply
found=$(phones)
if [ -z "$found" ]; then
say "No phone found. Plug it in with a USB cable that carries data, and turn on USB tethering."
say " Android: Settings > Network > Hotspot & tethering > USB tethering"
say " iPhone: tap Trust, then Settings > Personal Hotspot (needs: wrenctl install phone-tethering)"
return 1
fi
n=$(wc -l <<<"$found")
[ "$n" = 1 ] || die "more than one found; unplug the others:"$'\n'"$found"
read -r name driver <<<"$found"
say "Found a USB network device: $name ($driver)."
say "A phone looks like this, but so can other USB devices. Whoever runs it can see where your traffic goes."
online && say "You are online already; connecting may change the way your traffic goes."
read -r -p "Connect through it? [y/N] " reply </dev/tty || reply=
[[ $reply =~ ^[Yy] ]] || { say "Nothing was changed."; return 0; }
connect "$name" || die "couldn't get a connection through $name. Is tethering on, on the phone?"
say "Connected through $name."
}
# watch: one window per plug-in, only while offline. (No root: ip monitor is yours.)
watch() {
local name driver last_name='' last_at=0 now
[ -n "${SWAYSOCK:-}" ] || die "--watch runs in sway."
while IFS= read -r _; do
# (sway is gone: so is this, at the next change)
swaymsg -t get_version >/dev/null 2>&1 || exit 0
while read -r name driver; do
[ -n "$name" ] || continue
online && continue
now=$(date +%s)
[ "$name" = "$last_name" ] && [ $((now - last_at)) -lt "$COOLDOWN" ] && continue
last_name=$name last_at=$now
swaymsg exec "foot --app-id=wren-run --title='Phone tethering' $(readlink -f -- "$0") --hold" >/dev/null 2>&1
done <<<"$(phones)"
done < <(ip monitor link)
}
case ${1:-} in
"") ask_and_connect ;;
--list) phones ;;
--watch) watch ;;
--hold)
# in the watcher's window: what happened stays readable until Enter
ask_and_connect || true
read -r -p "Press Enter to close. " _ </dev/tty || true ;;
*) sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
esac