#!/bin/bash # ProxysVPN - диагностика сети на Mac. # Статья: https://proxysvpn.com/guides/mac-vpn-diagnostics # Запуск: curl -fsSL https://proxysvpn.com/mac-audit.sh | bash # # Только читает настройки: ничего не меняет, пароль администратора не # спрашивает, сам никуда ничего не отправляет. Пишет один файл - # ~/Desktop/proxysvpn-audit.txt - и собирает данные дважды, без ProxysVPN и # с ним, чтобы поддержке было с чем сравнить. Адреса в интернете в файле # заменены на [ip], MAC-адреса на [mac], имя пользователя - на [user]. # # English: read-only macOS network diagnostics for ProxysVPN support. # Collects interfaces, routes, DNS, profiles, proxies, VPN configurations, # system extensions, launch items, matching processes, the firewall state # and four HTTP checks, twice (VPN off and VPN on), into one file on the # Desktop; public IPv4 addresses, MAC addresses and the user name are # masked; then prints a first reading of the VPN-on part. Everything sits # inside functions and main() runs last, so a download cut off halfway runs # nothing. Bash 3.2. The guide and the ProxysAI rules (src/lib/support-ai.ts) # read the same section names; tests/guide-mac-diagnostics.test.mjs pins them. # v2 (02.11.2026): the tunnel line always comes first in the reading, only a # connected foreign VPN configuration and every active foreign extension # count as «other programs», MAC addresses masked. # Progress lines go to fd 3, the screen: the collectors run with 2>&1 so # that their errors land in the file, and these lines must not. say() { printf '%s\n' "$*" >&3; } sec() { printf '\n== %s ==\n' "$1"; } # Where a public address goes. The app keeps the default route and lays # 0.0.0.0/1 and 128.0.0.0/1 over it (pvpn-platform net/macos.rs), so # `route get default` still says en0 while the tunnel is up. vpn_state() { case "$(route -n get 1.1.1.1 2>/dev/null | awk '/interface:/ {print $2; exit}')" in utun*|ipsec*|ppp*) echo on ;; *) echo off ;; esac } collect() { say "Собираю настройки сети..." sec system; sw_vers; date sec interfaces; ifconfig | grep -E '^(en|utun|bridge|ipsec|ppp)[0-9]+:|inet ' sec default-route; route -n get default 2>&1 | grep -E 'interface|gateway|flags'; echo '[to 8.8.8.8]'; route -n get 8.8.8.8 2>&1 | grep -E 'interface|gateway' sec routes; netstat -rn -f inet | head -45 sec dns; scutil --dns | grep -E 'resolver #|nameserver|if_index|flags|search' | head -40 sec dns-manual; networksetup -listallnetworkservices 2>/dev/null | tail -n +2 | while read -r s; do printf '%s: ' "$s"; networksetup -getdnsservers "$s" 2>&1 | tr '\n' ' '; echo; done; grep -v '^#' /etc/resolv.conf 2>/dev/null sec profiles; profiles list 2>&1 | head -20 sec dns-test; echo '[system]'; dscacheutil -q host -a name www.google.com; echo '[resolver]'; dig +time=4 +tries=1 +short www.google.com; echo '[direct 8.8.8.8]'; dig +time=4 +tries=1 +short @8.8.8.8 www.google.com sec proxy; scutil --proxy sec vpn-configs; scutil --nc list sec network-services; networksetup -listallnetworkservices sec system-extensions; systemextensionsctl list 2>&1 sec launch-items; ls /Library/LaunchDaemons /Library/LaunchAgents "$HOME/Library/LaunchAgents" 2>/dev/null sec processes; ps -axo comm | grep -i -E 'vpn|adguard|kaspersky|snitch|lulu|warp|proxy|xray|sing-box|hiddify|happ|incy|outline|wireguard|tunnel|filter|tun2socks|watafast|hysteria|cisco|fortinet|zscaler|nord|express|surfshark|amnezia|avast|avg|norton|eset|drweb|bitdefender|mcafee' | sort -u sec firewall; /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>&1; /usr/libexec/ApplicationFirewall/socketfilterfw --getstealthmode 2>&1 say "Проверяю доступ в интернет (до минуты)..." sec tunnel-tests curl -4 -s -m 15 -o /dev/null -w 'http-small %{http_code} %{time_total}s\n' http://captive.apple.com/hotspot-detect.html curl -4 -s -m 15 -o /dev/null -w 'https-small %{http_code} %{time_total}s\n' https://www.google.com/generate_204 curl -4 -s -m 25 -o /dev/null -w 'https-big %{http_code} %{size_download}B %{time_total}s\n' 'https://speed.cloudflare.com/__down?bytes=300000' curl -4 -s -m 15 -o /dev/null -w 'telegram %{http_code} %{time_total}s\n' https://core.telegram.org/ sec tls-handshake; curl -4 -sv -m 15 -o /dev/null https://www.youtube.com/ 2>&1 | grep -i -E 'connected to|TLS|SSL|HTTP/|timed out|error|closed' | head -8 sec done } # Public IPv4 addresses become [ip] (private, loopback, link-local, # multicast, 8.8.x.x and 1.1.x.x stay: they are not the person's); MAC # addresses of the neighbours in the routing table become [mac]; the macOS # user name becomes [user]. mask() { perl -pe 'BEGIN{$u=$ENV{USER}//""} s/\b(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\b/($1==10||$1==127||($1==192&&$2==168)||($1==172&&$2>=16&&$2<=31)||($1==169&&$2==254)||($1==198&&($2==18||$2==19))||$1==0||$1>=224||($1==8&&$2==8)||($1==1&&$2==1))?"$1.$2.$3.$4":"[ip]"/ge; s/\b(?:[0-9a-f]{1,2}:){5}[0-9a-f]{1,2}\b/[mac]/gi; s/\Q$u\E/[user]/g if length($u)>=3' } # The script usually arrives through a pipe (curl | bash), so the keyboard # is /dev/tty, not stdin. Fails where there is no terminal. A Return pressed # early (twice after the paste) is thrown away first, or it would answer the # prompt before the VPN was switched. flush_tty() { perl -e 'use POSIX; POSIX::tcflush(0, POSIX::TCIFLUSH())' < /dev/tty 2>/dev/null; } wait_return() { flush_tty; read -r _ < /dev/tty; } # One section of one part, without its header. section() { sed -n "/^== $2 ==\$/,/^== /{/^== /d;p;}" "$1"; } # The answer code of one tunnel test: 200, 204, 000 when nothing came back. code_of() { local c c=$(section "$1" tunnel-tests | awk -v n="$2" '$1==n {print $2; exit}') echo "${c:-000}" } # A line of the reading under «Что ещё заметно», whose heading is printed # before the first one. `noted` is global: verdict() prints the heading once. noted=0 note() { if [ "$noted" = 0 ]; then say "" say "Что ещё заметно (поддержка скажет, мешает ли это):" noted=1 fi say "$1" } # A first reading of the VPN-on part, on screen: the tunnel first, then what # else stands out. Support reads the file. verdict() { local on if [ "$first" = on ]; then on="$part1" elif [ "$second" = on ]; then on="$part2" else say "" say "VPN так и не был включён: предварительного вывода нет, файл всё равно пригодится поддержке." return 0 fi local http small big tg http=$(code_of "$on" http-small); small=$(code_of "$on" https-small) big=$(code_of "$on" https-big); tg=$(code_of "$on" telegram) local sys_ok=0 direct_ok=0 section "$on" dns-test | sed -n '/^\[system\]/,/^\[resolver\]/p' | grep -q 'ip_address:' && sys_ok=1 section "$on" dns-test | sed -n '/^\[direct/,$p' | grep -q '^\[ip\]' && direct_ok=1 local via via=$(section "$on" default-route | sed -n '/^\[to 8.8.8.8\]/,$p' | awk '/interface:/ {print $2; exit}') # Other products on the network: every active system extension that is # not ours, the processes of known filters and VPNs, and a VPN # configuration of another app that is connected right now (a disconnected # one is common and harmless). local others others=$({ section "$on" system-extensions | grep -i 'activated enabled' section "$on" processes | grep -i -E 'adguard|kaspersky|drweb|dr\.web|eset|norton|bitdefender|avast|avg|mcafee|snitch|lulu|warp|cisco|zscaler|fortinet|nordvpn|expressvpn|surfshark|amnezia|outline|wireguard|hiddify|sing-box|happ|incy|v2ray|v2box|clash' section "$on" vpn-configs | grep '(Connected)'; } \ | grep -v -i -E 'proxysvpn|watafast|pvpn' | sort -u | head -4) local proxies proxies=$(section "$on" proxy | grep -c 'Enable : 1') say "" say "Предварительный вывод (точнее скажет поддержка по файлу):" local tunnel=ok if [ "$http" = 000 ] && [ "$small" = 000 ] && [ "$big" = 000 ] && [ "$tg" = 000 ]; then say "- Через туннель не проходит ни один из четырёх запросов." tunnel=bad elif [ "$small" != 000 ] && [ "$big" = 000 ]; then say "- Небольшие запросы через туннель проходят, большой (300 КБ) - нет: похоже на фильтр или ограничение размера пакетов." tunnel=bad else say "- Все четыре проверки через туннель прошли: туннель работает. Если что-то всё же не открывается, напишите поддержке, что именно и в каком браузере." fi noted=0 case "$via" in utun*|ipsec*|ppp*) ;; *) note "- Путь в интернет идёт мимо туннеля (${via:-?}): туннель не поднялся до конца. Выключите и включите ProxysVPN." ;; esac if [ "$sys_ok" = 0 ] && [ "$direct_ok" = 1 ]; then note "- Системный DNS не отвечает, а напрямую имена разрешаются: уберите DNS-серверы из настроек Wi-Fi (Подробнее → DNS) или удалите профиль с DNS, затем включите ProxysVPN заново." fi if [ "$proxies" -gt 0 ]; then note "- В системе включён прокси: Системные настройки → Wi-Fi → Подробнее → Прокси → выключите все." fi if [ -n "$others" ]; then note "- Другие сетевые программы или фильтры:" printf '%s\n' "$others" | sed 's/^/ /' >&3 say " Если сайты не открываются, выключите их на время проверки (у антивируса - веб-защиту или сетевой фильтр) и включите ProxysVPN заново." fi if [ "$tunnel" = bad ] && [ "$noted" = 0 ]; then say "- В настройках ничего подозрительного не видно: поддержка посмотрит файл целиком." fi } # part1, part2, first and second stay global: verdict() reads them, and the # EXIT trap runs after main() has returned. main() { local out="$HOME/Desktop/proxysvpn-audit.txt" part1=$(mktemp -t pvpn-audit) || return 1 part2=$(mktemp -t pvpn-audit) || return 1 trap 'rm -f "$part1" "$part2"' EXIT exec 3>&2 say "" say "ProxysVPN: диагностика сети на Mac. Ничего не меняем, пароль не нужен, две-три минуты." first=$(vpn_state) if [ "$first" = on ]; then say "Сейчас VPN включён: собираю данные с включённым VPN..." else say "Сейчас VPN выключен: собираю данные без VPN..." fi { echo "#### ProxysVPN audit v2 $(date '+%Y-%m-%d %H:%M %z') ####"; echo "#### PART 1: VPN $first ####"; collect; } 2>&1 | mask > "$part1" say "" if [ "$first" = on ]; then say "*** Теперь ВЫКЛЮЧИТЕ ProxysVPN (кнопка в приложении), подождите 5 секунд и нажмите Return." else say "*** Теперь ВКЛЮЧИТЕ ProxysVPN, дождитесь надписи «Защищено» и нажмите Return." fi if wait_return; then second=$(vpn_state) if [ "$second" = "$first" ]; then say "Состояние VPN не изменилось (путь наружу тот же). Продолжаю: это тоже полезно знать." fi say "Собираю данные ещё раз..." { echo; echo "#### PART 2: VPN $second ####"; collect; } 2>&1 | mask > "$part2" else say "Терминал не ждёт ввода: вторую часть пропускаю." second=skipped fi cat "$part1" "$part2" > "$out" verdict say "" say "ГОТОВО. Файл proxysvpn-audit.txt лежит на Рабочем столе: отправьте его в чат поддержки." open -R "$out" 2>/dev/null } main "$@"