diff --git a/.gitignore b/.gitignore index 7a60b85..f3b621e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ __pycache__/ *.pyc +macos/ +windows/ diff --git a/macos/PhobosMenu.swift b/macos/PhobosMenu.swift deleted file mode 100644 index 2f141c4..0000000 --- a/macos/PhobosMenu.swift +++ /dev/null @@ -1,97 +0,0 @@ -// PCA Phobos — menu bar app for macOS. Drives /usr/local/bin/phobos through -// `sudo -n` (sudoers NOPASSWD for that one root-owned script). No Network Extension. -import AppKit - -final class AppDelegate: NSObject, NSApplicationDelegate { - private let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - private var status = "" - - func applicationDidFinishLaunching(_ n: Notification) { - refresh() - Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in self?.refresh() } - } - - @discardableResult - private func phobos(_ args: [String]) -> String { - let p = Process() - p.executableURL = URL(fileURLWithPath: "/usr/bin/sudo") - p.arguments = ["-n", "/usr/local/bin/phobos"] + args - let pipe = Pipe() - p.standardOutput = pipe - p.standardError = pipe - try? p.run() - p.waitUntilExit() - return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - } - - private func refresh() { - DispatchQueue.global().async { - let s = self.phobos(["status"]) - DispatchQueue.main.async { self.status = s; self.rebuild() } - } - } - - private var isOn: Bool { status.contains("VPN: включён") } - - private func rebuild() { - item.button?.title = isOn ? (status.contains("TCP") ? "🛡 TCP" : "🛡") : "◌" - item.button?.toolTip = "PCA Phobos" - let m = NSMenu() - for line in status.split(separator: "\n") { - let i = NSMenuItem(title: String(line), action: nil, keyEquivalent: "") - i.isEnabled = false - m.addItem(i) - } - m.addItem(.separator()) - m.addItem(action(isOn ? "Отключить" : "Подключить", isOn ? ["off"] : ["on"])) - m.addItem(.separator()) - let mode = status.contains("Режим: tcp") ? "tcp" : status.contains("Режим: udp") ? "udp" : "auto" - for (t, v) in [("Канал: Авто", "auto"), ("Канал: UDP", "udp"), ("Канал: TCP", "tcp")] { - let i = action(t, [v]); i.state = mode == v ? .on : .off; m.addItem(i) - } - let ruOn = status.contains("РФ напрямую: да") - let ru = action("Российские сайты — напрямую", ["ru", ruOn ? "off" : "on"]); ru.state = ruOn ? .on : .off - m.addItem(ru) - m.addItem(.separator()) - m.addItem(withTitle: "Импорт ссылки phobos:// из буфера", action: #selector(importLink), keyEquivalent: "").target = self - m.addItem(withTitle: "Выход", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") - item.menu = m - } - - private func action(_ title: String, _ args: [String]) -> NSMenuItem { - let i = NSMenuItem(title: title, action: #selector(run(_:)), keyEquivalent: "") - i.target = self - i.representedObject = args - return i - } - - @objc private func run(_ sender: NSMenuItem) { - guard let args = sender.representedObject as? [String] else { return } - item.button?.title = "…" - DispatchQueue.global().async { - let out = self.phobos(args) - DispatchQueue.main.async { - if out.contains("password is required") { self.alert("Нет права sudo без пароля для /usr/local/bin/phobos") } - self.refresh() - } - } - } - - @objc private func importLink() { - guard let s = NSPasteboard.general.string(forType: .string), s.hasPrefix("phobos://") else { - alert("Скопируйте ссылку phobos:// из панели (клиент → 📱) и повторите"); return - } - alert(phobos(["import", s])) - refresh() - } - - private func alert(_ text: String) { - let a = NSAlert(); a.messageText = "PCA Phobos"; a.informativeText = text; a.runModal() - } -} - -let app = NSApplication.shared -let delegate = AppDelegate() -app.delegate = delegate -app.setActivationPolicy(.accessory) -app.run() diff --git a/macos/README.md b/macos/README.md deleted file mode 100644 index 5dce26a..0000000 --- a/macos/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# PCA Phobos для macOS - -Без Network Extension и без платного аккаунта Apple: `wireguard-go` (utun) + `wg-obfuscator` + `udptcp`, -запуск от root через `/usr/local/bin/phobos` (разрешён без пароля только он — sudoers.d/pca-phobos). - -- `phobos on|off|status|auto|udp|tcp|ru on|off|import ` -- Интернет — через сервер Phobos, **российские сайты напрямую** (ru_bypass.txt, как в Android/iOS). -- **Авто UDP→TCP**: нет рукопожатия 15 с — обфускатор переключается на `udptcp` (TCP 443 сервера). -- **Дом**: отдельный WireGuard до домашнего Keenetic (Wireguard1, пир `macbook-phobos-home`, 172.16.6.32), - только 192.168.88.0/24 и 172.16.6.0/24; дома (шлюз 192.168.88.1) не поднимается. Смена сети отслеживается. -- DNS на время работы — 1.1.1.1/8.8.8.8, при выключении возвращаются прежние. -- `PhobosMenu.swift` — значок в строке меню (сборка: `swiftc -O -o PCAPhobos PhobosMenu.swift`, ad-hoc подпись). - -Установка: собрать `bin/wg-obfuscator` (clang *.c) и `bin/udptcp` в `~/.pca-phobos`, положить туда profile.conf, -home.key, home_pub, home_endpoint, home_addr, затем `sudo bash ~/.pca-phobos/install-mac.sh`. diff --git a/macos/install-mac.sh b/macos/install-mac.sh deleted file mode 100755 index 8937819..0000000 --- a/macos/install-mac.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -# Install PCA Phobos for macOS from the staged folder (~/.pca-phobos). Run once: sudo bash ~/.pca-phobos/install-mac.sh -set -euo pipefail -[ "$(id -u)" = 0 ] || { echo "run with sudo"; exit 1; } -U="${SUDO_USER:?run via sudo from your account}" -S="$(eval echo "~$U")/.pca-phobos" -D=/usr/local/pca-phobos - -install -d -m 755 -o root -g wheel "$D" "$D/bin" -install -m 755 -o root -g wheel "$S/bin/wg-obfuscator" "$S/bin/udptcp" "$D/bin/" -install -m 644 -o root -g wheel "$S/ru_bypass.txt" "$D/" -install -m 600 -o root -g wheel "$S/profile.conf" "$S/home.key" "$D/" -for f in home_pub home_endpoint home_addr; do install -m 644 -o root -g wheel "$S/$f" "$D/"; done -install -m 755 -o root -g wheel "$S/phobos" /usr/local/bin/phobos - -# the menu bar app may run exactly this root-owned script without a password -echo "$U ALL=(root) NOPASSWD: /usr/local/bin/phobos" > /etc/sudoers.d/pca-phobos -chmod 440 /etc/sudoers.d/pca-phobos -visudo -cf /etc/sudoers.d/pca-phobos >/dev/null || { rm -f /etc/sudoers.d/pca-phobos; echo "sudoers check failed"; exit 1; } - -rm -rf "/Applications/PCA Phobos.app" -cp -R "$S/PCAPhobos.app" "/Applications/PCA Phobos.app" -LA="$(eval echo "~$U")/Library/LaunchAgents/com.bobyrev.pcaphobos.mac.plist" -cat > "$LA" < - - -Labelcom.bobyrev.pcaphobos.mac -ProgramArguments/Applications/PCA Phobos.app/Contents/MacOS/PCAPhobos -RunAtLoad - -EOF -chown "$U" "$LA" -sudo -u "$U" launchctl bootstrap "gui/$(id -u "$U")" "$LA" 2>/dev/null || sudo -u "$U" open "/Applications/PCA Phobos.app" -echo "PCA Phobos installed: menu bar icon, or 'sudo phobos on|off|status'" diff --git a/macos/phobos b/macos/phobos deleted file mode 100755 index b461566..0000000 --- a/macos/phobos +++ /dev/null @@ -1,151 +0,0 @@ -#!/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