319 lines
11 KiB
Bash
Executable File
319 lines
11 KiB
Bash
Executable File
#!/bin/sh
|
||
# Сброс сети после переподключения Android-хотспота (macOS).
|
||
#
|
||
# RCA (почему старый recover не помогал):
|
||
# - Ядерный сброс (route flush, ifconfig down) выполнялся только при sudo -n (passwordless).
|
||
# - Без NOPASSWD в sudoers скрипт делал лишь toggle Wi‑Fi — ARP REJECT и битые маршруты оставались.
|
||
# - Manual IPv4 на Wi‑Fi (Joplin 10.219.227.100) конфликтует с новой подсетью хотспота (10.36.x).
|
||
#
|
||
# Решение:
|
||
# - Wi‑Fi всегда DHCP (правильный шлюз после reconnect).
|
||
# - Joplin: вторичный IP (alias) *.100 в текущей подсети — не трогает default route.
|
||
# - recover всегда запрашивает sudo (пароль) для полного сброса.
|
||
#
|
||
# Из любой папки (одна команда, чинит интернет + Joplin alias):
|
||
# bash /Users/eternal/code/local_machine/docs/connectivity/scripts/macos-hotspot-network-recover.sh recover
|
||
#
|
||
# Переменные:
|
||
# HOTSPOT_JOPLIN_MODE=alias|manual|off alias (по умолчанию при ~/.config/joplin-webdav/env), manual, off
|
||
# HOTSPOT_RECOVER_SKIP_JOPLIN=1 не трогать Joplin alias
|
||
# JOPLIN_STATIC_IP последний октет .100 из env; подсеть — текущая
|
||
# MACOS_PAC_SERVICES, WIFI_DEVICE
|
||
|
||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||
PAC_SCRIPT="$ROOT/macos-system-pac.sh"
|
||
JOPLIN_ENV="${JOPLIN_WEBDAV_CONFIG:-$HOME/.config/joplin-webdav/env}"
|
||
JOPLIN_ALIAS_SCRIPT="${JOPLIN_ALIAS_SCRIPT:-$(cd "$ROOT/../../apps/joplin/scripts" 2>/dev/null && pwd)/joplin-hotspot-alias.sh}"
|
||
SCRIPT_SELF="$ROOT/$(basename "$0")"
|
||
|
||
wifi_device() {
|
||
if [ -n "${WIFI_DEVICE:-}" ]; then
|
||
printf '%s\n' "$WIFI_DEVICE"
|
||
return
|
||
fi
|
||
networksetup -listallhardwareports 2>/dev/null \
|
||
| awk '/Hardware Port: Wi-Fi$/ {p=1; next} p && /^Device: / {print $2; exit}'
|
||
}
|
||
|
||
ipv4_subnet() {
|
||
printf '%s\n' "$1" | awk -F. '{print $1"."$2"."$3}'
|
||
}
|
||
|
||
ipv4_last_octet() {
|
||
printf '%s\n' "$1" | awk -F. '{print $4}'
|
||
}
|
||
|
||
load_joplin_env() {
|
||
if [ -f "$JOPLIN_ENV" ]; then
|
||
# shellcheck disable=SC1090
|
||
. "$JOPLIN_ENV"
|
||
fi
|
||
}
|
||
|
||
wifi_config_method() {
|
||
networksetup -getinfo "Wi-Fi" 2>/dev/null | awk 'NR==1 {print $1; exit}'
|
||
}
|
||
|
||
wifi_manual_router() {
|
||
networksetup -getinfo "Wi-Fi" 2>/dev/null | awk -F': ' '/^Router: / {print $2; exit}'
|
||
}
|
||
|
||
wifi_manual_ip() {
|
||
networksetup -getinfo "Wi-Fi" 2>/dev/null | awk -F': ' '/^IP address: / {print $2; exit}'
|
||
}
|
||
|
||
default_gateway() {
|
||
route -n get default 2>/dev/null | awk '/gateway:/{print $2; exit}'
|
||
}
|
||
|
||
joplin_mode() {
|
||
if [ "${HOTSPOT_RECOVER_SKIP_JOPLIN:-0}" = "1" ]; then
|
||
echo off
|
||
return
|
||
fi
|
||
if [ -n "${HOTSPOT_JOPLIN_MODE:-}" ]; then
|
||
echo "$HOTSPOT_JOPLIN_MODE"
|
||
return
|
||
fi
|
||
# mDNS (joplin-mac.local) — URL на телефоне не зависит от подсети; alias не нужен
|
||
echo off
|
||
}
|
||
|
||
joplin_target_ip() {
|
||
load_joplin_env
|
||
gw="$(default_gateway 2>/dev/null || true)"
|
||
[ -n "$gw" ] || return 1
|
||
subnet="$(ipv4_subnet "$gw")"
|
||
if [ -n "${JOPLIN_STATIC_IP:-}" ]; then
|
||
last="$(ipv4_last_octet "$JOPLIN_STATIC_IP")"
|
||
else
|
||
last=100
|
||
fi
|
||
printf '%s\n' "${subnet}.${last}"
|
||
}
|
||
|
||
list_en0_alias_ips() {
|
||
dev="${1:-en0}"
|
||
ifconfig "$dev" 2>/dev/null | awk '/inet / && /alias/ {print $2}'
|
||
}
|
||
|
||
sudo_run() {
|
||
echo " \$ sudo $*"
|
||
sudo "$@"
|
||
}
|
||
|
||
ensure_dhcp_wifi() {
|
||
method="$(wifi_config_method 2>/dev/null || true)"
|
||
if [ "$method" = "Manual" ]; then
|
||
echo " Wi‑Fi Manual $(wifi_manual_ip) → $(wifi_manual_router) — переводим на DHCP..."
|
||
networksetup -setdhcp "Wi-Fi" 2>/dev/null || sudo_run networksetup -setdhcp "Wi-Fi"
|
||
else
|
||
echo " Wi‑Fi уже DHCP (или не Manual)."
|
||
fi
|
||
}
|
||
|
||
nuclear_network_reset() {
|
||
dev="$1"
|
||
echo " Ядерный сброс на $dev (нужен пароль sudo)..."
|
||
sudo_run ifconfig "$dev" down
|
||
sudo_run route -n flush
|
||
sudo_run dscacheutil -flushcache
|
||
sudo_run killall -HUP mDNSResponder
|
||
sudo_run ifconfig "$dev" up
|
||
sudo_run ipconfig set "$dev" DHCP
|
||
}
|
||
|
||
wait_for_connectivity() {
|
||
dev="$1"
|
||
i=0
|
||
while [ "$i" -lt 30 ]; do
|
||
gw="$(default_gateway 2>/dev/null || true)"
|
||
ip="$(ipconfig getifaddr "$dev" 2>/dev/null || true)"
|
||
if [ -n "$gw" ] && [ -n "$ip" ]; then
|
||
if ping -c 1 -W 1500 "$gw" >/dev/null 2>&1; then
|
||
echo " Связность OK: IP=$ip gateway=$gw (${i}s)"
|
||
return 0
|
||
fi
|
||
fi
|
||
sleep 1
|
||
i=$((i + 1))
|
||
done
|
||
echo " WARN: gateway/IP не поднялись за 30с — проверьте captive portal" >&2
|
||
return 1
|
||
}
|
||
|
||
remove_joplin_aliases() {
|
||
dev="$1"
|
||
target="$2"
|
||
for ip in $(list_en0_alias_ips "$dev"); do
|
||
case "$ip" in
|
||
*.*.*.100|"$target")
|
||
echo " Удаляем alias $ip на $dev"
|
||
sudo_run ifconfig "$dev" inet "$ip" -alias 2>/dev/null || true
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
setup_joplin_alias() {
|
||
dev="$1"
|
||
target="$(joplin_target_ip 2>/dev/null || true)"
|
||
[ -n "$target" ] || { echo " Joplin alias: нет gateway" >&2; return 1; }
|
||
primary="$(ipconfig getifaddr "$dev" 2>/dev/null || true)"
|
||
if [ "$primary" = "$target" ]; then
|
||
echo " Joplin: primary IP уже $target"
|
||
return 0
|
||
fi
|
||
remove_joplin_aliases "$dev" "$target"
|
||
if ifconfig "$dev" 2>/dev/null | grep -q "inet $target "; then
|
||
echo " Joplin alias $target уже на $dev"
|
||
return 0
|
||
fi
|
||
echo " Joplin alias: $target/24 на $dev (primary DHCP: ${primary:-?})"
|
||
sudo_run ifconfig "$dev" inet "$target" netmask 255.255.255.0 alias
|
||
echo " URL на телефоне: http://${target}:8080"
|
||
if [ -f "$JOPLIN_ENV" ] && [ -n "${JOPLIN_STATIC_IP:-}" ]; then
|
||
cur_sub="$(ipv4_subnet "$target")"
|
||
env_sub="$(ipv4_subnet "$JOPLIN_STATIC_IP")"
|
||
if [ "$cur_sub" != "$env_sub" ]; then
|
||
echo " ⚠ Подсеть сменилась ($env_sub.x → $cur_sub.x) — обновите URL в Joplin на телефоне"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
setup_joplin_manual() {
|
||
gw="$(default_gateway 2>/dev/null || true)"
|
||
[ -n "$gw" ] || return 1
|
||
target="$(joplin_target_ip 2>/dev/null || true)"
|
||
echo " Joplin manual (legacy): $target → router $gw"
|
||
networksetup -setmanual "Wi-Fi" "$target" 255.255.255.0 "$gw"
|
||
}
|
||
|
||
print_manual_ipv4_status() {
|
||
info="$(networksetup -getinfo "Wi-Fi" 2>/dev/null || true)"
|
||
echo "$info" | head -5
|
||
method="$(printf '%s\n' "$info" | awk 'NR==1 {print $1; exit}')"
|
||
dev="$(wifi_device 2>/dev/null || echo en0)"
|
||
current_ip="$(ipconfig getifaddr "$dev" 2>/dev/null || true)"
|
||
gw="$(default_gateway 2>/dev/null || true)"
|
||
echo "--- runtime $dev ---"
|
||
echo "primary IP: ${current_ip:-(none)}"
|
||
echo "default gateway: ${gw:-(none)}"
|
||
aliases="$(list_en0_alias_ips "$dev" | tr '\n' ' ')"
|
||
if [ -n "$aliases" ]; then
|
||
echo "aliases: $aliases"
|
||
fi
|
||
if [ -f "$JOPLIN_ENV" ]; then
|
||
target="$(joplin_target_ip 2>/dev/null || true)"
|
||
echo "joplin target: ${target:-?} (mode: $(joplin_mode))"
|
||
if [ -n "$target" ] && ! ifconfig "$dev" 2>/dev/null | grep -q "inet $target "; then
|
||
echo "⚠ Joplin IP $target не на интерфейсе — телефон не достучится"
|
||
fi
|
||
fi
|
||
if [ "$method" = "Manual" ]; then
|
||
manual_ip="$(printf '%s\n' "$info" | awk -F': ' '/^IP address: / {print $2; exit}')"
|
||
manual_router="$(printf '%s\n' "$info" | awk -F': ' '/^Router: / {print $2; exit}')"
|
||
if [ -n "$manual_router" ] && [ -n "$gw" ]; then
|
||
if [ "$(ipv4_subnet "$manual_router")" != "$(ipv4_subnet "$gw")" ]; then
|
||
echo "⚠ MISMATCH: Manual router $(ipv4_subnet "$manual_router").x vs gateway $(ipv4_subnet "$gw").x"
|
||
echo " → запустите: bash $SCRIPT_SELF recover"
|
||
elif [ -n "$current_ip" ] && [ "$current_ip" != "$manual_ip" ]; then
|
||
echo "⚠ MISMATCH: Manual IP $manual_ip, runtime $current_ip"
|
||
fi
|
||
fi
|
||
fi
|
||
if [ -n "$gw" ]; then
|
||
echo "--- gateway $gw ---"
|
||
arp -n "$gw" 2>/dev/null || true
|
||
route -n get "$gw" 2>/dev/null | grep -E 'interface|flags' || true
|
||
fi
|
||
}
|
||
|
||
usage() {
|
||
cat <<EOF
|
||
Usage: $(basename "$0") status|recover
|
||
|
||
recover — PAC/SOCKS off, DHCP, ядерный сброс (sudo), Joplin alias *.100
|
||
status — диагностика
|
||
|
||
Полная команда из любой папки:
|
||
bash $SCRIPT_SELF recover
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[ "$#" -eq 1 ] || usage
|
||
cmd=$1
|
||
|
||
case "$cmd" in
|
||
status)
|
||
echo "=== Wi-Fi device ==="
|
||
wifi_device || true
|
||
echo "=== IPv4 ==="
|
||
print_manual_ipv4_status || true
|
||
echo "=== PAC ==="
|
||
if [ -x "$PAC_SCRIPT" ]; then
|
||
MACOS_PAC_SERVICES="${MACOS_PAC_SERVICES:-Wi-Fi|Thunderbolt Bridge 2}" "$PAC_SCRIPT" status || true
|
||
fi
|
||
echo "=== SOCKS (Wi-Fi) ==="
|
||
networksetup -getsocksfirewallproxy "Wi-Fi" 2>/dev/null || true
|
||
echo "=== PAC listener :1089 ==="
|
||
lsof -i :1089 -sTCP:LISTEN 2>/dev/null | head -3 || echo "(none)"
|
||
echo "=== Default route ==="
|
||
route -n get default 2>/dev/null | head -8 || echo "(no default route)"
|
||
gw="$(default_gateway 2>/dev/null || true)"
|
||
if [ -n "$gw" ]; then
|
||
ping -c 1 -W 2000 "$gw" >/dev/null 2>&1 && echo "ping gateway: ok" || echo "ping gateway: fail"
|
||
fi
|
||
curl -sS --max-time 5 -o /dev/null https://1.1.1.1 2>/dev/null && echo "curl: ok" || echo "curl: fail"
|
||
;;
|
||
recover)
|
||
echo "=== Hotspot recover (DHCP + route flush) ==="
|
||
echo "Скрипт запросит пароль sudo для сброса маршрутов."
|
||
echo ""
|
||
echo "1) PAC off..."
|
||
if [ -x "$PAC_SCRIPT" ]; then
|
||
MACOS_PAC_SERVICES="${MACOS_PAC_SERVICES:-Wi-Fi|Thunderbolt Bridge 2}" "$PAC_SCRIPT" off
|
||
fi
|
||
echo "2) SOCKS off..."
|
||
networksetup -setsocksfirewallproxystate "Wi-Fi" off 2>/dev/null || true
|
||
echo "3) Wi‑Fi → DHCP (снимаем конфликтный Manual Joplin)..."
|
||
ensure_dhcp_wifi
|
||
dev="$(wifi_device)"
|
||
if [ -z "$dev" ]; then
|
||
echo "Ошибка: Wi‑Fi device не найден" >&2
|
||
exit 1
|
||
fi
|
||
echo "4) Ядерный сброс $dev..."
|
||
nuclear_network_reset "$dev"
|
||
echo "5) Ожидание DHCP..."
|
||
wait_for_connectivity "$dev" || true
|
||
mode="$(joplin_mode)"
|
||
case "$mode" in
|
||
alias)
|
||
echo "6) Joplin: alias IP (не ломает маршрутизацию)..."
|
||
if [ -x "$JOPLIN_ALIAS_SCRIPT" ]; then
|
||
"$JOPLIN_ALIAS_SCRIPT" ensure || true
|
||
else
|
||
setup_joplin_alias "$dev" || true
|
||
fi
|
||
;;
|
||
manual)
|
||
echo "6) Joplin: manual Wi‑Fi (legacy)..."
|
||
setup_joplin_manual || true
|
||
;;
|
||
*)
|
||
echo "6) Joplin: пропуск (нет $JOPLIN_ENV)"
|
||
;;
|
||
esac
|
||
echo ""
|
||
echo "=== Итог ==="
|
||
"$SCRIPT_SELF" status
|
||
echo ""
|
||
echo "Captive portal: http://neverssl.com"
|
||
;;
|
||
*)
|
||
usage
|
||
;;
|
||
esac
|