#!/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 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
-> 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 "; exit 2 ;; esac