Версия 0.6.0

Заменяет tpws на utunws (форк nfqws под utun+BPF из Flowseal/zapret-mac-discord-youtube)
на macOS: движок уровня пакетов вместо socks5-прокси даёт macOS те же 22 стратегии
Flowseal, что у Windows и Linux, включая fake-пакеты и десинк UDP/QUIC — раньше tpws
этого не умел вовсе. Обход теперь ставится как LaunchDaemon (resources/darwin-helper/),
пароль администратора спрашивается при каждом включении/выключении, тот же принцип, что
pkexec на Linux. Системный прокси и авто-список hostlist-auto больше не нужны и убраны.

README: Windows отмечена как гарантированно рабочая, macOS и Linux — как частично.
This commit is contained in:
2026-09-04 14:36:52 +03:00
parent 6c0509e090
commit ebb9191d6f
28 changed files with 984 additions and 687 deletions
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
# Меняет RunAtLoad уже установленного LaunchDaemon без перезапуска демона и без разрыва
# pf-правил — root нужен только чтобы переписать защищённый plist в /Library/LaunchDaemons.
# Отдельный скрипт вместо повторного install.sh: переустановка перечитала бы и
# перекопировала utunws/fakes/strategy.cfg и пересобрала pf-правила заново — ради смены
# одного булева поля это лишний риск моргнуть сетью, которого нет у setServiceAutoStart()
# на Windows (`sc config start=`, тоже не трогает работающую службу).
set -eu
PLIST=/Library/LaunchDaemons/com.rknboost.zapret.plist
if [ "$#" -ne 1 ]; then
echo 'autostart.sh: ожидается 1 аргумент (0 или 1)' >&2
exit 1
fi
case "$1" in
0) RUNATLOAD=false ;;
1) RUNATLOAD=true ;;
*) echo 'autostart.sh: ожидается 0 или 1' >&2; exit 1 ;;
esac
[ -f "$PLIST" ] || { echo 'autostart.sh: демон не установлен' >&2; exit 1; }
/usr/bin/plutil -replace RunAtLoad -bool "$RUNATLOAD" "$PLIST"
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>@LABEL@</string>
<key>ProgramArguments</key>
<array>
<string>@DEST@/daemon.sh</string>
</array>
<key>RunAtLoad</key>
<@RUNATLOAD@/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>ThrottleInterval</key>
<integer>2</integer>
<key>StandardOutPath</key>
<string>@DEST@/zapret.log</string>
<key>StandardErrorPath</key>
<string>@DEST@/zapret.log</string>
</dict>
</plist>
+119
View File
@@ -0,0 +1,119 @@
#!/bin/sh
# Супервизор обхода — то, что реально запускает launchd (com.rknboost.zapret.plist.in,
# ProgramArguments). Живёт под root, пока LaunchDaemon загружен; KeepAlive/SuccessfulExit
# в plist перезапускает этот скрипт заново при любом его завершении — поэтому определение
# интерфейса/MAC шлюза выполняется здесь при каждом старте (в том числе после смены сети),
# а не один раз в install.sh. Портировано из run.sh форка Flowseal/zapret-mac-discord-youtube
# (MIT) — та же логика с utunws через utun+BPF, свои идентификаторы (см. install.sh).
set -eu
DEST='/Library/Application Support/RKNboost/zapret'
ANCHOR=com.apple/rknboost
UTUN_UNIT=61
UTUN_IFACE=utun60
UTUN_LOCAL=10.78.0.1
UTUN_REMOTE=10.78.0.2
PIDFILE=/var/run/rknboost-zapret.pid
TOKEN_FILE=/var/run/rknboost-zapret.pf-token
ROUTE6_FILE=/var/run/rknboost-zapret.route6
ENGINE_PID=
WATCHDOG_PID=
if [ -L "$DEST" ] || [ ! -f "$DEST/strategy.cfg" ] || [ ! -f "$DEST/ports.conf" ]; then
exit 1
fi
TCP_PORTS=$(/usr/bin/sed -n '1p' "$DEST/ports.conf")
UDP_PORTS=$(/usr/bin/sed -n '2p' "$DEST/ports.conf")
# ':' — диапазоны в синтаксисе pf (19294:19344), не дефис — см. install.sh.
case ",$TCP_PORTS," in *[!0-9,:]*) exit 1 ;; esac
case ",$UDP_PORTS," in *[!0-9,:]*) exit 1 ;; esac
clear_intercept() {
/sbin/pfctl -a "$ANCHOR" -F all >/dev/null 2>&1 || true
}
release_pf_token() {
if [ -s "$TOKEN_FILE" ]; then
TOKEN=$(/bin/cat "$TOKEN_FILE" 2>/dev/null || true)
if [ -n "$TOKEN" ]; then /sbin/pfctl -X "$TOKEN" >/dev/null 2>&1 || true; fi
/bin/rm -f "$TOKEN_FILE"
fi
}
cleanup() {
trap - EXIT INT TERM HUP
clear_intercept
if [ -n "$WATCHDOG_PID" ]; then kill "$WATCHDOG_PID" >/dev/null 2>&1 || true; fi
if [ -n "$ENGINE_PID" ]; then kill "$ENGINE_PID" >/dev/null 2>&1 || true; fi
if [ -n "$WATCHDOG_PID" ]; then wait "$WATCHDOG_PID" >/dev/null 2>&1 || true; fi
if [ -n "$ENGINE_PID" ]; then wait "$ENGINE_PID" >/dev/null 2>&1 || true; fi
release_pf_token
/bin/rm -f "$PIDFILE"
}
trap cleanup EXIT INT TERM HUP
clear_intercept
/usr/bin/pkill -9 -f "$DEST/bin/utunws" >/dev/null 2>&1 || true
PHYSICAL_IFACE=$(/sbin/route -n get default | /usr/bin/awk '/interface:/{print $2; exit}')
GATEWAY=$(/sbin/route -n get default | /usr/bin/awk '/gateway:/{print $2; exit}')
# Нет маршрута по умолчанию или он смотрит не в физический интерфейс (например, весь
# трафик уже уходит в VPN/другой utun) — переотправка через BPF работать не будет, честная
# ошибка вместо демона, который поднялся, но ничего не десинхронизирует.
if [ -z "$PHYSICAL_IFACE" ] || [ -z "$GATEWAY" ]; then exit 1; fi
/sbin/ping -c 1 -t 1 "$GATEWAY" >/dev/null 2>&1 || true
GATEWAY_MAC=$(/usr/sbin/arp -n "$GATEWAY" | /usr/bin/awk '/ at /{print $4; exit}')
case "$GATEWAY_MAC" in
*:*:*:*:*:*) ;;
*) exit 1 ;;
esac
GATEWAY6_MAC=
if /sbin/route -n get -inet6 default >"$ROUTE6_FILE" 2>/dev/null; then
GATEWAY6=$(/usr/bin/awk '/gateway:/{print $2; exit}' "$ROUTE6_FILE")
if [ -n "$GATEWAY6" ]; then
GATEWAY6_MAC=$(/usr/sbin/ndp -n "$GATEWAY6" 2>/dev/null | /usr/bin/awk '/ at |%/{for(i=1;i<=NF;i++) if($i ~ /^([0-9a-f]{1,2}:){5}[0-9a-f]{1,2}$/){print $i; exit}}')
fi
fi
/bin/rm -f "$ROUTE6_FILE"
export ZAPRET_IFACE="$PHYSICAL_IFACE"
export ZAPRET_GATEWAY_MAC="$GATEWAY_MAC"
export ZAPRET_GATEWAY6_MAC="$GATEWAY6_MAC"
export ZAPRET_UTUN_UNIT=$UTUN_UNIT
"$DEST/bin/utunws" --pidfile="$PIDFILE" "@$DEST/strategy.cfg" >>"$DEST/engine.log" 2>&1 &
ENGINE_PID=$!
I=0
while ! /sbin/ifconfig "$UTUN_IFACE" >/dev/null 2>&1; do
I=$((I + 1))
if ! kill -0 "$ENGINE_PID" 2>/dev/null || [ "$I" -gt 100 ]; then exit 1; fi
sleep 0.1
done
/sbin/ifconfig "$UTUN_IFACE" "$UTUN_LOCAL" "$UTUN_REMOTE" netmask 255.255.255.255 up
# utunws сам пишет pidfile до начала работы (fopen до цикла чтения utun) — chmod здесь
# только на случай нестандартного umask, чтобы sync()/наблюдатель читали его без root.
/bin/chmod 0644 "$PIDFILE" 2>/dev/null || true
"$DEST/watchdog.sh" $$ "$ENGINE_PID" &
WATCHDOG_PID=$!
if /sbin/pfctl -s info 2>/dev/null | /usr/bin/grep -q '^Status: Disabled'; then
TOKEN=$(/sbin/pfctl -E 2>&1 | /usr/bin/awk '/Token :/ { print $3 }')
if [ -n "$TOKEN" ]; then /bin/echo "$TOKEN" >"$TOKEN_FILE"; fi
fi
printf '%s\n' \
"pass out quick route-to ($UTUN_IFACE $UTUN_REMOTE) inet proto tcp from any to any port {$TCP_PORTS} user { >root } no state" \
"pass out quick route-to ($UTUN_IFACE $UTUN_REMOTE) inet proto udp from any to any port {$UDP_PORTS} user { >root } no state" \
| /sbin/pfctl -a "$ANCHOR" -f -
# Смена сети (Wi-Fi ↔ Ethernet, новый шлюз при DHCP-обновлении) делает пойманный при старте
# gateway MAC неверным — переотправка на старый MAC уходит в никуда. Выход отсюда с ошибкой
# перезапускает весь скрипт заново через KeepAlive плиста, включая переопределение шлюза.
while kill -0 "$ENGINE_PID" 2>/dev/null; do
sleep 2
CURRENT_IFACE=$(/sbin/route -n get default 2>/dev/null | /usr/bin/awk '/interface:/{print $2; exit}')
CURRENT_GATEWAY=$(/sbin/route -n get default 2>/dev/null | /usr/bin/awk '/gateway:/{print $2; exit}')
if [ "$CURRENT_IFACE" != "$PHYSICAL_IFACE" ] || [ "$CURRENT_GATEWAY" != "$GATEWAY" ]; then exit 1; fi
done
exit 1
+133
View File
@@ -0,0 +1,133 @@
#!/bin/sh
# Привилегированная установка обхода блокировок на macOS — единственное, что RKNboost
# запускает через osascript "with administrator privileges" (см. elevate.darwin.ts) для
# включения обхода. Не редактировать под bash-специфику: этот файл исполняется как
# /bin/sh — на macOS это тот же путь, что и на Linux, но лучше не полагаться на bashisms.
#
# Зовётся из src/main/zapret/utunws.darwin.ts ровно так:
# osascript … 'do shell script "'\''<этот файл>'\'' '\''<utunws>'\'' …" with administrator privileges'
#
# Аргументы (все уже подготовлены и провалидированы на стороне приложения — сюда попадают
# только готовые пути и провалидированные строки портов, тот же принцип, что у Linux-
# помощника resources/linux-helper/rknboost-helper.sh):
# 1: utunws_src — путь к бинарнику utunws в resources/zapret/darwin (ещё не установлен)
# 2: fakes_src — каталог с fake-пакетами (resources/zapret/fakes)
# 3: config_src — уже отрендеренный strategy.cfg (маркер --comment=rknboost:<id>:<hash>
# первой строкой, пути {LISTS}/{FAKES} уже резолвлены — см. strategies.ts)
# 4: tcp_ports — список портов TCP для pf, формат "80,443,8443" (уже проверен регуляркой
# в packetEngineConfig())
# 5: udp_ports — то же для UDP
# 6: run_at_load — 0 или 1: должен ли демон переживать перезагрузку системы без
# запущенного приложения (настройка «Автозапуск» в интерфейсе)
set -eu
HELPER_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
DEST='/Library/Application Support/RKNboost/zapret'
LABEL=com.rknboost.zapret
PLIST=/Library/LaunchDaemons/$LABEL.plist
ANCHOR=com.apple/rknboost
PIDFILE=/var/run/rknboost-zapret.pid
TOKEN_FILE=/var/run/rknboost-zapret.pf-token
KEEPINIT_FILE=/var/db/rknboost-zapret.keepinit
if [ "$#" -ne 6 ]; then
echo 'install.sh: ожидается 6 аргументов' >&2
exit 1
fi
UTUNWS_SRC=$1
FAKES_SRC=$2
CONFIG_SRC=$3
TCP_PORTS=$4
UDP_PORTS=$5
RUN_AT_LOAD=$6
[ -x "$UTUNWS_SRC" ] || { echo 'install.sh: не найден бинарник utunws' >&2; exit 1; }
[ -d "$FAKES_SRC" ] || { echo 'install.sh: не найден каталог fake-пакетов' >&2; exit 1; }
[ -f "$CONFIG_SRC" ] || { echo 'install.sh: не найден strategy.cfg' >&2; exit 1; }
# Формат уже проверен регуляркой в packetEngineConfig()/utunwsConfig() — здесь просто не
# доверяем слепо тому, что дошло до root, тот же приём, что в rknboost-helper.sh на Linux.
# ':' — не опечатка: utunwsConfig() переводит диапазоны в синтаксис pf (19294-19344 →
# 19294:19344, см. toPfPortRanges() в strategies.ts), дефисов в этих строках уже нет.
case ",$TCP_PORTS," in *[!0-9,:]*) echo 'install.sh: странный формат tcp-портов' >&2; exit 1 ;; esac
case ",$UDP_PORTS," in *[!0-9,:]*) echo 'install.sh: странный формат udp-портов' >&2; exit 1 ;; esac
case "$RUN_AT_LOAD" in
0) RUNATLOAD=false ;;
1) RUNATLOAD=true ;;
*) echo 'install.sh: run_at_load должен быть 0 или 1' >&2; exit 1 ;;
esac
# Сносим прошлую установку — самолечение на случай аварийно прерванной прошлой сессии,
# тот же приём, что cmd_stop() в rknboost-helper.sh на Linux делает первым шагом start.
/bin/launchctl bootout system/"$LABEL" >/dev/null 2>&1 || true
/sbin/pfctl -a "$ANCHOR" -F all >/dev/null 2>&1 || true
# Матчим процесс по полному пути НАШЕЙ установки, а не по имени: "utunws" называется так
# же и у независимо установленного Flowseal/zapret-mac-discord-youtube — pkill -x убил бы
# чужой процесс, если пользователь пользуется обоими приложениями одновременно.
/usr/bin/pkill -9 -f "$DEST/bin/utunws" >/dev/null 2>&1 || true
if [ -s "$TOKEN_FILE" ]; then
TOKEN=$(/bin/cat "$TOKEN_FILE" 2>/dev/null || true)
if [ -n "$TOKEN" ]; then /sbin/pfctl -X "$TOKEN" >/dev/null 2>&1 || true; fi
/bin/rm -f "$TOKEN_FILE"
fi
/bin/rm -f "$PIDFILE"
# Копия, а не запуск прямо из resources/zapret приложения: root не должен исполнять файл
# из каталога, куда обычный пользователь может писать (то же рассуждение, что у
# protectedWinwsPath() в paths.ts на Windows). daemon.sh/watchdog.sh лежат рядом с этим
# скриптом в bundledDarwinHelperDir() — тоже часть сборки, тоже копируем в защищённое место.
/bin/rm -rf "$DEST/bin" "$DEST/fakes"
/bin/mkdir -p "$DEST/bin" "$DEST/fakes"
/usr/bin/ditto "$UTUNWS_SRC" "$DEST/bin/utunws"
/usr/bin/ditto "$FAKES_SRC" "$DEST/fakes"
/usr/bin/ditto "$HELPER_DIR/daemon.sh" "$DEST/daemon.sh"
/usr/bin/ditto "$HELPER_DIR/watchdog.sh" "$DEST/watchdog.sh"
# Скачанный релиз-архив помечен com.apple.quarantine — карантин на исполняемый файл внутри
# защищённого каталога, который запускает launchd, только мешал бы.
/usr/bin/xattr -dr com.apple.quarantine "$DEST" >/dev/null 2>&1 || true
/usr/bin/install -m 0644 "$CONFIG_SRC" "$DEST/strategy.cfg"
printf '%s\n%s\n' "$TCP_PORTS" "$UDP_PORTS" > "$DEST/ports.conf"
/usr/sbin/chown -R root:wheel "$DEST"
/bin/chmod 755 "$DEST" "$DEST/bin" "$DEST/bin/utunws" "$DEST/daemon.sh" "$DEST/watchdog.sh" "$DEST/fakes"
/bin/chmod 644 "$DEST/strategy.cfg" "$DEST/ports.conf"
/usr/bin/sed \
-e "s|@LABEL@|$LABEL|g" \
-e "s|@DEST@|$DEST|g" \
-e "s|@RUNATLOAD@|$RUNATLOAD|g" \
"$HELPER_DIR/com.rknboost.zapret.plist.in" > "$PLIST"
/usr/sbin/chown root:wheel "$PLIST"
/bin/chmod 644 "$PLIST"
# net.inet.tcp.keepinit=7000 удлиняет время до TCP RST на этапе установления соединения —
# desync нужно время сработать. Сохраняем исходное значение один раз, откатываем в stop.sh.
if [ ! -s "$KEEPINIT_FILE" ]; then
/usr/sbin/sysctl -n net.inet.tcp.keepinit > "$KEEPINIT_FILE"
/bin/chmod 600 "$KEEPINIT_FILE"
fi
/usr/sbin/sysctl -w net.inet.tcp.keepinit=7000 >/dev/null
/bin/launchctl enable system/"$LABEL"
I=0
while ! /bin/launchctl bootstrap system "$PLIST"; do
I=$((I + 1))
if [ "$I" -gt 10 ]; then
/bin/sh "$HELPER_DIR/stop.sh"
exit 1
fi
sleep 0.5
done
/bin/launchctl kickstart -k system/"$LABEL"
I=0
while ! /sbin/ifconfig utun60 >/dev/null 2>&1; do
I=$((I + 1))
if [ "$I" -gt 100 ]; then
/bin/sh "$HELPER_DIR/stop.sh"
echo 'install.sh: utunws не поднялся' >&2
/usr/bin/tail -n 40 "$DEST/engine.log" >&2 2>/dev/null || true
exit 1
fi
sleep 0.1
done
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
# Привилегированная остановка обхода блокировок на macOS. Без аргументов — вызывается и как
# osascript-точка входа из utunws.darwin.ts (выключение обхода), и напрямую (уже под root,
# без повторной элевации) из install.sh, когда новая установка не поднялась.
#
# Полный снос, а не пауза: LaunchDaemon, strategy.cfg, pf-правила — всё убирается. Обратное
# решение (оставить демона установленным и просто «выключенным», как служба на Windows) не
# подходит здесь — на macOS нет отдельного «выключенного» состояния для LaunchDaemon кроме
# unload/bootout, так что «выключить» и «удалить установку» здесь одно и то же действие.
# Настройка автозапуска (RunAtLoad) значения не теряет — она хранится в приложении и
# применяется заново при следующем включении (см. engine.darwin.ts).
set -eu
LABEL=com.rknboost.zapret
ANCHOR=com.apple/rknboost
DEST='/Library/Application Support/RKNboost/zapret'
PLIST=/Library/LaunchDaemons/$LABEL.plist
PIDFILE=/var/run/rknboost-zapret.pid
TOKEN_FILE=/var/run/rknboost-zapret.pf-token
KEEPINIT_FILE=/var/db/rknboost-zapret.keepinit
/bin/launchctl disable system/"$LABEL" >/dev/null 2>&1 || true
/bin/launchctl bootout system/"$LABEL" >/dev/null 2>&1 || true
/sbin/pfctl -a "$ANCHOR" -F all >/dev/null 2>&1 || true
# См. install.sh про то, почему по полному пути, а не по имени процесса.
/usr/bin/pkill -9 -f "$DEST/bin/utunws" >/dev/null 2>&1 || true
/bin/rm -f "$PIDFILE" "$PLIST"
if [ -s "$TOKEN_FILE" ]; then
TOKEN=$(/bin/cat "$TOKEN_FILE" 2>/dev/null || true)
if [ -n "$TOKEN" ]; then /sbin/pfctl -X "$TOKEN" >/dev/null 2>&1 || true; fi
/bin/rm -f "$TOKEN_FILE"
fi
if [ -s "$KEEPINIT_FILE" ]; then
KEEPINIT=$(/bin/cat "$KEEPINIT_FILE" 2>/dev/null || true)
case "$KEEPINIT" in
*[!0-9]*|'') ;;
*) /usr/sbin/sysctl -w "net.inet.tcp.keepinit=$KEEPINIT" >/dev/null 2>&1 || true ;;
esac
/bin/rm -f "$KEEPINIT_FILE"
fi
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
# Подчищает pf, если daemon.sh (супервизор) умер, не успев выполнить свой cleanup() —
# например, получил SIGKILL. Без этого правило route-to пережило бы демона, и трафик на
# заблокированные порты уходил бы в мёртвый utun вместо прохождения как обычно (pf-правило
# не имеет bypass-семантики, в отличие от очереди NFQUEUE на Linux). Портировано из
# watchdog.sh форка Flowseal/zapret-mac-discord-youtube (MIT) без изменений логики.
SUPERVISOR_PID=$1
ENGINE_PID=$2
ANCHOR=com.apple/rknboost
TOKEN_FILE=/var/run/rknboost-zapret.pf-token
while kill -0 "$SUPERVISOR_PID" 2>/dev/null && kill -0 "$ENGINE_PID" 2>/dev/null; do
sleep 0.2
done
/sbin/pfctl -a "$ANCHOR" -F all >/dev/null 2>&1 || true
kill "$ENGINE_PID" >/dev/null 2>&1 || true
if [ -s "$TOKEN_FILE" ]; then
TOKEN=$(/bin/cat "$TOKEN_FILE" 2>/dev/null || true)
if [ -n "$TOKEN" ]; then /sbin/pfctl -X "$TOKEN" >/dev/null 2>&1 || true; fi
/bin/rm -f "$TOKEN_FILE"
fi