#!/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). # # Решение: # - recover: DHCP → сброс маршрутов → joplin-hotspot-alias repair (setmanual *.100 + текущий gw). # - Stale Manual (старый шлюз) — снимается DHCP на шаге 3, затем setmanual с актуальным gw. # # Из любой папки (одна команда, чинит интернет + Joplin *.100): # bash /Users/eternal/code/local_machine/docs/connectivity/scripts/macos-hotspot-network-recover.sh recover # # Переменные: # HOTSPOT_JOPLIN_MODE=static|alias|manual|off static (по умолчанию при env), alias/manual legacy, 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 [ -f "$JOPLIN_ENV" ] && echo static || 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 </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 static|alias) echo "6) Joplin: primary *.100 (setmanual)..." if [ -x "$JOPLIN_ALIAS_SCRIPT" ]; then "$JOPLIN_ALIAS_SCRIPT" repair || "$JOPLIN_ALIAS_SCRIPT" ensure || true elif [ "$mode" = alias ]; then 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