Files
PCA_Phobos/macos/phobos
Бобырев Андрей 01632034b0 macos: PCA Phobos for Mac
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:54:34 +03:00

152 lines
8.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# PCA Phobos for macOS (no Network Extension, no paid Apple account):
# wireguard-go (utun) -> 127.0.0.1:13255 wg-obfuscator -> server UDP, or via udptcp -> server TCP.
# + optional "home" tunnel (plain WireGuard to the home Keenetic) for 192.168.88.0/24 when away.
# Usage (root): phobos on | off | status | auto | udp | tcp | ru on|off | import <phobos://...>
set -uo pipefail
export PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
D=/usr/local/pca-phobos
RUN=/var/run/pca-phobos
PROFILE=$D/profile.conf
WGPORT=13255 TCPLOCAL=13256
mkdir -p "$RUN"
say() { echo "phobos: $*"; }
need_root() { [ "$(id -u)" = 0 ] || { echo "run with sudo"; exit 1; }; }
opt() { cat "$D/$1" 2>/dev/null || echo "$2"; } # opt transport auto | opt ru 1
sec() { # sec <section> <key> -> value (no "= none")
awk -v s="[$1]" -v k="$2" 'BEGIN{IGNORECASE=1}
/^\[/{in_s=(tolower($0)==tolower(s)); next}
in_s && !/^#/ { split($0,a,"="); key=a[1]; gsub(/[ \t]/,"",key);
if (tolower(key)==tolower(k)) { v=substr($0,index($0,"=")+1); gsub(/^[ \t]+|[ \t]+$/,"",v);
if (tolower(v)!="none") print v; exit } }' "$PROFILE"
}
tcp_port() { grep -E '^#.*pca-tcp' "$PROFILE" | sed 's/.*=[[:space:]]*//' | head -1; }
target() { sec instance target; }
host() { target | sed 's/:[0-9]*$//'; }
gateway() { route -n get default 2>/dev/null | awk '/gateway:/{print $2; exit}'; }
at_home() { [ "$(gateway)" = "$(opt home_gw 192.168.88.1)" ] && ipconfig getifaddr en0 2>/dev/null | grep -q '^192\.168\.88\.'; }
utun() { cat "$RUN/utun" 2>/dev/null; }
start_obfuscator() { # $1 = udp|tcp
pkill -f "$D/bin/wg-obfuscator" 2>/dev/null; sleep 0.3
local t; [ "$1" = tcp ] && t="127.0.0.1:$TCPLOCAL" || t="$(target)"
{ echo "[instance]"; echo "source-if = 127.0.0.1"; echo "source-lport = $WGPORT"; echo "target = $t"
awk 'BEGIN{IGNORECASE=1} /^\[/{i=(tolower($0)=="[instance]"); next}
i && !/^#/ && !/^(source-if|source-lport|target|verbose)[ \t]*=/ && !/=[ \t]*none[ \t]*$/' "$PROFILE"
echo "verbose = INFO"; } > "$RUN/obf.conf"
nohup "$D/bin/wg-obfuscator" -c "$RUN/obf.conf" >> "$RUN/obf.log" 2>&1 &
echo "$1" > "$RUN/transport"
}
dns_set() {
: > "$RUN/dns.saved"
networksetup -listallnetworkservices | tail -n +2 | grep -v '^\*' | while IFS= read -r s; do
printf '%s\t%s\n' "$s" "$(networksetup -getdnsservers "$s" | tr '\n' ' ')" >> "$RUN/dns.saved"
networksetup -setdnsservers "$s" 1.1.1.1 8.8.8.8 2>/dev/null
done
}
dns_restore() {
[ -f "$RUN/dns.saved" ] || return 0
while IFS=$'\t' read -r s v; do
case "$v" in *"aren't any"*|"") networksetup -setdnsservers "$s" empty ;; *) networksetup -setdnsservers "$s" $v ;; esac
done < "$RUN/dns.saved"
rm -f "$RUN/dns.saved"
}
home_up() {
[ -f "$D/home.key" ] || return 0
{ echo "[Interface]"; echo "PrivateKey = $(cat "$D/home.key")"; echo "Address = $(opt home_addr 172.16.6.32)/32"
echo; echo "[Peer]"; echo "PublicKey = $(opt home_pub '')"; echo "Endpoint = $(opt home_endpoint '')"
echo "AllowedIPs = 192.168.88.0/24, 172.16.6.0/24"; echo "PersistentKeepalive = 25"; } > "$RUN/pcahome.conf"
chmod 600 "$RUN/pcahome.conf"
wg-quick up "$RUN/pcahome.conf" >/dev/null 2>&1 && echo up > "$RUN/home" && say "home tunnel up"
}
home_down() { [ -f "$RUN/home" ] && wg-quick down "$RUN/pcahome.conf" >/dev/null 2>&1; rm -f "$RUN/home"; }
watch() { # background: auto UDP->TCP, gateway changes, home on/off when roaming
local started=$(date +%s) gw=$(gateway)
while [ -f "$RUN/utun" ]; do
sleep 5
local now_gw=$(gateway)
if [ -n "$now_gw" ] && [ "$now_gw" != "$gw" ]; then
route -n delete -host "$(host)" >/dev/null 2>&1; route -n add -host "$(host)" "$now_gw" >/dev/null 2>&1
gw=$now_gw; started=$(date +%s)
if at_home; then home_down; elif [ ! -f "$RUN/home" ]; then home_up; fi
fi
if [ "$(opt transport auto)" = auto ] && [ "$(cat "$RUN/transport")" = udp ] && [ -n "$(tcp_port)" ] \
&& [ $(( $(date +%s) - started )) -ge 15 ]; then
local hs=$(wg show "$(utun)" latest-handshakes 2>/dev/null | awk '{print $2; exit}')
if [ "${hs:-0}" = 0 ]; then say "no UDP answer in 15 s -> TCP"; start_obfuscator tcp; fi
fi
done
}
cmd_on() {
need_root
[ -f "$PROFILE" ] || { say "no profile: sudo phobos import 'phobos://...'"; exit 1; }
[ -f "$RUN/utun" ] && cmd_off >/dev/null
local gw=$(gateway) h=$(host) tp=$(tcp_port)
[ -n "$gw" ] || { say "no network"; exit 1; }
route -n add -host "$h" "$gw" >/dev/null 2>&1 # obfuscator/udptcp go around the tunnel
if [ -n "$tp" ]; then nohup "$D/bin/udptcp" -c "127.0.0.1:$TCPLOCAL" "$h:$tp" >> "$RUN/tcp.log" 2>&1 & fi
local mode=$(opt transport auto)
start_obfuscator "$([ "$mode" = tcp ] && echo tcp || echo udp)"
WG_TUN_NAME_FILE="$RUN/utun" wireguard-go utun >/dev/null 2>&1
local u=$(utun) addr=$(sec Interface Address | cut -d, -f1) mtu=$(sec Interface MTU)
[ -n "$u" ] || { say "wireguard-go failed"; cmd_off; exit 1; }
umask 077
{ echo "[Interface]"; echo "PrivateKey = $(sec Interface PrivateKey)"
echo; echo "[Peer]"; echo "PublicKey = $(sec Peer PublicKey)"
psk=$(sec Peer PresharedKey); [ -n "$psk" ] && echo "PresharedKey = $psk"
echo "Endpoint = 127.0.0.1:$WGPORT"; echo "AllowedIPs = 0.0.0.0/0"; echo "PersistentKeepalive = 25"; } > "$RUN/wg.conf"
wg setconf "$u" "$RUN/wg.conf"
ifconfig "$u" inet "${addr%/*}" "${addr%/*}" netmask 255.255.255.255 mtu "${mtu:-1420}" up
if [ "$(opt ru 1)" = 1 ]; then # Internet minus Russian networks
grep -v '^#' "$D/ru_bypass.txt" | tr ',' '\n' | sed 's/ //g' | grep / | while read -r n; do
route -q -n add -inet "$n" -interface "$u" >/dev/null 2>&1; done
else
route -q -n add -inet 0.0.0.0/1 -interface "$u"; route -q -n add -inet 128.0.0.0/1 -interface "$u"
fi
dns_set
at_home || home_up
nohup "$0" _watch >/dev/null 2>&1 &
echo $! > "$RUN/watch.pid"
say "on ($(cat "$RUN/transport"), RU direct: $(opt ru 1), home: $([ -f "$RUN/home" ] && echo up || echo off))"
}
cmd_off() {
need_root
[ -f "$RUN/watch.pid" ] && kill "$(cat "$RUN/watch.pid")" 2>/dev/null
local u=$(utun)
rm -f "$RUN/utun" "$RUN/watch.pid"
[ -n "$u" ] && rm -f "/var/run/wireguard/$u.sock" # wireguard-go exits, its routes go with utun
pkill -f "$D/bin/wg-obfuscator" 2>/dev/null; pkill -f "$D/bin/udptcp" 2>/dev/null
[ -f "$PROFILE" ] && route -n delete -host "$(host)" >/dev/null 2>&1
dns_restore; home_down
say "off"
}
cmd_status() {
local u=$(utun)
if [ -z "$u" ]; then echo "VPN: выключен"; else
local hs=$(wg show "$u" latest-handshakes 2>/dev/null | awk '{print $2; exit}')
echo "VPN: включён · $(cat "$RUN/transport" | tr a-z A-Z) · рукопожатие $([ "${hs:-0}" = 0 ] && echo нет || echo "$(( $(date +%s) - hs )) с назад")"
fi
echo "Режим: $(opt transport auto) · РФ напрямую: $([ "$(opt ru 1)" = 1 ] && echo да || echo нет) · Дом: $([ -f "$RUN/home" ] && echo подключён || ( at_home && echo 'вы дома' || echo выкл ))"
}
case "${1:-status}" in
on) cmd_on ;;
off) cmd_off ;;
status) cmd_status ;;
auto|udp|tcp) need_root; echo "$1" > "$D/transport"; [ -f "$RUN/utun" ] && cmd_on >/dev/null; cmd_status ;;
ru) need_root; [ "${2:-on}" = on ] && echo 1 > "$D/ru" || echo 0 > "$D/ru"; [ -f "$RUN/utun" ] && cmd_on >/dev/null; cmd_status ;;
import) need_root; s="${2#phobos://}"; s="${s%%#*}"; s=$(echo "$s" | tr '_-' '/+')
while [ $(( ${#s} % 4 )) -ne 0 ]; do s="$s="; done
echo "$s" | base64 -D > "$PROFILE.new" && grep -q '\[instance\]' "$PROFILE.new" && mv "$PROFILE.new" "$PROFILE" && chmod 600 "$PROFILE" && say "imported $(target)" ;;
_watch) watch ;;
*) echo "usage: phobos on|off|status|auto|udp|tcp|ru on|off|import <phobos://...>"; exit 2 ;;
esac