Add Joplin, old Mac, and Fedora WiFi runbooks; simplify JA3 Whisky scripts and hotspot recovery.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ahauimix
2026-06-19 22:26:33 +03:00
parent 1938b7c743
commit 4bc9629a09
381 changed files with 3442 additions and 773 deletions

View File

@@ -1,32 +1,244 @@
#!/bin/sh
# Сброс сетевого стека macOS после смены хотспота / «интернет не поднимается без перезагрузки».
# Типичная причина в связке с docs/connectivity: системный PAC на localhost:1089 при мёртвом/ещё не
# поднятом PAC-сервере + кэш DNS / WiFi. См. HOTSPOT_NETWORK_EMERGENCY.md
# Сброс сети после переподключения Android-хотспота (macOS).
#
# Использование (из корня репозитория):
# ./docs/connectivity/scripts/macos-hotspot-network-recover.sh status
# ./docs/connectivity/scripts/macos-hotspot-network-recover.sh recover
# RCA (почему старый recover не помогал):
# - Ядерный сброс (route flush, ifconfig down) выполнялся только при sudo -n (passwordless).
# - Без NOPASSWD в sudoers скрипт делал лишь toggle WiFi — ARP REJECT и битые маршруты оставались.
# - Manual IPv4 на WiFi (Joplin 10.219.227.100) конфликтует с новой подсетью хотспота (10.36.x).
#
# Решение:
# - WiFi всегда 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
#
# Переменные:
# MACOS_PAC_SERVICES — как в macos-system-pac.sh (по умолчанию Wi-Fi|Thunderbolt Bridge 2)
# WIFI_DEVICE например en0; иначе авто: первый Device у Hardware Port: Wi-Fi
# 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
set -e
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
# "Device: en0" под блоком "Hardware Port: Wi-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 " WiFi Manual $(wifi_manual_ip)$(wifi_manual_router) — переводим на DHCP..."
networksetup -setdhcp "Wi-Fi" 2>/dev/null || sudo_run networksetup -setdhcp "Wi-Fi"
else
echo " WiFi уже 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() {
printf '%s\n' "Usage: $(basename "$0") status|recover" >&2
cat <<EOF
Usage: $(basename "$0") status|recover
recover — PAC/SOCKS off, DHCP, ядерный сброс (sudo), Joplin alias *.100
status — диагностика
Полная команда из любой папки:
bash $SCRIPT_SELF recover
EOF
exit 1
}
@@ -35,69 +247,70 @@ cmd=$1
case "$cmd" in
status)
echo "=== Wi-Fi device (hardware) ==="
echo "=== Wi-Fi device ==="
wifi_device || true
echo "=== networksetup -listallnetworkservices (excerpt) ==="
networksetup -listallnetworkservices 2>/dev/null | head -15 || true
echo "=== PAC / autoproxy (Wi-Fi + Thunderbolt Bridge 2 if present) ==="
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 firewall (Wi-Fi) ==="
echo "=== SOCKS (Wi-Fi) ==="
networksetup -getsocksfirewallproxy "Wi-Fi" 2>/dev/null || true
echo "=== Web proxy (Wi-Fi) ==="
networksetup -getwebproxy "Wi-Fi" 2>/dev/null || true
echo "=== Listener on 1089 (PAC URL default) ==="
if lsof -i :1089 -sTCP:LISTEN 2>/dev/null | head -5; then
:
else
echo "(none — при Enabled PAC в Safari/системе возможны сбои)"
fi
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 -12 || true
echo "=== Quick reachability (ICMP may be blocked on some hotspots) ==="
ping -c 1 -W 2000 1.1.1.1 >/dev/null 2>&1 && echo "ping 1.1.1.1: ok" || echo "ping 1.1.1.1: fail"
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 "1) Отключаем автоматический PAC на выбранных сервисах..."
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
else
echo "WARN: нет $PAC_SCRIPT — вручную: Системные настройки → Сеть → WiFi → Подробнее → Прокси → снять «Автоматическая настройка прокси»" >&2
fi
echo "2) Отключаем системный SOCKS-прокси на Wi-Fi (если был включён Shadowsocks и т.п.)..."
if networksetup -listallnetworkservices 2>/dev/null | grep -Fqx "Wi-Fi"; then
networksetup -setsocksfirewallproxystate "Wi-Fi" off 2>/dev/null || true
fi
echo "3) Сброс DNS-кэша (нужен sudo)..."
if sudo -n true 2>/dev/null; then
sudo dscacheutil -flushcache 2>/dev/null || true
sudo killall -HUP mDNSResponder 2>/dev/null || true
echo " mDNSResponder перезагружен."
else
echo " Пропуск (нет sudo без пароля). Выполните вручную:"
echo " sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder"
fi
echo "2) SOCKS off..."
networksetup -setsocksfirewallproxystate "Wi-Fi" off 2>/dev/null || true
echo "3) WiFi → DHCP (снимаем конфликтный Manual Joplin)..."
ensure_dhcp_wifi
dev="$(wifi_device)"
if [ -n "$dev" ]; then
echo "4) Обновление DHCP на $dev (нужен sudo)..."
if sudo -n true 2>/dev/null; then
sudo ipconfig set "$dev" DHCP 2>/dev/null || true
else
echo " Пропуск. Вручную: sudo ipconfig set $dev DHCP"
fi
echo "5) Краткое выключение/включение WiFi радиомодуля ($dev)..."
networksetup -setairportpower "$dev" off 2>/dev/null || true
sleep 2
networksetup -setairportpower "$dev" on 2>/dev/null || true
else
echo "45) Не удалось определить WiFi device — пропуск DHCP/airport power. Вручную выключите/включите WiFi в меню."
if [ -z "$dev" ]; then
echo "Ошибка: WiFi device не найден" >&2
exit 1
fi
echo "=== Готово. Проверка: ==="
"$0" status
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 WiFi (legacy)..."
setup_joplin_manual || true
;;
*)
echo "6) Joplin: пропуск (нет $JOPLIN_ENV)"
;;
esac
echo ""
echo "Если интернет всё ещё нет: откройте хотспот в браузере (captive portal), затем при необходимости снова включите PAC:"
echo " ./docs/connectivity/scripts/macos-pac-on.sh"
echo "=== Итог ==="
"$SCRIPT_SELF" status
echo ""
echo "Captive portal: http://neverssl.com"
;;
*)
usage