Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5768968863 |
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
|||||||
custom:
|
|
||||||
- "https://boosty.to/andrey27/donate"
|
|
||||||
- "https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798"
|
|
||||||
3
.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
windows/build/
|
|
||||||
10
BETA.md
@@ -1,10 +0,0 @@
|
|||||||
# Каналы PCA Phobos
|
|
||||||
|
|
||||||
С версии **1.4.0** все три канала (stable / beta / dev) — одна и та же версия.
|
|
||||||
Новые фичи по-прежнему сначала выкатываются в `dev`, затем в `beta`, затем в `main`
|
|
||||||
(`promote.sh`), но база одна — без расхождений.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
phobos-update # stable (открытый)
|
|
||||||
GH_TOKEN=… phobos-update beta|dev # для подписчиков Boosty
|
|
||||||
```
|
|
||||||
137
CHANGELOG.md
@@ -1,137 +0,0 @@
|
|||||||
# Changelog — PCA Phobos
|
|
||||||
|
|
||||||
Каналы: **stable** (публичный, без токена) · **beta** / **dev** (приватный repo, токен подписчика).
|
|
||||||
Обновление: `phobos-update stable` · `GH_TOKEN=… phobos-update beta|dev` · из панели → «Версия и обновления».
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Как обновиться после фиксов
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# stable (открытый)
|
|
||||||
phobos-update stable
|
|
||||||
|
|
||||||
# beta/dev (токен уже сохранён в /opt/phobos-panel/.gh_token)
|
|
||||||
phobos-update beta # или: phobos-update dev
|
|
||||||
```
|
|
||||||
Затем **пересоздать клиентов** в панели (старые конфиги могли содержать неверный порт обфускатора).
|
|
||||||
Обновляются: панель, скрипты онбординга, агент, updater. Ключи WireGuard, `server.env`, клиенты — НЕ трогаются.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1.4.2 — настройка обфускатора сервера
|
|
||||||
|
|
||||||
- **feat:** в панели «Настройки» → «Обфускатор сервера»: masking (AUTO/STUN/NONE), max-dummy, idle — записываются во все порты сервера, обфускатор перезапускается (клиенты переподключаются за секунды).
|
|
||||||
- **fix (Android PCA Phobos VPN 1.4.3):** конфиги других сборок Phobos с лишними ключами (`obfuscate-bytes` и т. п.) больше не роняют обфускатор («обфускатор завершился, код 1»): передаются только ключи, известные wg-obfuscator v1.6.
|
|
||||||
|
|
||||||
## 1.4.1 — резервный TCP-канал, «РФ напрямую»
|
|
||||||
|
|
||||||
- **feat:** резервный канал TCP: если провайдер режет UDP, клиент сам переходит на TCP (порт 443, иначе 8443/2096/4433). Сервер: служба `phobos-tcp` (`udptcp`), ставится и включается автоматически при установке и обновлении. Проверено реальной блокировкой UDP: переключение за ~15 с, без блокировки остаётся UDP.
|
|
||||||
- **feat (Android 1.4.2):** «Российские сайты — напрямую, без VPN» (по умолчанию), выбор канала Авто/UDP/TCP, журнал TCP-канала.
|
|
||||||
- **fix:** установка и обновления с git.bobyrev.com (GitHub — запасной), каскад раздаёт панель.
|
|
||||||
- Ограничения: TCP-резерв пока только на основном сервере (не на вторых) и только в приложении PCA Phobos VPN (роутеры и PhobosWG — по-прежнему UDP с перебором портов).
|
|
||||||
|
|
||||||
## 1.4.0 — единая версия (stable = beta = dev)
|
|
||||||
|
|
||||||
Три канала сведены в одну версию: всё из dev (WARP, ttyd, бэкап, маршрут Keenetic, параметры обфускатора)
|
|
||||||
+ фиксы beta (обновлятор через API, порт/схема панели) + новое:
|
|
||||||
- **feat:** светлая/тёмная тема, компактные карточки клиентов, мобильная версия без горизонтальной прокрутки.
|
|
||||||
- **feat:** лимит скорости на клиента (tc на wg0, применяется и на вторых серверах).
|
|
||||||
- **feat:** замер скорости сервера (реальный) и клиента (живой + `/speedtest` на устройстве через туннель).
|
|
||||||
- **feat:** домен панели (nginx + Let's Encrypt), адрес для клиентов по домену, каскад (`phobos-cascade.sh`).
|
|
||||||
- **feat:** сторож — блоклист сканеров РКН (`phobos-guard.sh`).
|
|
||||||
- **fix:** secondary-setup на чистой Ubuntu 24.04 (нет iptables → wg0 не поднимался), без tty, `SERVER_IP` для NAT; клиенты досылаются на новый сервер сразу.
|
|
||||||
- **fix:** очистка orphan-peer (не было WG_CONFIG), дубль карточки WARP и кнопки WARP, имена клиентов кириллицей.
|
|
||||||
- **fix:** после обновления старым обновлятором 1.2.x/1.3.x панель сама докачивает новые скрипты.
|
|
||||||
- **fix:** APK без токена подписчика — из публичного релиза (как в 1.2.8).
|
|
||||||
|
|
||||||
Обновление: `phobos-update` (stable) — клиенты, ключи и `server.env` не трогаются. Проверено на школе: 1.2.8 → 1.4.0, старый клиент работает без переустановки.
|
|
||||||
|
|
||||||
## stable
|
|
||||||
|
|
||||||
### v1.2.7
|
|
||||||
- **fix:** импорт `phobos://` в PhobosWG. Конфиги дополняются `= none` для недостающих полей (`PresharedKey` и др.) перед base64 — иначе приложение писало «невозможно импортировать туннель».
|
|
||||||
|
|
||||||
### v1.2.6
|
|
||||||
- **fix:** бинарники wg-obfuscator для роутеров. Keenetic = aarch64, а Ground-Zerro раздаёт только x86_64. Теперь install.sh и сборка пакета клиента авто-качают aarch64/mipsel/armv7 из ClusterM/wg-obfuscator releases.
|
|
||||||
|
|
||||||
### v1.2.5
|
|
||||||
- **feat:** поле токена подписчика + кнопки каналов (stable/beta/dev) прямо в карточке «Версия и обновления». Подписчик вставляет токен в панель и переключается на beta/dev без консоли.
|
|
||||||
|
|
||||||
### v1.2.4
|
|
||||||
- **feat:** установка по умолчанию стабильная; beta/dev — опционально (`CHANNEL=`).
|
|
||||||
|
|
||||||
### v1.2.3
|
|
||||||
- **fix:** дебаунс DOWN-алертов (3 цикла + повтор) — убирает ложный флап серверов.
|
|
||||||
- **fix:** `phobos-update` отменяет обновление на несуществующую версию (текущая не трогается).
|
|
||||||
- **feat:** `--check` / `--versions`, кнопка «Проверить обновления».
|
|
||||||
|
|
||||||
### v1.2.2
|
|
||||||
- контакт во всех местах → группа `@PCAdministration`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Критичные фиксы (общие, во всех каналах)
|
|
||||||
|
|
||||||
| Версия | Проблема | Решение |
|
|
||||||
|--------|----------|---------|
|
|
||||||
| 1.2.8 / 1.3.x | Android PhobosWG: «Неправильная длина ключа в PublicKey» | Android/iOS payload восстанавливает потерянный `=` в WireGuard ключах и не добавляет `PresharedKey = none`; QR/link нужно открыть заново |
|
|
||||||
| 1.2.8 / 1.3.x | Установка/обновление могли пройти, но потом панель генерила кривую ссылку | `phobos-self-check --fix` встроен в install/update и доступен как отдельная команда |
|
|
||||||
| 1.2.7 / 1.3.x | PhobosWG: «невозможно импортировать туннель» | `phobos://` дополняется `= none` для всех обязательных полей (`PresharedKey`) по спеке формата |
|
|
||||||
| 1.2.8 / 1.3.x | Install `[6/9] PCA patches`: `curl: (23) Failure writing output to destination` | `fetch()` создаёт папку назначения перед `curl -o` |
|
|
||||||
| 1.2.8 / 1.3.x | VPS x86_64: `ERROR: obfuscator binary for x86_64 missing` | авто-загрузка `linux-x64` из ClusterM releases + нормализация `amd64 -> x86_64` |
|
|
||||||
| 1.2.6 / 1.3.x | Роутер aarch64: «бинарник не найден в архиве» | авто-загрузка aarch64/mipsel/armv7 из ClusterM releases |
|
|
||||||
| (внутр.) | Клиент не подключается, «НЕТ СОЕДИНЕНИЯ», handshake не встаёт | клиентский обфускатор целился в порт 51821 (никто не слушал); теперь берёт первый из `OBFUSCATOR_PORTS` (2083) |
|
|
||||||
| (внутр.) | агент secondary не обновлялся через `phobos-update` | `server/api.py` добавлен в managed_files |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## beta (приватный)
|
|
||||||
|
|
||||||
### v1.3.0-beta
|
|
||||||
- **fix:** install/update теперь запускают `phobos-self-check --fix`: автоматически чинят потерянный `=` в `SERVER_WG_PUBLIC_KEY`, сверяют ключ с `wg0`, проверяют router helper-файлы и shell syntax до выдачи клиентских ссылок.
|
|
||||||
- **fix:** Android/iOS phone configs теперь восстанавливают потерянный `=` в WireGuard `PrivateKey/PublicKey/PresharedKey`. Это закрывает ошибку PhobosWG «Неправильная длина ключа в PublicKey».
|
|
||||||
- **fix:** `lib-core.sh` больше не теряет финальный `=` при чтении `server.env`, поэтому новые клиентские `.conf` получают полный `PublicKey`.
|
|
||||||
- **fix:** клиентский router package теперь содержит обязательные `lib-client.sh`, `install-obfuscator.sh`, `install-wireguard.sh` и дополнительные helper-скрипты из PCA repo, поэтому `wget .../init/TOKEN.sh | sh` больше не падает с `can't open './lib-client.sh'`.
|
|
||||||
- **fix:** шаг `[6/9] PCA patches` больше не падает с `curl: (23) Failure writing output to destination`, если папки `repo/server/scripts` или `repo/client/templates` не были созданы upstream sparse clone.
|
|
||||||
- **fix:** установка `wg-obfuscator` на x86_64 больше не падает с `ERROR: obfuscator binary for x86_64 missing`. Installer и secondary installer берут `linux-x64` из ClusterM releases.
|
|
||||||
- **fix:** свежая beta-установка снова получает `lib-core.sh`, без которого client helper мог ломаться на чистой машине.
|
|
||||||
- **fix:** старые ссылки `/tags` теперь ведут в `/labels`, без 404.
|
|
||||||
- **fix:** поле имени клиента больше не даёт ошибку в консоли Chrome из-за HTML `pattern`. UI bench-test прошёл без console/page errors.
|
|
||||||
- **fix:** WG tunnel subnet перестала быть жёстко зашита в `10.25.0.0/16`. Installer, secondary deploy, client packaging и router pull теперь используют `SERVER_WG_IPV4_NETWORK`, так что `10.26`/`10.27` и другие отдельные WG-сети на разных VPS работают без ручных правок.
|
|
||||||
- **fix:** в клиентский пакет добавляется `panel_tunnel_ip`, а `phobos-pull` использует его как tunnel endpoint панели вместо жёсткого `10.25.0.1`.
|
|
||||||
- скачивание `.conf` для телефона (Android `phobos://` + QR, iOS обычный WireGuard + QR);
|
|
||||||
- кнопка скачивания приложения **PhobosWG (APK)** — авто-загрузка из GitHub Release приватного repo;
|
|
||||||
- все критичные фиксы выше.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## dev (приватный, активная разработка)
|
|
||||||
|
|
||||||
### v1.3.4-dev
|
|
||||||
- **fix:** install/update теперь запускают `phobos-self-check --fix`: автоматически чинят потерянный `=` в `SERVER_WG_PUBLIC_KEY`, сверяют ключ с `wg0`, проверяют router helper-файлы и shell syntax до выдачи клиентских ссылок.
|
|
||||||
- **fix:** Android/iOS phone configs теперь восстанавливают потерянный `=` в WireGuard `PrivateKey/PublicKey/PresharedKey`. Это закрывает ошибку PhobosWG «Неправильная длина ключа в PublicKey».
|
|
||||||
- **fix:** `lib-core.sh` и WARP helper больше не теряют финальный `=` при чтении `server.env`, поэтому новые клиентские `.conf` получают полный `PublicKey`.
|
|
||||||
- **fix:** клиентский router package теперь содержит обязательные `lib-client.sh`, `install-obfuscator.sh`, `install-wireguard.sh` и дополнительные helper-скрипты из PCA repo, поэтому `wget .../init/TOKEN.sh | sh` больше не падает с `can't open './lib-client.sh'`.
|
|
||||||
- **fix:** шаг `[6/9] PCA patches` больше не падает с `curl: (23) Failure writing output to destination`, если папки `repo/server/scripts` или `repo/client/templates` не были созданы upstream sparse clone.
|
|
||||||
- **fix:** установка `wg-obfuscator` на x86_64 больше не падает с `ERROR: obfuscator binary for x86_64 missing`. Installer и secondary installer берут `linux-x64` из ClusterM releases.
|
|
||||||
- **fix:** поле имени клиента больше не даёт ошибку в консоли Chrome из-за HTML `pattern`. UI bench-test прошёл без console/page errors.
|
|
||||||
- **fix:** WG tunnel subnet больше не зашита в `10.25.0.0/16`. `install.sh`, `secondary-setup.sh`, client packaging, router pull и secondary deploy теперь берут `SERVER_WG_IPV4_NETWORK`, поэтому один сервер можно держать на `10.25`, другой на `10.26`, `10.27` и дальше без ручного `nano`.
|
|
||||||
- **fix:** в клиентский пакет добавляется `panel_tunnel_ip`, а `phobos-pull`/`phobos-ttyd` используют его вместо жёсткого `10.25.0.1`. Это убирает поломку управления роутером после смены WG-подсети на сервере.
|
|
||||||
- **fix:** per-client WARP стал действительно точечным. Удалено старое правило на всю подсеть WG, правила теперь ставятся и снимаются только по `client tunnel IP /32`.
|
|
||||||
- **fix:** клиентские пакеты и install-команды снова собираются стабильно: создаётся `/opt/Phobos/bin`, шаблон `install-router.sh.template` обрабатывается мягко, пустой `OBFUSCATOR_KEY` больше не валит сборку.
|
|
||||||
- **fix:** `/clients` больше не падает при неполной выкладке dev-стенда. Добавлены обязательные `lib-core.sh` и client templates в install/update flow.
|
|
||||||
- **fix:** “живой peer есть, клиента нет” больше не выглядит как пропавший клиент. Сиротские peer теперь отображаются в `Сессии` и `Клиенты` как `orphan-*`, с кнопками `WARP`, `Kick`, `Purge`.
|
|
||||||
- **fix:** старые ссылки `/tags` теперь ведут в `/labels`.
|
|
||||||
- **security:** login hardening — `HttpOnly` + `SameSite=Lax` cookie, ограничение попыток входа, constant-time compare для пароля и API keys, upload limit `64 MB`.
|
|
||||||
- **ops:** `phobos-update` и `phobos-backup restore` теперь сами определяют активный panel service (`phobos-panel` или `phobos-dev-test`) и перезапускают правильный unit.
|
|
||||||
- **security/cleanup:** из secondary API убран `shell=True` там, где он был не нужен; чтение CPU/mem/network переведено на более безопасный путь.
|
|
||||||
|
|
||||||
### v1.3.1-dev
|
|
||||||
- **баланс по трафику:** агенты отдают пропускную способность канала (rx/tx NIC), `server_load = 0.45·cpu + 0.30·mem + 0.25·bw`; карточки серверов показывают ↕ Mbps;
|
|
||||||
- **WARP / WARP+:** в `/settings` добавлен отдельный блок с режимами `egress`, `relay`, `custom profile` и полем `WARP+ key`;
|
|
||||||
- **egress mode:** исходящий трафик клиентов можно завернуть через Cloudflare WARP на стороне сервера;
|
|
||||||
- **relay mode:** добавлен UDP relay по мотивам `warp-relay` для отдельных WARP/WireGuard профилей;
|
|
||||||
- **custom profile:** полный `warp.conf` можно вставить и сохранить прямо из веб-панели;
|
|
||||||
- **help UI:** у блока WARP появился `?` с человеческим объяснением режимов, ограничений и сценариев использования;
|
|
||||||
- все фиксы и фичи из beta.
|
|
||||||
240
README.md
@@ -1,235 +1,11 @@
|
|||||||
# PCA Phobos — VPN, который не отключается
|
# PCA Phobos — канал beta закрыт
|
||||||
|
|
||||||
**Веб-панель и turnkey-установщик для [Phobos](https://github.com/Ground-Zerro/Phobos)** — обфусцированного WireGuard VPN. Ставится на чистый VPS одной командой, управляет десятками роутеров и устройств с одной страницы, сам переключается между серверами при сбоях и восстанавливается после перезагрузок.
|
Каналы **beta** и **dev** выдаются по токену подписчика (read-only токен Gitea) и лежат в приватном репозитории.
|
||||||
|
Здесь их нет — публичная только стабильная версия (`main`, теги `v*`).
|
||||||
|
|
||||||
<p align="center">
|
Как подключить:
|
||||||
<img src="docs/img/sessions.png" alt="Панель — активные сессии" width="850">
|
1. Обновись до стабильной 1.4.5 или новее: `phobos-update stable`
|
||||||
</p>
|
2. Вставь токен: панель → Настройки → Версия и обновления → «токен подписчика» → Сохранить токен
|
||||||
|
3. `phobos-update beta` (или кнопка beta в панели)
|
||||||
|
|
||||||
---
|
Токен — по подписке: https://boosty.to/andrey27
|
||||||
|
|
||||||
## Что это простыми словами
|
|
||||||
|
|
||||||
Обычный VPN падает, когда падает сервер или провайдер банит IP. **PCA Phobos — нет.**
|
|
||||||
|
|
||||||
- 🌐 **Маскированный WireGuard** — трафик не отличить от обычного, проходит мимо DPI и блокировок по сигнатуре.
|
|
||||||
- 🔁 **Авто-переключение между серверами** — сервер лёг или порт прикрыли → роутер сам прыгает на резервный за ~10–15 секунд, без твоего участия.
|
|
||||||
- 📡 **Работает за любым NAT** — серый IP, мобильный интернет, домашний роутер за провайдером. Управление едет внутри самого защищённого туннеля, поэтому бан публичного IP ему не страшен.
|
|
||||||
- 🖥️ **Одна команда — новое устройство** — создал клиента в панели → вставил команду на роутер → готово.
|
|
||||||
- ♻️ **Само-восстановление** — роутер перезагрузился, провайдер сменил IP, сервис отвалился → система поднимет всё обратно сама.
|
|
||||||
- 📱 **Телефоны** — своё приложение **PCA Phobos VPN** для Android (обфускация + раздельный трафик по приложениям), PhobosWG, iPhone (обычный WireGuard) — через QR/ссылку.
|
|
||||||
- 🛠 **Админ-приложение** PCA Phobos для Android — все ваши панели в одном месте.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Возможности
|
|
||||||
|
|
||||||
- **Активные сессии** — VPN IP, Real IP, метка, handshake, трафик RX/TX, Kick.
|
|
||||||
- **Клиенты VPN** — добавление/удаление, статус online (по handshake на любом сервере), назначение сервера.
|
|
||||||
- **Мульти-сервер + failover** — приоритеты, балансировка по нагрузке (CPU/RAM + пропускная способность канала), резервные серверы; роутеры переключаются автоматически.
|
|
||||||
- **NAT-friendly управление** — роутеры сами тянут конфиг по туннелю (`10.25.0.1`), переживает бан публичного IP.
|
|
||||||
- **Авто-восстановление роутеров** — серверный сторож (watchdog) поднимает роутер после неудачной перезагрузки через KeenDNS.
|
|
||||||
- **Конфиги для телефона** — 📱 Android `phobos://` + QR, 🍎 iPhone обычный WireGuard + QR.
|
|
||||||
- **Метки** — имя объекта по IP (видно в сессиях и Telegram).
|
|
||||||
- **Срок подписки** — дата окончания, обратный отсчёт, автокик + предупреждения.
|
|
||||||
- **Telegram-уведомления** — подключение/отключение, сервер вверх/вниз, истечение подписки, авто-восстановление.
|
|
||||||
- **Двуязычный интерфейс** RU/EN + подсказки `?` у каждого действия. **Светлая и тёмная тема** (кнопка ◐), компактная вёрстка — удобно с телефона.
|
|
||||||
- **Лимит скорости на клиента** — Мбит/с в карточке клиента; работает на всех серверах, сам восстанавливается после перезапуска WireGuard.
|
|
||||||
- **Замер скорости** — сервера (реальная скачка/отдача, кнопка ⚡ на «Серверах»), клиента (живая скорость в карточке + страница `http://10.25.0.1:<порт>/speedtest` на устройстве через VPN).
|
|
||||||
- **Домен панели** — `https://ваш-домен/` с бесплатным сертификатом Let's Encrypt; старый адрес `http://IP:порт` продолжает работать.
|
|
||||||
- **Адрес для клиентов и каскад** — клиенты подключаются по домену или через входной сервер (например, в РФ) → ваш сервер; роутеры получают адрес сами.
|
|
||||||
- **Резервный TCP-канал** — если провайдер режет UDP, клиент сам переходит на TCP (порт 443), обфускация сохраняется.
|
|
||||||
- **Сторож** — блокировка зондов и сканеров РКН (nftables, список обновляется раз в сутки).
|
|
||||||
- **WARP / WARP+** — выпуск клиентов в интернет через Cloudflare WARP (всем или выборочно), relay-режим.
|
|
||||||
- **Бэкап и восстановление** сервера из панели; **терминал роутера в браузере** (ttyd/RCI) за NAT.
|
|
||||||
- **Версии и обновления** — обновление и откат прямо из панели.
|
|
||||||
- **Уведомления per-client** — 🔔 у каждого клиента: включить/выключить Telegram-оповещения о подключении/отключении.
|
|
||||||
- **Telegram-бот** — управление клиентами командами прямо из чата:
|
|
||||||
- `/add <имя>` — создать клиента (бот пришлёт команду установки)
|
|
||||||
- `/del <имя>` — удалить клиента
|
|
||||||
- `/list` — список клиентов
|
|
||||||
- **Кнопки** — постоянная клавиатура: ➕ Создать клиента · 📋 Список · 🗑 Удалить (инлайн-выбор) · 📊 Статус · ❓ Помощь.
|
|
||||||
(Работает только для чата из настроек `tg_chat_id`.) Бот **необязателен**: хочешь — впиши токен от @BotFather и свой chat id в «Настройки», не хочешь — оставь пустым.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Скриншоты
|
|
||||||
|
|
||||||
| Клиенты (светлая) | Клиенты (тёмная) |
|
|
||||||
|---|---|
|
|
||||||
|  |  |
|
|
||||||
| **Сессии** | **Серверы: замер скорости, второй сервер** |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
| Телефон: панель | Телефон: тёмная | Android: VPN-клиент | Android: раздельный трафик | Android: админ-приложение |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
|  |  |  |  |  |
|
|
||||||
|
|
||||||
Настройки (домен, каскад, сторож, WARP, бэкап): [светлая](docs/img/settings.png) · [тёмная](docs/img/settings-dark.png)
|
|
||||||
|
|
||||||
--------|---------|---------|
|
|
||||||
|  |  |  |
|
|
||||||
|
|
||||||
> Если картинки не отображаются — положи свои PNG в `docs/img/` (`sessions.png`, `clients.png`, `servers.png`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Источник с 23.10.2026:** `git.bobyrev.com/PCA/PCA_Phobos` (GitHub-аккаунт временно недоступен). Серверы на 1.2.x обновляются одной командой:
|
|
||||||
> ```bash
|
|
||||||
> bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/update.sh)
|
|
||||||
> ```
|
|
||||||
|
|
||||||
## Быстрый старт (с чистого VPS, одной командой)
|
|
||||||
|
|
||||||
Ставит **всё** со всеми зависимостями: wg-obfuscator, WireGuard, обфускатор-сервисы, веб-панель, nginx, скрипты онбординга роутеров, сторож авто-восстановления. Предустановленный Phobos **не требуется**.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/install.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
### С параметрами
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PANEL_PASS=МойПароль TG_TOKEN=123:abc TG_CHAT=123456789 \
|
|
||||||
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/install.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Переменная | По умолчанию | Описание |
|
|
||||||
|-------------------|-----------------|----------|
|
|
||||||
| `PANEL_PASS` | `OcAdmin2026!` | Пароль панели (логин `admin`) |
|
|
||||||
| `PANEL_PORT` | случайный | Порт веб-панели |
|
|
||||||
| `API_KEY` | случайный | Общий ключ (агенты + pull-токен роутеров) |
|
|
||||||
| `OBF_PORTS` | `2083,5443,993` | Порты обфускатора |
|
|
||||||
| `TG_TOKEN`/`TG_CHAT` | пусто | Telegram-уведомления |
|
|
||||||
| `ALLOW_PLAIN_WG` | пусто | `=1` открыть порт 51820 для iOS WireGuard (без обфускации) |
|
|
||||||
| `PANEL_TLS_CERT` / `PANEL_TLS_KEY` | пусто | Включить HTTPS для панели через свой сертификат |
|
|
||||||
|
|
||||||
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
|
|
||||||
|
|
||||||
Полный список портов и что можно закрывать: [docs/PORTS.md](docs/PORTS.md).
|
|
||||||
|
|
||||||
### Вторичный сервер (для failover / балансировки)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MAIN_PROTO=http MAIN_SERVER=<ip_основного> MAIN_PORT=<порт_панели> MAIN_API_KEY=<API key из основного> \
|
|
||||||
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/server/secondary-setup.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
Сервер сам зарегистрируется в панели, клиенты досылаются на него сразу. Если сервер за NAT — добавь `SERVER_IP=<внешний_ip>`.
|
|
||||||
|
|
||||||
### Каскад (вход в РФ → выход за рубежом)
|
|
||||||
|
|
||||||
На входном VPS (чистый Debian/Ubuntu, Phobos не нужен):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/server/phobos-cascade.sh) enable <IP_выходного_сервера> 2083,5443,993
|
|
||||||
```
|
|
||||||
|
|
||||||
Затем в панели выходного сервера → **Настройки → Адрес для клиентов и каскад** → впиши IP/домен входа. Роутеры переключатся сами (~15 с), телефонам — заново открыть ссылку/QR. Отключить: `phobos-cascade disable`.
|
|
||||||
|
|
||||||
### Сторонние «сторожа» по просьбам участников
|
|
||||||
|
|
||||||
- Защита от зондов РКН ([Loorrr293/Auto_IPtables](https://github.com/Loorrr293/Auto_IPtables)) — **встроена**: «Настройки → Защита сервера», включается кнопкой.
|
|
||||||
- «Прикрывашка» ([miha75vu-bit/Flashkeen](https://github.com/miha75vu-bit/Flashkeen)) — WARP на выход + Psiphon; интерактивная, ставится по SSH: `bash <(curl -fsSL https://raw.githubusercontent.com/miha75vu-bit/Flashkeen/main/wp)`. Перестраивает выход wg0 — не включай одновременно со встроенным WARP egress.
|
|
||||||
- ipidea-kimwolf-ban — репозиторий автора удалён (404), встроить нечего.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Приложения для Android
|
|
||||||
|
|
||||||
- **PCA Phobos VPN** — клиент: включает VPN одной кнопкой, импорт `phobos://` из панели, встроенный обфускатор, **раздельный трафик** (весь / только выбранные приложения / все кроме выбранных), **российские сайты напрямую** (по умолчанию), **канал Авто/UDP/TCP** (при блокировке UDP — сам на TCP), журнал, проверка скорости. [Скачать APK](https://git.bobyrev.com/PCA/PCA_Phobos/releases/download/v1.4.2/PCA-Phobos-VPN.apk) · исходники `android-client/`.
|
|
||||||
- **PCA Phobos** (админ) — список ваших панелей, открываются в приложении. [Скачать APK](https://git.bobyrev.com/PCA/PCA_Phobos/releases/download/v1.4.2/PCA-Phobos.apk) · исходники `android/`.
|
|
||||||
|
|
||||||
## Клиенты для телефона
|
|
||||||
|
|
||||||
В таблице **Клиенты** у каждого клиента есть кнопки:
|
|
||||||
|
|
||||||
- 📱 **Android** — `phobos://`-ссылка + QR для приложения **PhobosWG** (с обфускацией). Скан QR → импорт.
|
|
||||||
- 🍎 **iPhone / iOS** — обычный WireGuard-конфиг + QR для официального WireGuard (**без** обфускации). Требует открытого порта 51820 (`ALLOW_PLAIN_WG=1` при установке).
|
|
||||||
|
|
||||||
> Для каждого устройства создавай **отдельного клиента** (свой ключ и IP). Один конфиг на двух устройствах = конфликт ключей.
|
|
||||||
|
|
||||||
**Приложения:**
|
|
||||||
- **Android (PhobosWG):** кнопка «Скачать приложение PhobosWG (APK)» на странице Android-клиента. Можно положить свой `.apk` в `/opt/Phobos/www/app/PhobosWG.apk` — тогда панель раздаёт его.
|
|
||||||
- **iPhone (iOS):** официальный [WireGuard из App Store](https://apps.apple.com/app/wireguard/id1441195209) — кнопка есть на странице iOS-клиента.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Обновление и откат
|
|
||||||
|
|
||||||
Проект версионируется (см. файл `VERSION` и git-теги `vX.Y.Z`).
|
|
||||||
|
|
||||||
**Из панели:** Настройки → «Версия и обновления» → **Обновить** / **Откатить**.
|
|
||||||
|
|
||||||
**Из консоли:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
phobos-update # обновить до последней версии (stable)
|
|
||||||
phobos-update beta|dev # каналы для подписчиков (GH_TOKEN), сейчас = та же 1.4.0
|
|
||||||
phobos-update v1.1.0 # обновить до конкретной версии (git tag)
|
|
||||||
phobos-update --rollback # откатить на предыдущую версию
|
|
||||||
phobos-update --version # показать установленную и доступную версию
|
|
||||||
phobos-update --list # список бэкапов
|
|
||||||
```
|
|
||||||
|
|
||||||
Обновляется только слой панели и скриптов. **Ключи WireGuard, `server.env` и клиенты не трогаются.** Перед каждым обновлением делается бэкап → откат восстанавливает его.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Возможные проблемы и решения
|
|
||||||
|
|
||||||
| Симптом | Причина | Решение |
|
|
||||||
|---------|---------|---------|
|
|
||||||
| **Команда установки → 403 / не качается** | репозиторий приватный | Сделай репозиторий публичным, либо ставь с токеном: `GH_TOKEN=ghp_xxx bash <(curl -fsSL -H "Authorization: token ghp_xxx" .../install.sh)` |
|
|
||||||
| **Панель не открывается** | панель перезапускалась (обновление) / сервер недоступен | Подожди 5 сек, обнови страницу. Проверь: `systemctl status phobos-panel` |
|
|
||||||
| **Клиент Offline, хотя работает** | роутер на резервном сервере | Норма — статус считается по handshake на **любом** сервере; обнови страницу |
|
|
||||||
| **«SSH timeout» при тесте роутера** | роутер за NAT, входящий SSH закрыт | Это нормально — управление идёт по pull-каналу через туннель, SSH не нужен |
|
|
||||||
| **У роутера нет интернета на LAN-устройствах** | WG-интерфейс `private` / нет привязки устройства к политике | На роутере: `security-level public` + `ip hotspot host <mac> policy <PolicyName>`; health-скрипт чинит это сам в течение ~5 мин |
|
|
||||||
| **Роутер не поднялся после перезагрузки** | Entware/USB не запустил автозагрузку | Сторож (watchdog) поднимет за ~3 мин (нужны KeenDNS + web-логин роутера в `router_access`), либо перезагрузи роутер ещё раз |
|
|
||||||
| **iPhone не подключается по `phobos://`** | на iOS нет приложения-обфускатора | Используй 🍎-конфиг (обычный WireGuard) + открой 51820 (`ALLOW_PLAIN_WG=1`) |
|
|
||||||
| **Смена сервера применяется не сразу** | роутер тянет конфиг по cron | Применяется за ~12–15 сек, не мгновенно |
|
|
||||||
| **Забыл порт панели** | случайный порт | `cat /opt/phobos-panel/.port` |
|
|
||||||
| **Забыл API key** | | `grep server_api_key /opt/phobos-panel/settings.json` |
|
|
||||||
| **Домен: certbot failed** | A-запись не на этот сервер / закрыт 80 | `dig +short домен` = IP сервера; открой 80/tcp; повтори в «Настройках» |
|
|
||||||
| **Лимит скорости не действует** | wg0 перезапущен | панель сама восстановит за ≤1 мин; вручную: `phobos-limit.sh apply` |
|
|
||||||
| **Кнопки клиента «уезжают»** | старая версия | обнови до 1.4.0 — карточки клиентов компактные, телефоны тоже |
|
|
||||||
|
|
||||||
Логи: `journalctl -u phobos-panel -n 50` · сторож: `/opt/Phobos/server/watchdog.log` · роутер: `/opt/etc/Phobos/health.log`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Архитектура
|
|
||||||
|
|
||||||
```
|
|
||||||
Телефон/Роутер → wg-obfuscator (клиент) ──обфускация──► сервер :2083/5443/993
|
|
||||||
│ де-обфускация
|
|
||||||
WireGuard :51820
|
|
||||||
│ 10.25.0.x
|
|
||||||
┌──────────┴──────────┐
|
|
||||||
Веб-панель Failover на
|
|
||||||
(Flask+Gunicorn) резервные серверы
|
|
||||||
```
|
|
||||||
|
|
||||||
- Протокол: **WireGuard + wg-obfuscator** (обфускация от DPI).
|
|
||||||
- Управление роутерами — по **туннелю** (`10.25.0.1`), устойчиво к бану публичного IP.
|
|
||||||
- Состояние: JSON-файлы (без БД).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Поддержка проекта
|
|
||||||
|
|
||||||
Если PCA Phobos оказался полезен — буду благодарен за поддержку:
|
|
||||||
|
|
||||||
- 💖 **Boosty:** [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate)
|
|
||||||
- 💳 **Ozon Bank (СБП):** [ссылка](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
|
|
||||||
- ✉️ **Telegram:** [@PCAdministration](https://t.me/PCAdministration) — вопросы, баги, идеи
|
|
||||||
|
|
||||||
## На основе и благодарности
|
|
||||||
|
|
||||||
PCA Phobos — это веб-панель и turnkey-инсталлятор поверх отличных открытых проектов:
|
|
||||||
|
|
||||||
- [**Phobos**](https://github.com/Ground-Zerro/Phobos) — обфусцированный WireGuard, автор **Ground_Zerro**. Поддержать: [Boosty](https://boosty.to/ground_zerro) ❤️
|
|
||||||
- [**WireGuard Easy**](https://github.com/wg-easy/wg-easy) — веб-панель WireGuard (AGPL-3.0), автор **Emile Nijssen**. Поддержать: [GitHub Sponsors](https://github.com/sponsors/WeeJeWel) ❤️
|
|
||||||
- [**wg-obfuscator**](https://github.com/ClusterM/wg-obfuscator) — обфускация WireGuard-трафика (GPL-3.0), автор **ClusterM**. Поддержать: [Boosty](https://boosty.to/cluster) ❤️
|
|
||||||
|
|
||||||
Спасибо авторам за отличные инструменты.
|
|
||||||
|
|||||||
2
android-client/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
build/
|
|
||||||
libs/x/
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.pca.phobos.vpn"
|
|
||||||
android:versionCode="143"
|
|
||||||
android:versionName="1.4.3">
|
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="35" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:label="PCA Phobos VPN"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
|
||||||
android:theme="@style/AppTheme"
|
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:allowBackup="false">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
<!-- panel "phobos://" links open straight into the app -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="phobos" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<!-- from com.wireguard.android:tunnel (AAR manifest, merged by hand: no Gradle) -->
|
|
||||||
<service
|
|
||||||
android:name="com.wireguard.android.backend.GoBackend$VpnService"
|
|
||||||
android:exported="false"
|
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.net.VpnService" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# PCA Phobos VPN — Android-клиент
|
|
||||||
|
|
||||||
Своё приложение-клиент: импорт `phobos://` из панели, встроенный wg-obfuscator + WireGuard,
|
|
||||||
**раздельный трафик** по приложениям (весь / только выбранные / все кроме выбранных).
|
|
||||||
|
|
||||||
- WireGuard: `com.wireguard.android:tunnel` (Apache-2.0), `libs/`.
|
|
||||||
- Обфускатор: [ClusterM/wg-obfuscator](https://github.com/ClusterM/wg-obfuscator) v1.6 (GPL-3.0),
|
|
||||||
собран NDK под bionic: `jni/<abi>/libwgobfuscator.so`. Статический glibc-бинарник с роутеров
|
|
||||||
внутри приложения падает с SIGSYS (seccomp), поэтому только bionic-сборка.
|
|
||||||
- Само приложение всегда вне туннеля — иначе обфускатор не достучится до сервера.
|
|
||||||
|
|
||||||
Сборка без Gradle: `KS_PASS=… ./build.sh` → `build/PCA-Phobos-VPN.apk`
|
|
||||||
(ключ подписи — `~/.android/pca-phobos.jks`, пароль в ДОСТУПЫ.md; ключ не менять, иначе обновления не встанут).
|
|
||||||
|
|
||||||
Пересборка обфускатора:
|
|
||||||
```bash
|
|
||||||
NDK=~/Library/Android/sdk/ndk/29.0.14206865/toolchains/llvm/prebuilt/darwin-x86_64/bin
|
|
||||||
$NDK/aarch64-linux-android24-clang -O2 -std=gnu11 -o jni/arm64-v8a/libwgobfuscator.so wg-obfuscator/*.c
|
|
||||||
$NDK/armv7a-linux-androideabi24-clang -O2 -std=gnu11 -o jni/armeabi-v7a/libwgobfuscator.so wg-obfuscator/*.c
|
|
||||||
```
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build PCA Phobos VPN (client) without Gradle. Needs libs/: tunnel-*.aar (com.wireguard.android:tunnel),
|
|
||||||
# collection-*.jar, annotation-*.jar; jni/<abi>/libwgobfuscator.so (static ClusterM wg-obfuscator).
|
|
||||||
# KS_PASS=… ./build.sh -> build/PCA-Phobos-VPN.apk
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
SDK="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
|
|
||||||
BT="$SDK/build-tools/$(ls "$SDK/build-tools" | sort -V | tail -1)"
|
|
||||||
JAR="$SDK/platforms/$(ls "$SDK/platforms" | sort -V | tail -1)/android.jar"
|
|
||||||
KS="${KS:-$HOME/.android/pca-phobos.jks}"
|
|
||||||
KS_PASS="${KS_PASS:?set KS_PASS}"
|
|
||||||
AAR=$(ls libs/tunnel-*.aar | tail -1)
|
|
||||||
rm -rf build && mkdir -p build/res build/classes build/dex build/aar build/apk/lib
|
|
||||||
unzip -q "$AAR" -d build/aar
|
|
||||||
LIBS="build/aar/classes.jar:$(ls libs/*.jar | tr '\n' ':')"
|
|
||||||
"$BT/aapt2" compile --dir res -o build/res/
|
|
||||||
"$BT/aapt2" link -o build/base.apk -I "$JAR" --manifest AndroidManifest.xml -A assets \
|
|
||||||
--java build/gen build/res/*.flat --min-sdk-version 24 --target-sdk-version 35
|
|
||||||
javac -source 11 -target 11 -nowarn -classpath "$JAR:$LIBS" -d build/classes \
|
|
||||||
$(find src build/gen -name '*.java') 2>&1 | grep -v "bootstrap classpath\|^Note:\|^warning: \[options\]\|^1 warning" || true
|
|
||||||
[ -f build/classes/com/pca/phobos/vpn/MainActivity.class ] || { echo "javac failed"; exit 1; }
|
|
||||||
"$BT/d8" --release --lib "$JAR" --min-api 24 --output build/dex \
|
|
||||||
$(find build/classes -name '*.class') build/aar/classes.jar libs/*.jar
|
|
||||||
cp build/base.apk build/unsigned.apk
|
|
||||||
(cd build/dex && zip -q ../unsigned.apk classes*.dex)
|
|
||||||
for abi in arm64-v8a armeabi-v7a; do
|
|
||||||
mkdir -p "build/apk/lib/$abi"
|
|
||||||
cp build/aar/jni/$abi/*.so "build/apk/lib/$abi/"
|
|
||||||
cp "jni/$abi/libwgobfuscator.so" "jni/$abi/libudptcp.so" "build/apk/lib/$abi/"
|
|
||||||
done
|
|
||||||
(cd build/apk && zip -qr ../unsigned.apk lib)
|
|
||||||
"$BT/zipalign" -f 4 build/unsigned.apk build/aligned.apk
|
|
||||||
"$BT/apksigner" sign --ks "$KS" --ks-pass "pass:$KS_PASS" --out build/PCA-Phobos-VPN.apk build/aligned.apk
|
|
||||||
"$BT/apksigner" verify build/PCA-Phobos-VPN.apk && ls -la build/PCA-Phobos-VPN.apk
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp" android:height="108dp"
|
|
||||||
android:viewportWidth="108" android:viewportHeight="108">
|
|
||||||
<path android:fillColor="#1e1b4b" android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path android:fillColor="#4f46e5"
|
|
||||||
android:pathData="M54,18 L82,28 L82,52 C82,70 70,84 54,90 C38,84 26,70 26,52 L26,28 Z" />
|
|
||||||
<path android:fillColor="#c7d2fe"
|
|
||||||
android:pathData="M54,30 L72,36.5 L72,52 C72,64 64,73.5 54,78 C44,73.5 36,64 36,52 L36,36.5 Z" />
|
|
||||||
<path android:fillColor="#1e1b4b"
|
|
||||||
android:pathData="M47,46 h14 v5 h-4.5 v16 h-5 v-16 h-4.5 z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="AppTheme" parent="android:Theme.Material.NoActionBar">
|
|
||||||
<item name="android:colorPrimary">#a5b4fc</item>
|
|
||||||
<item name="android:colorPrimaryDark">#1e1b4b</item>
|
|
||||||
<item name="android:colorAccent">#818cf8</item>
|
|
||||||
<item name="android:windowBackground">#0f172a</item>
|
|
||||||
<item name="android:statusBarColor">#1e1b4b</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar">
|
|
||||||
<item name="android:colorPrimary">#4338ca</item>
|
|
||||||
<item name="android:colorPrimaryDark">#1e1b4b</item>
|
|
||||||
<item name="android:colorAccent">#4f46e5</item>
|
|
||||||
<item name="android:windowBackground">#f1f5f9</item>
|
|
||||||
<item name="android:statusBarColor">#1e1b4b</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,542 +0,0 @@
|
|||||||
package com.pca.phobos.vpn;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.net.VpnService;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Base64;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
import android.widget.RadioGroup;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.wireguard.android.backend.GoBackend;
|
|
||||||
import com.wireguard.android.backend.Statistics;
|
|
||||||
import com.wireguard.android.backend.Tunnel;
|
|
||||||
import com.wireguard.config.Config;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.StringReader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PCA Phobos VPN — client: imports a phobos:// link from the panel, runs the
|
|
||||||
* bundled wg-obfuscator + userspace WireGuard, split tunnelling by apps.
|
|
||||||
* The app itself is always outside the tunnel so the obfuscator reaches the server directly.
|
|
||||||
*/
|
|
||||||
public class MainActivity extends Activity {
|
|
||||||
static final int LPORT = 13255;
|
|
||||||
static final String MODE_ALL = "all", MODE_ONLY = "only", MODE_EXCEPT = "except";
|
|
||||||
|
|
||||||
static GoBackend backend;
|
|
||||||
static Process obfuscator, relay;
|
|
||||||
static final int TCP_LPORT = 13256;
|
|
||||||
static volatile String transport = ""; // "UDP" | "TCP" while up
|
|
||||||
static final Tunnel TUNNEL = new Tunnel() {
|
|
||||||
public String getName() { return "phobos"; }
|
|
||||||
public void onStateChange(Tunnel.State s) { }
|
|
||||||
};
|
|
||||||
|
|
||||||
private final ExecutorService bg = Executors.newSingleThreadExecutor();
|
|
||||||
private final Handler ui = new Handler(Looper.getMainLooper());
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
private boolean dark;
|
|
||||||
private TextView status, traffic;
|
|
||||||
private Button power;
|
|
||||||
private long lastRx, lastTx, lastT;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle b) {
|
|
||||||
super.onCreate(b);
|
|
||||||
prefs = getSharedPreferences("vpn", MODE_PRIVATE);
|
|
||||||
dark = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
|
|
||||||
if (backend == null) backend = new GoBackend(getApplicationContext());
|
|
||||||
handleIntent(getIntent());
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onNewIntent(Intent i) {
|
|
||||||
super.onNewIntent(i);
|
|
||||||
handleIntent(i);
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleIntent(Intent i) {
|
|
||||||
if (i != null && i.getData() != null && "phobos".equals(i.getData().getScheme())) {
|
|
||||||
importLink(i.getDataString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── colors / widgets ──────────────────────────────────────────────────
|
|
||||||
private int bg() { return dark ? 0xFF0F172A : 0xFFF1F5F9; }
|
|
||||||
private int card() { return dark ? 0xFF1E293B : 0xFFFFFFFF; }
|
|
||||||
private int text() { return dark ? 0xFFE2E8F0 : 0xFF0F172A; }
|
|
||||||
private int muted() { return dark ? 0xFF94A3B8 : 0xFF64748B; }
|
|
||||||
private int dp(int v) { return Math.round(v * getResources().getDisplayMetrics().density); }
|
|
||||||
|
|
||||||
private GradientDrawable round(int color, int r) {
|
|
||||||
GradientDrawable g = new GradientDrawable();
|
|
||||||
g.setColor(color);
|
|
||||||
g.setCornerRadius(dp(r));
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Button button(String label, int color, View.OnClickListener l) {
|
|
||||||
Button b = new Button(this);
|
|
||||||
b.setText(label);
|
|
||||||
b.setAllCaps(false);
|
|
||||||
b.setTextColor(Color.WHITE);
|
|
||||||
b.setBackground(round(color, 10));
|
|
||||||
b.setOnClickListener(l);
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextView tv(String s, int color, int size, boolean bold) {
|
|
||||||
TextView t = new TextView(this);
|
|
||||||
t.setText(s);
|
|
||||||
t.setTextColor(color);
|
|
||||||
t.setTextSize(size);
|
|
||||||
if (bold) t.setTypeface(Typeface.DEFAULT_BOLD);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinearLayout.LayoutParams mw(int top) {
|
|
||||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
lp.topMargin = dp(top);
|
|
||||||
return lp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── profile ───────────────────────────────────────────────────────────
|
|
||||||
/** phobos://<urlsafe-base64 of conf>#name (format produced by the PCA Phobos panel). */
|
|
||||||
private void importLink(String link) {
|
|
||||||
try {
|
|
||||||
String s = link.trim();
|
|
||||||
String name = "Phobos";
|
|
||||||
int hash = s.indexOf('#');
|
|
||||||
if (hash > 0) { name = Uri.decode(s.substring(hash + 1)); s = s.substring(0, hash); }
|
|
||||||
s = s.replaceFirst("^phobos://", "").trim();
|
|
||||||
String conf = new String(Base64.decode(s, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING), StandardCharsets.UTF_8);
|
|
||||||
if (!conf.contains("[Interface]") || !conf.contains("[instance]")) throw new IllegalArgumentException("not a Phobos config");
|
|
||||||
Config.parse(new BufferedReader(new StringReader(wgPart(conf, MODE_ALL, "")))); // validate
|
|
||||||
prefs.edit().putString("conf", conf).putString("name", name).apply();
|
|
||||||
toast("Профиль «" + name + "» импортирован");
|
|
||||||
} catch (Exception e) {
|
|
||||||
toast("Не удалось импортировать: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Split payload into sections: header -> lines (drop "= none" placeholders). */
|
|
||||||
private static List<String> section(String conf, String header) {
|
|
||||||
List<String> out = new ArrayList<>();
|
|
||||||
boolean in = false;
|
|
||||||
for (String raw : conf.split("\n")) {
|
|
||||||
String ln = raw.trim();
|
|
||||||
if (ln.startsWith("[") && ln.endsWith("]")) { in = ln.equalsIgnoreCase(header); continue; }
|
|
||||||
if (!in || ln.isEmpty() || !ln.contains("=")) continue;
|
|
||||||
if (ln.substring(ln.indexOf('=') + 1).trim().equalsIgnoreCase("none")) continue;
|
|
||||||
out.add(ln);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String key(String line) { return line.substring(0, line.indexOf('=')).trim().toLowerCase(); }
|
|
||||||
private static String val(String line) { return line.substring(line.indexOf('=') + 1).trim(); }
|
|
||||||
|
|
||||||
String wgPart(String conf, String mode, String apps) {
|
|
||||||
StringBuilder sb = new StringBuilder("[Interface]\n");
|
|
||||||
for (String ln : section(conf, "[Interface]")) {
|
|
||||||
String k = key(ln);
|
|
||||||
if (k.equals("excludedapplications") || k.equals("includedapplications")) continue;
|
|
||||||
sb.append(ln).append('\n');
|
|
||||||
}
|
|
||||||
List<String> sel = new ArrayList<>();
|
|
||||||
for (String a : apps.split(",")) if (!a.trim().isEmpty()) sel.add(a.trim());
|
|
||||||
if (MODE_ONLY.equals(mode) && !sel.isEmpty()) {
|
|
||||||
sel.remove(getPackageNameSafe());
|
|
||||||
sb.append("IncludedApplications = ").append(String.join(", ", sel)).append('\n');
|
|
||||||
} else {
|
|
||||||
Set<String> ex = new LinkedHashSet<>();
|
|
||||||
ex.add(getPackageNameSafe()); // obfuscator must bypass the tunnel
|
|
||||||
if (MODE_EXCEPT.equals(mode)) ex.addAll(sel);
|
|
||||||
sb.append("ExcludedApplications = ").append(String.join(", ", ex)).append('\n');
|
|
||||||
}
|
|
||||||
sb.append("\n[Peer]\n");
|
|
||||||
String ruBypass = prefs != null && prefs.getBoolean("ru_direct", true) ? ruBypassAllowedIps() : null;
|
|
||||||
for (String ln : section(conf, "[Peer]")) {
|
|
||||||
if (key(ln).equals("endpoint")) ln = "Endpoint = 127.0.0.1:" + LPORT;
|
|
||||||
if (key(ln).equals("allowedips") && ruBypass != null) ln = "AllowedIPs = " + ruBypass;
|
|
||||||
sb.append(ln).append('\n');
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String ruCache;
|
|
||||||
|
|
||||||
/** Internet minus Russian networks (assets/ru_bypass.txt, generated from ipdeny). */
|
|
||||||
private String ruBypassAllowedIps() {
|
|
||||||
if (ruCache != null) return ruCache;
|
|
||||||
try (BufferedReader r = new BufferedReader(new java.io.InputStreamReader(
|
|
||||||
getAssets().open("ru_bypass.txt"), StandardCharsets.UTF_8))) {
|
|
||||||
for (String ln; (ln = r.readLine()) != null; ) {
|
|
||||||
if (!ln.startsWith("#") && ln.contains("/")) { ruCache = ln.trim(); break; }
|
|
||||||
}
|
|
||||||
} catch (Exception e) { log("ru_bypass: " + e); }
|
|
||||||
return ruCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getPackageNameSafe() { return "com.pca.phobos.vpn"; }
|
|
||||||
|
|
||||||
private static final java.util.Set<String> OBF_KEYS = new java.util.HashSet<>(java.util.Arrays.asList(
|
|
||||||
"target", "key", "masking", "idle-timeout", "max-dummy", "static-bindings", "max-clients", "target-if", "allow-clean", "in-timeout", "resolve-interval"));
|
|
||||||
|
|
||||||
private String obfPart(String conf, boolean viaTcp) {
|
|
||||||
StringBuilder sb = new StringBuilder("[instance]\nsource-if = 127.0.0.1\nsource-lport = " + LPORT + "\n");
|
|
||||||
for (String ln : section(conf, "[instance]")) {
|
|
||||||
String k = key(ln);
|
|
||||||
if (k.equals("source-if") || k.equals("source-lport") || k.equals("verbose")) continue;
|
|
||||||
// configs from other Phobos builds carry keys our wg-obfuscator v1.6 rejects (obfuscate-bytes…)
|
|
||||||
if (!OBF_KEYS.contains(k)) continue;
|
|
||||||
if (viaTcp && k.equals("target")) ln = "target = 127.0.0.1:" + TCP_LPORT;
|
|
||||||
sb.append(ln).append('\n');
|
|
||||||
}
|
|
||||||
sb.append("verbose = INFO\n");
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** "# pca-tcp = 443" written by the panel when the server has the TCP fallback. */
|
|
||||||
private static String tcpPort(String conf) {
|
|
||||||
for (String ln : conf.split("\n")) {
|
|
||||||
String t = ln.trim();
|
|
||||||
if (t.startsWith("#") && t.contains("pca-tcp") && t.contains("=")) return val(t);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String target(String conf) {
|
|
||||||
for (String ln : section(conf, "[instance]")) if (key(ln).equals("target")) return val(ln);
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── connect / disconnect ──────────────────────────────────────────────
|
|
||||||
private boolean isUp() {
|
|
||||||
try { return backend.getState(TUNNEL) == Tunnel.State.UP; } catch (Exception e) { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggle() {
|
|
||||||
if (isUp()) { bg.execute(this::down); return; }
|
|
||||||
if (prefs.getString("conf", "").isEmpty()) { toast("Сначала импортируйте ссылку phobos:// из панели"); return; }
|
|
||||||
Intent prep = VpnService.prepare(this);
|
|
||||||
if (prep != null) startActivityForResult(prep, 7);
|
|
||||||
else bg.execute(this::up);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int req, int res, Intent data) {
|
|
||||||
if (req == 7 && res == RESULT_OK) bg.execute(this::up);
|
|
||||||
else if (req == 7) toast("Без разрешения на VPN подключиться нельзя");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Start (or restart) obfuscator; via TCP it dials the local udptcp relay. */
|
|
||||||
private void startTransport(String conf, boolean viaTcp) throws Exception {
|
|
||||||
stopObfuscator();
|
|
||||||
String lib = getApplicationInfo().nativeLibraryDir;
|
|
||||||
if (viaTcp) {
|
|
||||||
String host = target(conf).replaceAll(":\\d+$", "");
|
|
||||||
relay = new ProcessBuilder(lib + "/libudptcp.so", "-c", "127.0.0.1:" + TCP_LPORT, host + ":" + tcpPort(conf))
|
|
||||||
.redirectErrorStream(true).redirectOutput(new File(getFilesDir(), "tcp.log")).start();
|
|
||||||
}
|
|
||||||
File cfg = new File(getFilesDir(), "obf.conf");
|
|
||||||
try (FileOutputStream o = new FileOutputStream(cfg)) { o.write(obfPart(conf, viaTcp).getBytes(StandardCharsets.UTF_8)); }
|
|
||||||
obfuscator = new ProcessBuilder(lib + "/libwgobfuscator.so", "-c", cfg.getAbsolutePath())
|
|
||||||
.redirectErrorStream(true).redirectOutput(new File(getFilesDir(), "obf.log")).start();
|
|
||||||
Thread.sleep(600);
|
|
||||||
if (!obfuscator.isAlive()) throw new IllegalStateException("обфускатор завершился, код " + obfuscator.exitValue() + " — см. «Журнал»");
|
|
||||||
if (relay != null && !relay.isAlive()) throw new IllegalStateException("TCP-канал не запустился — см. «Журнал»");
|
|
||||||
transport = viaTcp ? "TCP" : "UDP";
|
|
||||||
log("transport " + transport + " -> " + target(conf) + (viaTcp ? " tcp:" + tcpPort(conf) : ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void up() {
|
|
||||||
try {
|
|
||||||
String conf = prefs.getString("conf", "");
|
|
||||||
String mode = prefs.getString("transport", "auto");
|
|
||||||
boolean tcpAvail = !tcpPort(conf).isEmpty();
|
|
||||||
if ("tcp".equals(mode) && !tcpAvail) throw new IllegalStateException("на сервере нет TCP-канала — обновите панель и заново импортируйте ссылку");
|
|
||||||
startTransport(conf, "tcp".equals(mode));
|
|
||||||
Config c = Config.parse(new BufferedReader(new StringReader(
|
|
||||||
wgPart(conf, prefs.getString("mode", MODE_ALL), prefs.getString("apps", "")))));
|
|
||||||
backend.setState(TUNNEL, Tunnel.State.UP, c);
|
|
||||||
log("up ok, mode " + prefs.getString("mode", MODE_ALL) + ", transport " + mode);
|
|
||||||
post("Подключено (" + transport + ")");
|
|
||||||
if ("auto".equals(mode) && tcpAvail) watchUdp(conf);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
stopObfuscator();
|
|
||||||
log("up failed: " + e);
|
|
||||||
post("Ошибка: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Auto: no bytes back over UDP within 15 s -> UDP is cut, move the obfuscator to TCP. */
|
|
||||||
private void watchUdp(String conf) {
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
Thread.sleep(15000);
|
|
||||||
if (!isUp() || !"UDP".equals(transport)) return;
|
|
||||||
if (backend.getStatistics(TUNNEL).totalRx() > 0) return;
|
|
||||||
log("UDP: no answer in 15 s -> switching to TCP");
|
|
||||||
startTransport(conf, true);
|
|
||||||
post("UDP не отвечает — переключился на TCP");
|
|
||||||
} catch (Throwable e) { log("watchUdp: " + e); }
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void down() {
|
|
||||||
try { backend.setState(TUNNEL, Tunnel.State.DOWN, null); } catch (Throwable ignored) { }
|
|
||||||
stopObfuscator();
|
|
||||||
transport = "";
|
|
||||||
post("Отключено");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void stopObfuscator() {
|
|
||||||
if (obfuscator != null) { obfuscator.destroy(); obfuscator = null; }
|
|
||||||
if (relay != null) { relay.destroy(); relay = null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void post(String msg) { ui.post(() -> { toast(msg); refresh(); }); }
|
|
||||||
|
|
||||||
// ── UI ────────────────────────────────────────────────────────────────
|
|
||||||
private void render() {
|
|
||||||
LinearLayout root = new LinearLayout(this);
|
|
||||||
root.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
root.setBackgroundColor(bg());
|
|
||||||
|
|
||||||
LinearLayout h = new LinearLayout(this);
|
|
||||||
h.setBackgroundColor(0xFF1E1B4B);
|
|
||||||
h.setPadding(dp(16), dp(14), dp(16), dp(14));
|
|
||||||
h.addView(tv("🛡 PCA Phobos VPN", 0xFFC7D2FE, 18, true));
|
|
||||||
root.addView(h);
|
|
||||||
|
|
||||||
ScrollView sv = new ScrollView(this);
|
|
||||||
LinearLayout box = new LinearLayout(this);
|
|
||||||
box.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
box.setPadding(dp(16), dp(16), dp(16), dp(24));
|
|
||||||
sv.addView(box);
|
|
||||||
|
|
||||||
String conf = prefs.getString("conf", "");
|
|
||||||
LinearLayout prof = new LinearLayout(this);
|
|
||||||
prof.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
prof.setBackground(round(card(), 14));
|
|
||||||
prof.setPadding(dp(16), dp(14), dp(16), dp(14));
|
|
||||||
if (conf.isEmpty()) {
|
|
||||||
prof.addView(tv("Профиль не импортирован", text(), 16, true));
|
|
||||||
prof.addView(tv("В панели PCA Phobos откройте клиента → 📱 Android → нажмите ссылку phobos:// "
|
|
||||||
+ "на этом устройстве, или скопируйте её и нажмите «Импорт».", muted(), 14, false));
|
|
||||||
} else {
|
|
||||||
prof.addView(tv(prefs.getString("name", "Phobos"), text(), 18, true));
|
|
||||||
prof.addView(tv("Сервер: " + target(conf), muted(), 14, false));
|
|
||||||
}
|
|
||||||
box.addView(prof, mw(0));
|
|
||||||
|
|
||||||
power = new Button(this);
|
|
||||||
power.setAllCaps(false);
|
|
||||||
power.setTextColor(Color.WHITE);
|
|
||||||
power.setTextSize(20);
|
|
||||||
power.setTypeface(Typeface.DEFAULT_BOLD);
|
|
||||||
power.setOnClickListener(v -> toggle());
|
|
||||||
LinearLayout.LayoutParams plp = new LinearLayout.LayoutParams(dp(180), dp(180));
|
|
||||||
plp.gravity = Gravity.CENTER_HORIZONTAL;
|
|
||||||
plp.topMargin = dp(24);
|
|
||||||
box.addView(power, plp);
|
|
||||||
|
|
||||||
status = tv("", text(), 16, true);
|
|
||||||
status.setGravity(Gravity.CENTER);
|
|
||||||
box.addView(status, mw(12));
|
|
||||||
traffic = tv("", muted(), 14, false);
|
|
||||||
traffic.setGravity(Gravity.CENTER);
|
|
||||||
box.addView(traffic, mw(2));
|
|
||||||
|
|
||||||
// split tunnelling
|
|
||||||
LinearLayout split = new LinearLayout(this);
|
|
||||||
split.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
split.setBackground(round(card(), 14));
|
|
||||||
split.setPadding(dp(16), dp(14), dp(16), dp(14));
|
|
||||||
split.addView(tv("Раздельный трафик", text(), 16, true));
|
|
||||||
RadioGroup rg = new RadioGroup(this);
|
|
||||||
String mode = prefs.getString("mode", MODE_ALL);
|
|
||||||
String[][] modes = {{MODE_ALL, "Весь трафик через VPN"}, {MODE_ONLY, "Только выбранные приложения"},
|
|
||||||
{MODE_EXCEPT, "Все, кроме выбранных приложений"}};
|
|
||||||
for (String[] m : modes) {
|
|
||||||
RadioButton r = new RadioButton(this);
|
|
||||||
r.setText(m[1]);
|
|
||||||
r.setTextColor(text());
|
|
||||||
r.setId(View.generateViewId());
|
|
||||||
r.setChecked(m[0].equals(mode));
|
|
||||||
r.setOnClickListener(v -> { prefs.edit().putString("mode", m[0]).apply(); applyIfUp(); refresh(); });
|
|
||||||
rg.addView(r);
|
|
||||||
}
|
|
||||||
split.addView(rg);
|
|
||||||
android.widget.CheckBox ru = new android.widget.CheckBox(this);
|
|
||||||
ru.setText("Российские сайты — напрямую, без VPN");
|
|
||||||
ru.setTextColor(text());
|
|
||||||
ru.setChecked(prefs.getBoolean("ru_direct", true));
|
|
||||||
ru.setOnCheckedChangeListener((b, on) -> { prefs.edit().putBoolean("ru_direct", on).apply(); applyIfUp(); });
|
|
||||||
split.addView(ru, mw(4));
|
|
||||||
split.addView(tv("Канал до сервера", text(), 14, true), mw(8));
|
|
||||||
RadioGroup tg = new RadioGroup(this);
|
|
||||||
tg.setOrientation(RadioGroup.HORIZONTAL);
|
|
||||||
String tmode = prefs.getString("transport", "auto");
|
|
||||||
for (String[] m : new String[][]{{"auto", "Авто"}, {"udp", "UDP"}, {"tcp", "TCP"}}) {
|
|
||||||
RadioButton r = new RadioButton(this);
|
|
||||||
r.setText(m[1]);
|
|
||||||
r.setTextColor(text());
|
|
||||||
r.setId(View.generateViewId());
|
|
||||||
r.setChecked(m[0].equals(tmode));
|
|
||||||
r.setOnClickListener(v -> { prefs.edit().putString("transport", m[0]).apply(); applyIfUp(); });
|
|
||||||
tg.addView(r);
|
|
||||||
}
|
|
||||||
split.addView(tg);
|
|
||||||
int n = prefs.getString("apps", "").isEmpty() ? 0 : prefs.getString("apps", "").split(",").length;
|
|
||||||
split.addView(button("Выбрать приложения (" + n + ")", 0xFF475569, v -> pickApps()), mw(8));
|
|
||||||
split.addView(tv("Изменения применяются сразу. Само приложение всегда идёт напрямую — так обфускатор держит связь с сервером.",
|
|
||||||
muted(), 12, false), mw(6));
|
|
||||||
box.addView(split, mw(20));
|
|
||||||
|
|
||||||
box.addView(button("Импорт ссылки phobos://", 0xFF4F46E5, v -> importDialog()), mw(16));
|
|
||||||
box.addView(button("Проверить скорость", 0xFF0EA5E9, v -> speedtest()), mw(8));
|
|
||||||
box.addView(button("Журнал", 0xFF475569, v -> showLog()), mw(8));
|
|
||||||
root.addView(sv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
|
|
||||||
setContentView(root);
|
|
||||||
refresh();
|
|
||||||
ui.removeCallbacksAndMessages(null);
|
|
||||||
ui.postDelayed(new Runnable() { public void run() { refresh(); ui.postDelayed(this, 2000); } }, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refresh() {
|
|
||||||
if (power == null) return;
|
|
||||||
boolean up = isUp();
|
|
||||||
power.setText(up ? "Отключить" : "Подключить");
|
|
||||||
power.setBackground(round(up ? 0xFF16A34A : 0xFF4F46E5, 90));
|
|
||||||
status.setText(up ? "🟢 VPN включён" + (transport.isEmpty() ? "" : " · " + transport) : "⚪ VPN выключен");
|
|
||||||
if (up) {
|
|
||||||
try {
|
|
||||||
Statistics st = backend.getStatistics(TUNNEL);
|
|
||||||
long rx = st.totalRx(), tx = st.totalTx(), now = System.currentTimeMillis();
|
|
||||||
String speed = "";
|
|
||||||
if (lastT > 0 && now > lastT) {
|
|
||||||
double s = (now - lastT) / 1000.0;
|
|
||||||
speed = String.format(" · ↓ %.1f / ↑ %.1f Мбит/с", (rx - lastRx) * 8 / s / 1e6, (tx - lastTx) * 8 / s / 1e6);
|
|
||||||
}
|
|
||||||
lastRx = rx; lastTx = tx; lastT = now;
|
|
||||||
traffic.setText(String.format("Получено %.1f МБ · Отправлено %.1f МБ", rx / 1048576.0, tx / 1048576.0) + speed);
|
|
||||||
} catch (Exception e) { traffic.setText(""); }
|
|
||||||
} else { traffic.setText(""); lastT = 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyIfUp() { if (isUp()) bg.execute(() -> { down(); up(); }); }
|
|
||||||
|
|
||||||
private void importDialog() {
|
|
||||||
final EditText e = new EditText(this);
|
|
||||||
e.setHint("phobos://…");
|
|
||||||
ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
|
||||||
if (cm != null && cm.getPrimaryClip() != null && cm.getPrimaryClip().getItemCount() > 0) {
|
|
||||||
CharSequence c = cm.getPrimaryClip().getItemAt(0).getText();
|
|
||||||
if (c != null && c.toString().trim().startsWith("phobos://")) e.setText(c);
|
|
||||||
}
|
|
||||||
new AlertDialog.Builder(this).setTitle("Ссылка из панели").setView(e)
|
|
||||||
.setPositiveButton("Импорт", (d, w) -> { importLink(e.getText().toString()); applyIfUp(); render(); })
|
|
||||||
.setNegativeButton("Отмена", null).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pickApps() {
|
|
||||||
PackageManager pm = getPackageManager();
|
|
||||||
Intent main = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
|
|
||||||
List<ResolveInfo> ris = pm.queryIntentActivities(main, 0);
|
|
||||||
List<String[]> apps = new ArrayList<>();
|
|
||||||
Set<String> seen = new LinkedHashSet<>();
|
|
||||||
for (ResolveInfo ri : ris) {
|
|
||||||
String p = ri.activityInfo.packageName;
|
|
||||||
if (p.equals(getPackageName()) || !seen.add(p)) continue;
|
|
||||||
apps.add(new String[]{String.valueOf(ri.loadLabel(pm)), p});
|
|
||||||
}
|
|
||||||
Collections.sort(apps, (a, b) -> a[0].compareToIgnoreCase(b[0]));
|
|
||||||
final Set<String> chosen = new LinkedHashSet<>(Arrays.asList(prefs.getString("apps", "").split(",")));
|
|
||||||
chosen.remove("");
|
|
||||||
String[] labels = new String[apps.size()];
|
|
||||||
boolean[] checked = new boolean[apps.size()];
|
|
||||||
for (int i = 0; i < apps.size(); i++) { labels[i] = apps.get(i)[0]; checked[i] = chosen.contains(apps.get(i)[1]); }
|
|
||||||
new AlertDialog.Builder(this).setTitle("Приложения")
|
|
||||||
.setMultiChoiceItems(labels, checked, (d, i, on) -> { if (on) chosen.add(apps.get(i)[1]); else chosen.remove(apps.get(i)[1]); })
|
|
||||||
.setPositiveButton("Готово", (d, w) -> { prefs.edit().putString("apps", String.join(",", chosen)).apply(); applyIfUp(); render(); })
|
|
||||||
.setNegativeButton("Отмена", null).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Real speed through the tunnel: the browser is inside the VPN (unless excluded by the split). */
|
|
||||||
private void speedtest() {
|
|
||||||
if (!isUp()) { toast("Сначала подключитесь"); return; }
|
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://speed.cloudflare.com/")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void log(String s) {
|
|
||||||
try (FileOutputStream o = new FileOutputStream(new File(getFilesDir(), "app.log"), true)) {
|
|
||||||
o.write((new java.util.Date() + " " + s + "\n").getBytes(StandardCharsets.UTF_8));
|
|
||||||
} catch (Exception ignored) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String tail(File f, int max) {
|
|
||||||
try {
|
|
||||||
java.io.ByteArrayOutputStream o = new java.io.ByteArrayOutputStream();
|
|
||||||
try (java.io.FileInputStream in = new java.io.FileInputStream(f)) {
|
|
||||||
byte[] buf = new byte[8192];
|
|
||||||
for (int n; (n = in.read(buf)) > 0; ) o.write(buf, 0, n);
|
|
||||||
}
|
|
||||||
String t = new String(o.toByteArray(), StandardCharsets.UTF_8);
|
|
||||||
return t.length() > max ? t.substring(t.length() - max) : t;
|
|
||||||
} catch (Exception e) { return ""; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showLog() {
|
|
||||||
String t = "— приложение —\n" + tail(new File(getFilesDir(), "app.log"), 3000)
|
|
||||||
+ "\n— обфускатор —\n" + tail(new File(getFilesDir(), "obf.log"), 3000)
|
|
||||||
+ "\n— TCP-канал —\n" + tail(new File(getFilesDir(), "tcp.log"), 1500)
|
|
||||||
+ "\nобфускатор запущен: " + (obfuscator != null && obfuscator.isAlive());
|
|
||||||
TextView v = tv(t, text(), 12, false);
|
|
||||||
v.setTextIsSelectable(true);
|
|
||||||
v.setPadding(dp(16), dp(8), dp(16), dp(8));
|
|
||||||
ScrollView sv = new ScrollView(this);
|
|
||||||
sv.addView(v);
|
|
||||||
new AlertDialog.Builder(this).setTitle("Журнал").setView(sv).setPositiveButton("OK", null).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toast(String s) { Toast.makeText(this, s, Toast.LENGTH_LONG).show(); }
|
|
||||||
}
|
|
||||||
1
android/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
build/
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.pca.phobos"
|
|
||||||
android:versionCode="140"
|
|
||||||
android:versionName="1.4.0">
|
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="35" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:label="PCA Phobos"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
|
||||||
android:theme="@style/AppTheme"
|
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
android:allowBackup="true">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
|
||||||
android:launchMode="singleTask">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# PCA Phobos — админ-приложение (Android)
|
|
||||||
|
|
||||||
Список ваших панелей Phobos, каждая открывается в приложении (WebView), тема по системе,
|
|
||||||
ссылки `phobos://` передаются в VPN-клиент. Сборка: `KS_PASS=… ./build.sh` → `build/PCA-Phobos.apk`.
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build PCA Phobos Android app without Gradle: aapt2 -> javac -> d8 -> zipalign -> apksigner.
|
|
||||||
# ANDROID_HOME=~/Library/Android/sdk KS=~/.android/pca-phobos.jks KS_PASS=… ./build.sh
|
|
||||||
# Output: build/PCA-Phobos.apk. The keystore is NOT in git (keep the same one, or updates won't install).
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
SDK="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
|
|
||||||
BT="$SDK/build-tools/$(ls "$SDK/build-tools" | sort -V | tail -1)"
|
|
||||||
JAR="$SDK/platforms/$(ls "$SDK/platforms" | sort -V | tail -1)/android.jar"
|
|
||||||
KS="${KS:-$HOME/.android/pca-phobos.jks}"
|
|
||||||
KS_PASS="${KS_PASS:?set KS_PASS (keystore password)}"
|
|
||||||
rm -rf build && mkdir -p build/res build/classes build/dex
|
|
||||||
|
|
||||||
"$BT/aapt2" compile --dir res -o build/res/
|
|
||||||
"$BT/aapt2" link -o build/base.apk -I "$JAR" --manifest AndroidManifest.xml \
|
|
||||||
--java build/gen build/res/*.flat --min-sdk-version 23 --target-sdk-version 35
|
|
||||||
javac -source 11 -target 11 -nowarn -classpath "$JAR" -d build/classes \
|
|
||||||
$(find src build/gen -name '*.java') 2>&1 | grep -v "bootstrap classpath" || true
|
|
||||||
"$BT/d8" --release --lib "$JAR" --min-api 23 --output build/dex $(find build/classes -name '*.class')
|
|
||||||
cp build/base.apk build/unsigned.apk
|
|
||||||
(cd build/dex && zip -q ../unsigned.apk classes.dex)
|
|
||||||
"$BT/zipalign" -f 4 build/unsigned.apk build/aligned.apk
|
|
||||||
[ -f "$KS" ] || keytool -genkeypair -keystore "$KS" -storepass "$KS_PASS" -keypass "$KS_PASS" \
|
|
||||||
-alias pca -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=PCA Phobos, O=PCAdministration" >/dev/null
|
|
||||||
"$BT/apksigner" sign --ks "$KS" --ks-pass "pass:$KS_PASS" --out build/PCA-Phobos.apk build/aligned.apk
|
|
||||||
"$BT/apksigner" verify build/PCA-Phobos.apk && ls -la build/PCA-Phobos.apk
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp" android:height="108dp"
|
|
||||||
android:viewportWidth="108" android:viewportHeight="108">
|
|
||||||
<path android:fillColor="#1e1b4b" android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path android:fillColor="#4f46e5"
|
|
||||||
android:pathData="M54,18 L82,28 L82,52 C82,70 70,84 54,90 C38,84 26,70 26,52 L26,28 Z" />
|
|
||||||
<path android:fillColor="#c7d2fe"
|
|
||||||
android:pathData="M54,30 L72,36.5 L72,52 C72,64 64,73.5 54,78 C44,73.5 36,64 36,52 L36,36.5 Z" />
|
|
||||||
<path android:fillColor="#1e1b4b"
|
|
||||||
android:pathData="M47,46 h14 v5 h-4.5 v16 h-5 v-16 h-4.5 z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="AppTheme" parent="android:Theme.Material.NoActionBar">
|
|
||||||
<item name="android:colorPrimary">#a5b4fc</item>
|
|
||||||
<item name="android:colorPrimaryDark">#1e1b4b</item>
|
|
||||||
<item name="android:colorAccent">#818cf8</item>
|
|
||||||
<item name="android:windowBackground">#0f172a</item>
|
|
||||||
<item name="android:statusBarColor">#1e1b4b</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar">
|
|
||||||
<item name="android:colorPrimary">#4338ca</item>
|
|
||||||
<item name="android:colorPrimaryDark">#1e1b4b</item>
|
|
||||||
<item name="android:colorAccent">#4f46e5</item>
|
|
||||||
<item name="android:windowBackground">#f1f5f9</item>
|
|
||||||
<item name="android:statusBarColor">#1e1b4b</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
package com.pca.phobos;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.InputType;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.webkit.CookieManager;
|
|
||||||
import android.webkit.WebChromeClient;
|
|
||||||
import android.webkit.WebResourceRequest;
|
|
||||||
import android.webkit.WebSettings;
|
|
||||||
import android.webkit.WebView;
|
|
||||||
import android.webkit.WebViewClient;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PCA Phobos — admin app: a list of your Phobos panels, each opens in a WebView.
|
|
||||||
* phobos:// links are handed to PhobosWG, downloads go to the system browser.
|
|
||||||
*/
|
|
||||||
public class MainActivity extends Activity {
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
private WebView web;
|
|
||||||
private ProgressBar progress;
|
|
||||||
private TextView title;
|
|
||||||
private boolean dark;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle b) {
|
|
||||||
super.onCreate(b);
|
|
||||||
prefs = getSharedPreferences("phobos", MODE_PRIVATE);
|
|
||||||
dark = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
|
|
||||||
== Configuration.UI_MODE_NIGHT_YES;
|
|
||||||
JSONArray list = panels();
|
|
||||||
String last = prefs.getString("last", "");
|
|
||||||
if (!last.isEmpty() && b == null) openPanel(last, nameFor(last));
|
|
||||||
else if (list.length() == 0) showEditor(null, -1);
|
|
||||||
else showList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── colors ────────────────────────────────────────────────────────────
|
|
||||||
private int bg() { return dark ? 0xFF0F172A : 0xFFF1F5F9; }
|
|
||||||
private int card() { return dark ? 0xFF1E293B : 0xFFFFFFFF; }
|
|
||||||
private int text() { return dark ? 0xFFE2E8F0 : 0xFF0F172A; }
|
|
||||||
private int muted() { return dark ? 0xFF94A3B8 : 0xFF64748B; }
|
|
||||||
private static final int ACCENT = 0xFF4F46E5;
|
|
||||||
private static final int HEADER = 0xFF1E1B4B;
|
|
||||||
|
|
||||||
private int dp(int v) { return Math.round(v * getResources().getDisplayMetrics().density); }
|
|
||||||
|
|
||||||
private GradientDrawable round(int color, int radius) {
|
|
||||||
GradientDrawable g = new GradientDrawable();
|
|
||||||
g.setColor(color);
|
|
||||||
g.setCornerRadius(dp(radius));
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Button button(String label, int color, View.OnClickListener l) {
|
|
||||||
Button btn = new Button(this);
|
|
||||||
btn.setText(label);
|
|
||||||
btn.setAllCaps(false);
|
|
||||||
btn.setTextColor(Color.WHITE);
|
|
||||||
btn.setBackground(round(color, 10));
|
|
||||||
btn.setOnClickListener(l);
|
|
||||||
return btn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinearLayout header(String text, View right) {
|
|
||||||
LinearLayout h = new LinearLayout(this);
|
|
||||||
h.setBackgroundColor(HEADER);
|
|
||||||
h.setGravity(Gravity.CENTER_VERTICAL);
|
|
||||||
h.setPadding(dp(16), dp(12), dp(8), dp(12));
|
|
||||||
TextView t = new TextView(this);
|
|
||||||
t.setText("🛡 " + text);
|
|
||||||
t.setTextColor(0xFFC7D2FE);
|
|
||||||
t.setTextSize(18);
|
|
||||||
t.setTypeface(Typeface.DEFAULT_BOLD);
|
|
||||||
t.setSingleLine(true);
|
|
||||||
h.addView(t, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
|
||||||
if (right != null) h.addView(right);
|
|
||||||
title = t;
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── storage ───────────────────────────────────────────────────────────
|
|
||||||
private JSONArray panels() {
|
|
||||||
try { return new JSONArray(prefs.getString("panels", "[]")); }
|
|
||||||
catch (Exception e) { return new JSONArray(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void savePanels(JSONArray a) { prefs.edit().putString("panels", a.toString()).apply(); }
|
|
||||||
|
|
||||||
private String nameFor(String url) {
|
|
||||||
JSONArray a = panels();
|
|
||||||
for (int i = 0; i < a.length(); i++) {
|
|
||||||
JSONObject o = a.optJSONObject(i);
|
|
||||||
if (o != null && url.equals(o.optString("url"))) return o.optString("name", url);
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String normalize(String raw) {
|
|
||||||
String u = raw.trim();
|
|
||||||
if (u.isEmpty()) return "";
|
|
||||||
if (!u.startsWith("http://") && !u.startsWith("https://")) u = "http://" + u;
|
|
||||||
while (u.endsWith("/")) u = u.substring(0, u.length() - 1);
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── screens ───────────────────────────────────────────────────────────
|
|
||||||
private void showList() {
|
|
||||||
web = null;
|
|
||||||
prefs.edit().remove("last").apply();
|
|
||||||
LinearLayout root = new LinearLayout(this);
|
|
||||||
root.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
root.setBackgroundColor(bg());
|
|
||||||
root.addView(header("PCA Phobos", null));
|
|
||||||
|
|
||||||
ScrollView sv = new ScrollView(this);
|
|
||||||
LinearLayout box = new LinearLayout(this);
|
|
||||||
box.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
box.setPadding(dp(16), dp(16), dp(16), dp(16));
|
|
||||||
sv.addView(box);
|
|
||||||
|
|
||||||
TextView hint = new TextView(this);
|
|
||||||
hint.setText("Ваши серверы Phobos. Нажмите — откроется панель. Долгое нажатие — изменить.");
|
|
||||||
hint.setTextColor(muted());
|
|
||||||
hint.setPadding(0, 0, 0, dp(12));
|
|
||||||
box.addView(hint);
|
|
||||||
|
|
||||||
final JSONArray a = panels();
|
|
||||||
for (int i = 0; i < a.length(); i++) {
|
|
||||||
final int idx = i;
|
|
||||||
final JSONObject o = a.optJSONObject(i);
|
|
||||||
if (o == null) continue;
|
|
||||||
LinearLayout c = new LinearLayout(this);
|
|
||||||
c.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
c.setBackground(round(card(), 12));
|
|
||||||
c.setPadding(dp(16), dp(14), dp(16), dp(14));
|
|
||||||
TextView n = new TextView(this);
|
|
||||||
n.setText(o.optString("name"));
|
|
||||||
n.setTextColor(text());
|
|
||||||
n.setTextSize(17);
|
|
||||||
n.setTypeface(Typeface.DEFAULT_BOLD);
|
|
||||||
TextView u = new TextView(this);
|
|
||||||
u.setText(o.optString("url"));
|
|
||||||
u.setTextColor(muted());
|
|
||||||
c.addView(n);
|
|
||||||
c.addView(u);
|
|
||||||
c.setOnClickListener(v -> openPanel(o.optString("url"), o.optString("name")));
|
|
||||||
c.setOnLongClickListener(v -> { showEditor(o, idx); return true; });
|
|
||||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
lp.bottomMargin = dp(10);
|
|
||||||
box.addView(c, lp);
|
|
||||||
}
|
|
||||||
box.addView(button("+ Добавить сервер", ACCENT, v -> showEditor(null, -1)));
|
|
||||||
root.addView(sv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
|
|
||||||
setContentView(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showEditor(final JSONObject existing, final int idx) {
|
|
||||||
web = null;
|
|
||||||
LinearLayout root = new LinearLayout(this);
|
|
||||||
root.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
root.setBackgroundColor(bg());
|
|
||||||
root.addView(header(existing == null ? "Новый сервер" : "Изменить сервер", null));
|
|
||||||
LinearLayout box = new LinearLayout(this);
|
|
||||||
box.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
box.setPadding(dp(16), dp(16), dp(16), dp(16));
|
|
||||||
|
|
||||||
final EditText name = field("Название (например: Амстердам)", existing == null ? "" : existing.optString("name"));
|
|
||||||
final EditText url = field("Адрес панели: http://IP:порт или https://домен", existing == null ? "" : existing.optString("url"));
|
|
||||||
url.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
|
||||||
box.addView(label("Название"));
|
|
||||||
box.addView(name);
|
|
||||||
box.addView(label("Адрес панели"));
|
|
||||||
box.addView(url);
|
|
||||||
TextView tip = new TextView(this);
|
|
||||||
tip.setText("Адрес и порт панели печатает установщик в конце установки (или: cat /opt/phobos-panel/.port). "
|
|
||||||
+ "Если задан домен в «Настройках» панели — используйте https://домен.");
|
|
||||||
tip.setTextColor(muted());
|
|
||||||
tip.setPadding(0, dp(4), 0, dp(16));
|
|
||||||
box.addView(tip);
|
|
||||||
|
|
||||||
box.addView(button("Сохранить и открыть", ACCENT, v -> {
|
|
||||||
String u = normalize(url.getText().toString());
|
|
||||||
if (u.isEmpty()) { toast("Введите адрес панели"); return; }
|
|
||||||
String n = name.getText().toString().trim();
|
|
||||||
if (n.isEmpty()) n = Uri.parse(u).getHost();
|
|
||||||
try {
|
|
||||||
JSONArray a = panels();
|
|
||||||
JSONObject o = new JSONObject().put("name", n).put("url", u);
|
|
||||||
if (idx >= 0) a.put(idx, o); else a.put(o);
|
|
||||||
savePanels(a);
|
|
||||||
} catch (Exception ignored) { }
|
|
||||||
openPanel(u, n);
|
|
||||||
}));
|
|
||||||
if (existing != null) {
|
|
||||||
Button del = button("Удалить из списка", 0xFFDC2626, v -> {
|
|
||||||
JSONArray a = panels(), out = new JSONArray();
|
|
||||||
for (int i = 0; i < a.length(); i++) if (i != idx) out.put(a.opt(i));
|
|
||||||
savePanels(out);
|
|
||||||
showList();
|
|
||||||
});
|
|
||||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
lp.topMargin = dp(10);
|
|
||||||
box.addView(del, lp);
|
|
||||||
}
|
|
||||||
if (panels().length() > 0) {
|
|
||||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
lp.topMargin = dp(10);
|
|
||||||
box.addView(button("Назад", 0xFF475569, v -> showList()), lp);
|
|
||||||
}
|
|
||||||
root.addView(box);
|
|
||||||
setContentView(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextView label(String s) {
|
|
||||||
TextView t = new TextView(this);
|
|
||||||
t.setText(s);
|
|
||||||
t.setTextColor(muted());
|
|
||||||
t.setPadding(0, dp(8), 0, dp(4));
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EditText field(String hint, String value) {
|
|
||||||
EditText e = new EditText(this);
|
|
||||||
e.setHint(hint);
|
|
||||||
e.setText(value);
|
|
||||||
e.setSingleLine(true);
|
|
||||||
e.setTextColor(text());
|
|
||||||
e.setHintTextColor(muted());
|
|
||||||
e.setBackground(round(card(), 10));
|
|
||||||
e.setPadding(dp(12), dp(12), dp(12), dp(12));
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openPanel(final String url, String name) {
|
|
||||||
prefs.edit().putString("last", url).apply();
|
|
||||||
LinearLayout root = new LinearLayout(this);
|
|
||||||
root.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
root.setBackgroundColor(bg());
|
|
||||||
|
|
||||||
LinearLayout actions = new LinearLayout(this);
|
|
||||||
TextView reload = headerAction("⟳", v -> { if (web != null) web.reload(); });
|
|
||||||
TextView servers = headerAction("☰", v -> showList());
|
|
||||||
actions.addView(reload);
|
|
||||||
actions.addView(servers);
|
|
||||||
root.addView(header(name, actions));
|
|
||||||
|
|
||||||
progress = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
|
|
||||||
progress.setMax(100);
|
|
||||||
root.addView(progress, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(3)));
|
|
||||||
|
|
||||||
web = new WebView(this);
|
|
||||||
WebSettings s = web.getSettings();
|
|
||||||
s.setJavaScriptEnabled(true);
|
|
||||||
s.setDomStorageEnabled(true);
|
|
||||||
s.setLoadWithOverviewMode(true);
|
|
||||||
s.setUseWideViewPort(true);
|
|
||||||
s.setBuiltInZoomControls(true);
|
|
||||||
s.setDisplayZoomControls(false);
|
|
||||||
s.setUserAgentString(s.getUserAgentString() + " PCAPhobosApp/1.4.0");
|
|
||||||
CookieManager.getInstance().setAcceptCookie(true);
|
|
||||||
web.setWebViewClient(new WebViewClient() {
|
|
||||||
@Override
|
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest req) {
|
|
||||||
Uri u = req.getUrl();
|
|
||||||
String scheme = u.getScheme() == null ? "" : u.getScheme();
|
|
||||||
if (scheme.equals("http") || scheme.equals("https")) {
|
|
||||||
Uri base = Uri.parse(url);
|
|
||||||
// other sites (GitHub, Boosty, Telegram…) -> system browser / apps
|
|
||||||
if (u.getHost() != null && !u.getHost().equals(base.getHost())) { external(u); return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
external(u); // phobos:// -> PhobosWG, tg:// -> Telegram
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageFinished(WebView view, String u) {
|
|
||||||
progress.setVisibility(View.GONE);
|
|
||||||
CookieManager.getInstance().flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceivedError(WebView view, int code, String desc, String failing) {
|
|
||||||
if (failing != null && failing.startsWith(url)) {
|
|
||||||
view.loadDataWithBaseURL(null, errorPage(url, desc), "text/html", "utf-8", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
web.setWebChromeClient(new WebChromeClient() {
|
|
||||||
@Override
|
|
||||||
public void onProgressChanged(WebView view, int p) {
|
|
||||||
progress.setVisibility(p < 100 ? View.VISIBLE : View.GONE);
|
|
||||||
progress.setProgress(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
web.setDownloadListener((dlUrl, ua, cd, mime, len) -> external(Uri.parse(dlUrl)));
|
|
||||||
root.addView(web, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
|
|
||||||
setContentView(root);
|
|
||||||
web.loadUrl(url + "/");
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextView headerAction(String s, View.OnClickListener l) {
|
|
||||||
TextView t = new TextView(this);
|
|
||||||
t.setText(s);
|
|
||||||
t.setTextColor(0xFFC7D2FE);
|
|
||||||
t.setTextSize(22);
|
|
||||||
t.setPadding(dp(14), 0, dp(14), 0);
|
|
||||||
t.setOnClickListener(l);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String errorPage(String url, String desc) {
|
|
||||||
String bgc = dark ? "#0f172a" : "#f1f5f9", fg = dark ? "#e2e8f0" : "#0f172a";
|
|
||||||
return "<html><body style='font-family:sans-serif;background:" + bgc + ";color:" + fg
|
|
||||||
+ ";padding:24px;text-align:center'><h2>Панель недоступна</h2><p>" + url + "</p><p style='color:#64748b'>"
|
|
||||||
+ desc + "</p><p>Проверьте адрес и порт, интернет и что сервер включён. Кнопка ⟳ — повторить.</p></body></html>";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void external(Uri u) {
|
|
||||||
try {
|
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, u));
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
if ("phobos".equals(u.getScheme())) {
|
|
||||||
new AlertDialog.Builder(this)
|
|
||||||
.setTitle("Нужно приложение PhobosWG")
|
|
||||||
.setMessage("Ссылки phobos:// открывает приложение PhobosWG. Скачайте его на странице Android-клиента в панели (кнопка «Скачать приложение PhobosWG»).")
|
|
||||||
.setPositiveButton("OK", null).show();
|
|
||||||
} else {
|
|
||||||
toast("Нет приложения для " + u.getScheme());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toast(String s) { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (web != null && web.canGoBack()) { web.goBack(); return; }
|
|
||||||
if (web != null && panels().length() > 0) { showList(); return; }
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Phobos Failover Configuration
|
|
||||||
# Format: SERVER_N=host:port1,port2,port3
|
|
||||||
# KEY_N=obfuscation_key_for_server_N
|
|
||||||
|
|
||||||
# Primary server (main, with web panel)
|
|
||||||
SERVER_1=144.124.252.104:2083,5443,993
|
|
||||||
KEY_1=Fc3X8GbVQD2FuHPbOjKiaP83nWSwNojJ
|
|
||||||
|
|
||||||
# Secondary server (backup, VPN only)
|
|
||||||
# SERVER_2=backup.example.com:51821,51822
|
|
||||||
# KEY_2=backup_obfuscation_key_here
|
|
||||||
|
|
||||||
# Emergency server (last resort)
|
|
||||||
# SERVER_3=emergency.example.com:43210
|
|
||||||
# KEY_3=emergency_key_here
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# ============================================================
|
|
||||||
# Phobos Health Monitor — Auto-heal + Failover for routers
|
|
||||||
# Runs via cron every 60 seconds on Keenetic/Entware
|
|
||||||
#
|
|
||||||
# Install: crontab -e → */1 * * * * /opt/etc/Phobos/phobos-health.sh
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
PHOBOS_DIR="/opt/etc/Phobos"
|
|
||||||
CONF="$PHOBOS_DIR/failover.conf"
|
|
||||||
STATE="$PHOBOS_DIR/state"
|
|
||||||
LOG="$PHOBOS_DIR/health.log"
|
|
||||||
WG_CONF="$PHOBOS_DIR/wg0.conf"
|
|
||||||
OBF_CONF="$PHOBOS_DIR/wg-obfuscator.conf"
|
|
||||||
LOCKFILE="/tmp/phobos-health.lock"
|
|
||||||
|
|
||||||
MAX_LOG_LINES=200
|
|
||||||
HANDSHAKE_WARN=180
|
|
||||||
HANDSHAKE_RESTART_OBF=300
|
|
||||||
HANDSHAKE_RESTART_WG=600
|
|
||||||
HANDSHAKE_FAILOVER=900
|
|
||||||
|
|
||||||
log() {
|
|
||||||
echo "$(date '+%H:%M:%S') $1" >> "$LOG"
|
|
||||||
# Trim log
|
|
||||||
if [ "$(wc -l < "$LOG" 2>/dev/null)" -gt "$MAX_LOG_LINES" ]; then
|
|
||||||
tail -n 100 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prevent concurrent runs
|
|
||||||
if [ -f "$LOCKFILE" ]; then
|
|
||||||
pid=$(cat "$LOCKFILE" 2>/dev/null)
|
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo $$ > "$LOCKFILE"
|
|
||||||
trap 'rm -f "$LOCKFILE"' EXIT
|
|
||||||
|
|
||||||
# Create state dir
|
|
||||||
mkdir -p "$STATE"
|
|
||||||
|
|
||||||
# ── Read failover config ──
|
|
||||||
if [ ! -f "$CONF" ]; then
|
|
||||||
log "ERROR: no failover.conf"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse servers: SERVER_1=ip:port1,port2,port3
|
|
||||||
# Parse current state
|
|
||||||
CURRENT_SERVER=$(cat "$STATE/current_server" 2>/dev/null || echo "1")
|
|
||||||
CURRENT_PORT_IDX=$(cat "$STATE/current_port_idx" 2>/dev/null || echo "0")
|
|
||||||
RESTART_COUNT=$(cat "$STATE/restart_count" 2>/dev/null || echo "0")
|
|
||||||
PRIMARY_CHECK_TS=$(cat "$STATE/primary_check_ts" 2>/dev/null || echo "0")
|
|
||||||
|
|
||||||
# Load server list
|
|
||||||
SERVER_COUNT=0
|
|
||||||
idx=1
|
|
||||||
while true; do
|
|
||||||
val=$(grep "^SERVER_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)
|
|
||||||
if [ -z "$val" ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
eval "SERVER_${idx}_HOST=$(echo "$val" | cut -d: -f1)"
|
|
||||||
eval "SERVER_${idx}_PORTS=$(echo "$val" | cut -d: -f2-)"
|
|
||||||
eval "SERVER_${idx}_KEY=$(grep "^KEY_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)"
|
|
||||||
SERVER_COUNT=$idx
|
|
||||||
idx=$((idx + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$SERVER_COUNT" -eq 0 ]; then
|
|
||||||
log "ERROR: no servers in failover.conf"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Get current handshake age ──
|
|
||||||
get_handshake_age() {
|
|
||||||
hs=$(wg show wg0 latest-handshakes 2>/dev/null | awk '{print $2}')
|
|
||||||
if [ -z "$hs" ] || [ "$hs" = "0" ]; then
|
|
||||||
echo "9999"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
now=$(date +%s)
|
|
||||||
age=$((now - hs))
|
|
||||||
echo "$age"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Get port by index from comma-separated list ──
|
|
||||||
get_port() {
|
|
||||||
server_idx=$1
|
|
||||||
port_idx=$2
|
|
||||||
eval "ports=\$SERVER_${server_idx}_PORTS"
|
|
||||||
echo "$ports" | tr ',' '\n' | sed -n "$((port_idx + 1))p"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_port_count() {
|
|
||||||
server_idx=$1
|
|
||||||
eval "ports=\$SERVER_${server_idx}_PORTS"
|
|
||||||
echo "$ports" | tr ',' '\n' | wc -l
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Switch to specific server:port ──
|
|
||||||
switch_endpoint() {
|
|
||||||
server_idx=$1
|
|
||||||
port_idx=$2
|
|
||||||
|
|
||||||
eval "host=\$SERVER_${server_idx}_HOST"
|
|
||||||
eval "obf_key=\$SERVER_${server_idx}_KEY"
|
|
||||||
port=$(get_port "$server_idx" "$port_idx")
|
|
||||||
|
|
||||||
if [ -z "$host" ] || [ -z "$port" ]; then
|
|
||||||
log "ERROR: invalid server $server_idx port_idx $port_idx"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "SWITCH → server $server_idx ($host:$port)"
|
|
||||||
|
|
||||||
# Update obfuscator config target
|
|
||||||
if [ -f "$OBF_CONF" ]; then
|
|
||||||
sed -i "s|^target = .*|target = ${host}:${port}|" "$OBF_CONF"
|
|
||||||
if [ -n "$obf_key" ]; then
|
|
||||||
sed -i "s|^key = .*|key = ${obf_key}|" "$OBF_CONF"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restart obfuscator
|
|
||||||
killall wg-obfuscator 2>/dev/null
|
|
||||||
sleep 1
|
|
||||||
wg-obfuscator --config "$OBF_CONF" &
|
|
||||||
|
|
||||||
# Save state
|
|
||||||
echo "$server_idx" > "$STATE/current_server"
|
|
||||||
echo "$port_idx" > "$STATE/current_port_idx"
|
|
||||||
echo "0" > "$STATE/restart_count"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Try next port on current server ──
|
|
||||||
try_next_port() {
|
|
||||||
port_count=$(get_port_count "$CURRENT_SERVER")
|
|
||||||
next_idx=$(( (CURRENT_PORT_IDX + 1) % port_count ))
|
|
||||||
|
|
||||||
if [ "$next_idx" -eq 0 ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "PORT HOP → port idx $next_idx on server $CURRENT_SERVER"
|
|
||||||
switch_endpoint "$CURRENT_SERVER" "$next_idx"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Try next server ──
|
|
||||||
try_next_server() {
|
|
||||||
next=$((CURRENT_SERVER + 1))
|
|
||||||
if [ "$next" -gt "$SERVER_COUNT" ]; then
|
|
||||||
next=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$next" -eq "$CURRENT_SERVER" ]; then
|
|
||||||
log "WARN: only one server, can't failover"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "FAILOVER → server $next"
|
|
||||||
switch_endpoint "$next" "0"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Check if primary is back (every 5 min when on secondary) ──
|
|
||||||
check_primary() {
|
|
||||||
if [ "$CURRENT_SERVER" -eq 1 ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
now=$(date +%s)
|
|
||||||
elapsed=$((now - PRIMARY_CHECK_TS))
|
|
||||||
if [ "$elapsed" -lt 300 ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo "$now" > "$STATE/primary_check_ts"
|
|
||||||
|
|
||||||
eval "host=\$SERVER_1_HOST"
|
|
||||||
port=$(get_port 1 0)
|
|
||||||
|
|
||||||
# Quick UDP probe — send a byte, see if port responds
|
|
||||||
if ping -c 1 -W 2 "$host" >/dev/null 2>&1; then
|
|
||||||
log "PRIMARY alive, switching back"
|
|
||||||
switch_endpoint 1 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Main logic ──
|
|
||||||
AGE=$(get_handshake_age)
|
|
||||||
|
|
||||||
if [ "$AGE" -lt "$HANDSHAKE_WARN" ]; then
|
|
||||||
# All good, reset counters
|
|
||||||
if [ "$RESTART_COUNT" -gt 0 ]; then
|
|
||||||
log "OK: connection restored (age=${AGE}s)"
|
|
||||||
echo "0" > "$STATE/restart_count"
|
|
||||||
fi
|
|
||||||
check_primary
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "STALE handshake: ${AGE}s (server=$CURRENT_SERVER port_idx=$CURRENT_PORT_IDX restarts=$RESTART_COUNT)"
|
|
||||||
|
|
||||||
if [ "$AGE" -lt "$HANDSHAKE_RESTART_OBF" ]; then
|
|
||||||
# Restart obfuscator only
|
|
||||||
RESTART_COUNT=$((RESTART_COUNT + 1))
|
|
||||||
echo "$RESTART_COUNT" > "$STATE/restart_count"
|
|
||||||
log "ACTION: restart obfuscator (attempt $RESTART_COUNT)"
|
|
||||||
killall wg-obfuscator 2>/dev/null
|
|
||||||
sleep 1
|
|
||||||
wg-obfuscator --config "$OBF_CONF" &
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$AGE" -lt "$HANDSHAKE_RESTART_WG" ]; then
|
|
||||||
# Restart WireGuard + obfuscator
|
|
||||||
RESTART_COUNT=$((RESTART_COUNT + 1))
|
|
||||||
echo "$RESTART_COUNT" > "$STATE/restart_count"
|
|
||||||
log "ACTION: restart WG + obfuscator (attempt $RESTART_COUNT)"
|
|
||||||
wg-quick down wg0 2>/dev/null
|
|
||||||
killall wg-obfuscator 2>/dev/null
|
|
||||||
sleep 2
|
|
||||||
wg-quick up wg0 2>/dev/null
|
|
||||||
sleep 1
|
|
||||||
wg-obfuscator --config "$OBF_CONF" &
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$AGE" -lt "$HANDSHAKE_FAILOVER" ]; then
|
|
||||||
# Try next port
|
|
||||||
log "ACTION: try port hop"
|
|
||||||
if ! try_next_port; then
|
|
||||||
log "ACTION: all ports exhausted, try next server"
|
|
||||||
try_next_server
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Full failover
|
|
||||||
log "ACTION: failover (handshake ${AGE}s)"
|
|
||||||
if ! try_next_server; then
|
|
||||||
# Only one server, cycle ports
|
|
||||||
try_next_port
|
|
||||||
fi
|
|
||||||
107
docs/PORTS.md
@@ -1,107 +0,0 @@
|
|||||||
# PCA Phobos ports and firewall checklist
|
|
||||||
|
|
||||||
This page is the short truth table for normal Phobos work. Use it before
|
|
||||||
opening a ticket about "router does not switch", "secondary is online but sync
|
|
||||||
fails", or "panel is too exposed".
|
|
||||||
|
|
||||||
## Primary server
|
|
||||||
|
|
||||||
| Port | Protocol | Who connects | Required | Why |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| `PANEL_PORT` | TCP | admin browser, secondary setup/register, router pull fallback | yes | Web panel and `/api/*`. Real value: `cat /opt/phobos-panel/.port`. |
|
|
||||||
| `80` | TCP | routers during install, Android APK download | recommended | nginx serves `/init/`, `/packages/`, `/app/`. |
|
|
||||||
| `2083,5443,993` | UDP | routers and Android PhobosWG | yes | wg-obfuscator public entry ports. Real value: `grep OBFUSCATOR_PORTS /opt/Phobos/server/server.env`. |
|
|
||||||
| `51820` | UDP | plain WireGuard clients only | no by default | Usually blocked to force obfuscation. Open only if installed with `ALLOW_PLAIN_WG=1` for iOS/plain WG. |
|
|
||||||
| `22` | TCP | admin only | optional | SSH maintenance. Do not expose broadly. |
|
|
||||||
|
|
||||||
`PANEL_PORT` is random on clean install unless passed as env:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat /opt/phobos-panel/.port
|
|
||||||
```
|
|
||||||
|
|
||||||
## Secondary server
|
|
||||||
|
|
||||||
| Port | Protocol | Who connects | Required | Why |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| `8444` | TCP | primary panel, routers via tunnel/public path | yes | Mini-API: health, peer sync, router config cache. |
|
|
||||||
| `2083,5443,993` | UDP | routers and Android PhobosWG | yes | wg-obfuscator public entry ports. |
|
|
||||||
| `51820` | UDP | nobody public | no | WireGuard listens locally behind obfuscator; public access is blocked. |
|
|
||||||
| `22` | TCP | admin during install | optional | SSH maintenance. |
|
|
||||||
|
|
||||||
If secondary is "Online" but `Info`, `Sync`, or client assignment fails, check
|
|
||||||
`8444/tcp` first and confirm the API key matches the main panel key.
|
|
||||||
|
|
||||||
## Router outbound access
|
|
||||||
|
|
||||||
Routers need outbound access to:
|
|
||||||
|
|
||||||
- every server UDP obfuscator port, default `2083,5443,993`;
|
|
||||||
- primary `PANEL_PORT/tcp` for initial install and public pull fallback;
|
|
||||||
- primary `80/tcp` if using the nginx install fallback;
|
|
||||||
- tunnel management paths after VPN is up:
|
|
||||||
- `http://10.25.0.1:8444` for secondary mini-API;
|
|
||||||
- `http(s)://10.25.0.1:<PANEL_PORT>` for primary panel API.
|
|
||||||
|
|
||||||
Current router packages write these files on the router:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/opt/etc/Phobos/panel_url
|
|
||||||
/opt/etc/Phobos/panel_urls
|
|
||||||
/opt/etc/Phobos/panel_port
|
|
||||||
/opt/etc/Phobos/panel_scheme
|
|
||||||
```
|
|
||||||
|
|
||||||
If a router does not switch after changing its server in the panel, check:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat /opt/etc/Phobos/panel_urls
|
|
||||||
cat /opt/etc/Phobos/failover.conf
|
|
||||||
tail -n 80 /opt/etc/Phobos/health.log
|
|
||||||
/opt/etc/Phobos/phobos-pull.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
If `panel_urls` contains an old IP or old panel port, create a fresh install
|
|
||||||
command in the panel and reinstall/update that router package.
|
|
||||||
|
|
||||||
## HTTPS panel
|
|
||||||
|
|
||||||
Native HTTPS is supported by the primary installer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PANEL_TLS_CERT=/etc/letsencrypt/live/example.com/fullchain.pem \
|
|
||||||
PANEL_TLS_KEY=/etc/letsencrypt/live/example.com/privkey.pem \
|
|
||||||
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/install.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the panel runs as:
|
|
||||||
|
|
||||||
```text
|
|
||||||
https://<server-ip>:<PANEL_PORT>
|
|
||||||
```
|
|
||||||
|
|
||||||
The generated router and secondary commands use `curl -k`/`wget
|
|
||||||
--no-check-certificate` fallback so a self-signed certificate can work for
|
|
||||||
bootstrap. For humans in a browser, use a real certificate to avoid warnings.
|
|
||||||
|
|
||||||
## Can I IP-whitelist the panel port?
|
|
||||||
|
|
||||||
Yes, but with care. `PANEL_PORT` is not only the admin UI. It also carries
|
|
||||||
server registration and router pull API. If you whitelist it too tightly, these
|
|
||||||
features break:
|
|
||||||
|
|
||||||
- secondary auto-register;
|
|
||||||
- `Info` / `Sync` / client assignment to secondary;
|
|
||||||
- router pull fallback before tunnel is up;
|
|
||||||
- router config update when public path is the only available path.
|
|
||||||
|
|
||||||
Minimum allowlist for `PANEL_PORT`:
|
|
||||||
|
|
||||||
- your admin IPs;
|
|
||||||
- all secondary server public IPs;
|
|
||||||
- router public IPs, if they are static;
|
|
||||||
- or no router IP restriction until first install is complete and tunnel pull is proven.
|
|
||||||
|
|
||||||
For CGNAT/dynamic-router fleets, do not rely on a strict public-IP allowlist for
|
|
||||||
router management. Use strong panel password, API key secrecy, optional HTTPS,
|
|
||||||
and keep only the required ports open.
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
# WARP in `dev` — modes, checks, current limits
|
|
||||||
|
|
||||||
Date: `2026-06-06`
|
|
||||||
Channel: `dev`
|
|
||||||
Version: `v1.3.1-dev`
|
|
||||||
|
|
||||||
## What was added
|
|
||||||
|
|
||||||
The `dev` panel now has a dedicated `WARP / WARP+` block in `/settings`.
|
|
||||||
|
|
||||||
Supported modes:
|
|
||||||
|
|
||||||
- `egress` — route client outbound traffic through WARP on the server side;
|
|
||||||
- `relay` — expose a UDP relay for a separate WARP/WireGuard client profile;
|
|
||||||
- `custom profile` — save a full custom `warp.conf` from the web UI;
|
|
||||||
- `WARP+ key` — save a license key and rebuild the profile through `wgcf`.
|
|
||||||
|
|
||||||
The UI also has a `?` help block with plain-language explanations:
|
|
||||||
|
|
||||||
- what each mode does;
|
|
||||||
- when to use `free` vs `custom`;
|
|
||||||
- what `WARP+ key` changes;
|
|
||||||
- what `Enable` and `Disable` do;
|
|
||||||
- how per-client WARP works from the client list.
|
|
||||||
|
|
||||||
## How the modes work
|
|
||||||
|
|
||||||
### `egress`
|
|
||||||
|
|
||||||
Use this when you want Phobos clients to keep connecting the usual way, but send
|
|
||||||
their outgoing traffic through Cloudflare WARP after they land on the server.
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
|
|
||||||
- WireGuard clients still connect to the Phobos server;
|
|
||||||
- server-side policy routing sends only selected client tunnel IPs through `warp`;
|
|
||||||
- NAT is done with `POSTROUTING MASQUERADE`;
|
|
||||||
- client rows in `/clients` have `WARP ON/OFF`, which adds/removes `ip rule from <client-ip>/32`.
|
|
||||||
|
|
||||||
### `relay`
|
|
||||||
|
|
||||||
Use this when you need a separate relay endpoint for another WARP/WireGuard
|
|
||||||
client profile.
|
|
||||||
|
|
||||||
Important:
|
|
||||||
|
|
||||||
- this is **not** a drop-in replacement for the normal Phobos router config;
|
|
||||||
- the client must use a separate WARP/WireGuard profile aimed at the relay port;
|
|
||||||
- this mode is for relay-style scenarios, not for replacing the regular
|
|
||||||
`home.conf` path one-to-one.
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
|
|
||||||
- UDP relay rules are added with tagged iptables entries;
|
|
||||||
- incoming UDP on the selected port is DNAT'ed to the target WARP endpoint;
|
|
||||||
- outgoing packets are MASQUERADE'd back through the relay host.
|
|
||||||
|
|
||||||
### `custom profile` and `WARP+`
|
|
||||||
|
|
||||||
Two ways to use a non-default profile:
|
|
||||||
|
|
||||||
- paste a full custom `warp.conf`;
|
|
||||||
- save a `WARP+ key` and let `wgcf` rebuild the profile from it.
|
|
||||||
|
|
||||||
This keeps the UI simple:
|
|
||||||
|
|
||||||
- `free WARP` still works out of the box;
|
|
||||||
- `WARP+` is possible when the owner has a real key;
|
|
||||||
- no hidden magic is required outside the panel.
|
|
||||||
|
|
||||||
## Live checks completed
|
|
||||||
|
|
||||||
### Server-side checks
|
|
||||||
|
|
||||||
Passed:
|
|
||||||
|
|
||||||
- `python3 -m py_compile app.py server/api.py`
|
|
||||||
- `bash -n install.sh update.sh server/phobos-warp.sh`
|
|
||||||
- live `status` checks on dev server `64.188.81.115`
|
|
||||||
|
|
||||||
Verified on the dev server:
|
|
||||||
|
|
||||||
- `egress` mode enables cleanly;
|
|
||||||
- current egress IPv4 is reported in status JSON;
|
|
||||||
- `custom profile` and `WARP+ key` save from the web UI;
|
|
||||||
- relay rules appear with separate tagged iptables entries.
|
|
||||||
|
|
||||||
### Real router check
|
|
||||||
|
|
||||||
Router used for testing:
|
|
||||||
|
|
||||||
- public SSH: `95.72.5.253`
|
|
||||||
- web: `https://homesmart.netcraze.pro/dashboard`
|
|
||||||
|
|
||||||
Confirmed:
|
|
||||||
|
|
||||||
- real router traffic was sent through the dev server;
|
|
||||||
- with `egress` enabled, a live `curl https://api.ipify.org` from the router
|
|
||||||
returned WARP egress IP `104.28.193.219`;
|
|
||||||
- this proves the real data path for `router -> Phobos -> WARP -> internet`.
|
|
||||||
|
|
||||||
### Relay check
|
|
||||||
|
|
||||||
Confirmed:
|
|
||||||
|
|
||||||
- relay mode enables from the server side;
|
|
||||||
- relay iptables rules are created;
|
|
||||||
- counters increase on live traffic;
|
|
||||||
- a separate WARP profile pointed at the relay port completes handshake.
|
|
||||||
|
|
||||||
Not fully closed yet:
|
|
||||||
|
|
||||||
- relay handshake and packet flow were confirmed;
|
|
||||||
- a full end-to-end "internet works through relay profile" check on the router
|
|
||||||
was not fully finished in a clean state before router access became limited.
|
|
||||||
|
|
||||||
## Router recovery note
|
|
||||||
|
|
||||||
During live tests, WAN SSH access to the router stopped responding, so full file
|
|
||||||
restore over shell was blocked.
|
|
||||||
|
|
||||||
What was still restored safely:
|
|
||||||
|
|
||||||
- temporary KeenDNS helper proxies created for testing were removed;
|
|
||||||
- temporary extra WireGuard peer entries were cleaned up;
|
|
||||||
- the router is currently left on a **working direct dev WireGuard peer** so it
|
|
||||||
stays reachable through the test tunnel rather than returning to a broken
|
|
||||||
down state.
|
|
||||||
|
|
||||||
What still needs one final shell pass later:
|
|
||||||
|
|
||||||
- restore `/opt/etc/Phobos/home.conf` from backup;
|
|
||||||
- restore `/opt/etc/Phobos/wg-obfuscator.conf` from backup;
|
|
||||||
- restore `/opt/etc/Phobos/failover.conf` from backup;
|
|
||||||
- restore minute cron jobs for `phobos-health.sh` and `phobos-pull.sh`.
|
|
||||||
|
|
||||||
Known backup path on the router:
|
|
||||||
|
|
||||||
- `/opt/etc/Phobos/test-backup-20260606-044834`
|
|
||||||
|
|
||||||
## Current honest status
|
|
||||||
|
|
||||||
Ready now:
|
|
||||||
|
|
||||||
- `egress` mode
|
|
||||||
- per-client `WARP ON/OFF`
|
|
||||||
- `custom profile`
|
|
||||||
- `WARP+ key`
|
|
||||||
- UI help text
|
|
||||||
|
|
||||||
Partially verified:
|
|
||||||
|
|
||||||
- `relay` mode
|
|
||||||
|
|
||||||
Extra audit fixes after live testing:
|
|
||||||
|
|
||||||
- orphan WireGuard peers now show up in both `Sessions` and `Clients` as `orphan-*` instead of disappearing into `unknown`;
|
|
||||||
- orphan rows can be `Kick`ed, `Purge`d, and also toggled through per-peer WARP;
|
|
||||||
- client package build no longer fails when `/opt/Phobos/bin` is missing or `OBFUSCATOR_KEY` is empty;
|
|
||||||
- update / restore scripts now restart the active panel service automatically (`phobos-panel` or `phobos-dev-test`).
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Скриншоты
|
|
||||||
|
|
||||||
Положи сюда 3 PNG (панель открыта — сделай скрины своих страниц):
|
|
||||||
|
|
||||||
- `sessions.png` — страница «Сессии»
|
|
||||||
- `clients.png` — страница «Клиенты»
|
|
||||||
- `servers.png` — страница «Серверы»
|
|
||||||
|
|
||||||
README в корне на них ссылается. Рекомендуемая ширина ~1000–1400px.
|
|
||||||
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 221 KiB |
350
install.sh
@@ -1,350 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ============================================================
|
|
||||||
# PCA Phobos — TURNKEY installer (primary / panel node)
|
|
||||||
#
|
|
||||||
# One command, all dependencies, from a clean VPS:
|
|
||||||
# bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/install.sh)
|
|
||||||
#
|
|
||||||
# Installs, in order:
|
|
||||||
# deps -> wg-obfuscator (Ground-Zerro) -> WireGuard wg0 ->
|
|
||||||
# obfuscator services -> Phobos repo (onboarding scripts) +
|
|
||||||
# PCA patches -> web panel -> nginx (/init,/packages) ->
|
|
||||||
# server-side router watchdog.
|
|
||||||
#
|
|
||||||
# Env (all optional):
|
|
||||||
# PANEL_PORT random 10000-59999 web panel port
|
|
||||||
# PANEL_TLS_CERT / PANEL_TLS_KEY enable native HTTPS for panel
|
|
||||||
# PANEL_PASS OcAdmin2026! panel admin password
|
|
||||||
# API_KEY random shared key (agents + router pull token)
|
|
||||||
# OBF_PORTS 2083,5443,993 obfuscator listen ports
|
|
||||||
# TG_TOKEN / TG_CHAT Telegram alerts
|
|
||||||
# PCA_BRANCH main branch to pull PCA files from
|
|
||||||
# ============================================================
|
|
||||||
set -e
|
|
||||||
|
|
||||||
PANEL_PASS="${PANEL_PASS:-OcAdmin2026!}"
|
|
||||||
PANEL_TLS_CERT="${PANEL_TLS_CERT:-}"
|
|
||||||
PANEL_TLS_KEY="${PANEL_TLS_KEY:-}"
|
|
||||||
PANEL_SCHEME="${PANEL_SCHEME:-http}"
|
|
||||||
TG_TOKEN="${TG_TOKEN:-}"
|
|
||||||
TG_CHAT="${TG_CHAT:-}"
|
|
||||||
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
|
||||||
SERVER_WG_IPV4_NETWORK="${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}"
|
|
||||||
# Источник: Gitea git.bobyrev.com (основной), GitHub — запасной.
|
|
||||||
# Каналы stable/beta/dev — ветки main/beta/dev одного публичного репозитория.
|
|
||||||
PCA_GIT="${PCA_GIT:-https://git.bobyrev.com/PCA/PCA_Phobos}"
|
|
||||||
GH_RAW="${GH_RAW:-https://raw.githubusercontent.com/andrey271192/PCA_Phobos}"
|
|
||||||
CHANNEL="${CHANNEL:-stable}"
|
|
||||||
case "$CHANNEL" in
|
|
||||||
stable|main|"") PCA_BRANCH="${PCA_BRANCH:-main}";;
|
|
||||||
*) PCA_BRANCH="${PCA_BRANCH:-$CHANNEL}";;
|
|
||||||
esac
|
|
||||||
case "$PCA_BRANCH" in v[0-9]*) _kind=tag;; *) _kind=branch;; esac
|
|
||||||
PHOBOS_DIR="/opt/Phobos"
|
|
||||||
PANEL_DIR="/opt/phobos-panel"
|
|
||||||
RAW="$PCA_GIT/raw/$_kind/$PCA_BRANCH"
|
|
||||||
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
[ "$EUID" -eq 0 ] || { echo "Run as root"; exit 1; }
|
|
||||||
|
|
||||||
if [ -z "$PANEL_PORT" ]; then
|
|
||||||
PANEL_PORT=$(shuf -i 10000-59999 -n 1 2>/dev/null || awk 'BEGIN{srand(); print int(10000+rand()*50000)}')
|
|
||||||
fi
|
|
||||||
if [ -n "$PANEL_TLS_CERT" ] || [ -n "$PANEL_TLS_KEY" ]; then
|
|
||||||
[ -f "$PANEL_TLS_CERT" ] || { echo "ERROR: PANEL_TLS_CERT not found: $PANEL_TLS_CERT"; exit 1; }
|
|
||||||
[ -f "$PANEL_TLS_KEY" ] || { echo "ERROR: PANEL_TLS_KEY not found: $PANEL_TLS_KEY"; exit 1; }
|
|
||||||
PANEL_SCHEME="https"
|
|
||||||
fi
|
|
||||||
case "$PANEL_SCHEME" in http|https) ;; *) echo "ERROR: PANEL_SCHEME must be http or https"; exit 1;; esac
|
|
||||||
API_KEY="${API_KEY:-$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24)}"
|
|
||||||
WG_SERVER_IPV4="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
|
|
||||||
import ipaddress, sys
|
|
||||||
net = ipaddress.ip_network(sys.argv[1], strict=False)
|
|
||||||
print(next(net.hosts()))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
WG_PREFIXLEN="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
|
|
||||||
import ipaddress, sys
|
|
||||||
print(ipaddress.ip_network(sys.argv[1], strict=False).prefixlen)
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
|
|
||||||
SERVER_IP=$(curl -s -m8 https://api.ipify.org || hostname -I | awk '{print $1}')
|
|
||||||
IFACE=$(ip route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1)
|
|
||||||
IFACE="${IFACE:-eth0}"
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
|
|
||||||
echo "============================================"
|
|
||||||
echo " PCA Phobos - turnkey primary install"
|
|
||||||
echo " IP=$SERVER_IP iface=$IFACE arch=$ARCH"
|
|
||||||
echo " panel port=$PANEL_PORT obf ports=$OBF_PORTS"
|
|
||||||
echo "============================================"
|
|
||||||
|
|
||||||
# ── 1. dependencies ──
|
|
||||||
echo "[1/9] dependencies..."
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
apt-get update -qq
|
|
||||||
apt-get install -y -qq wireguard wireguard-tools iptables jq curl git ca-certificates \
|
|
||||||
python3 python3-flask python3-pip gunicorn nginx cron python3-qrcode sshpass >/dev/null
|
|
||||||
apt-get install -y -qq python3-flask-sock >/dev/null 2>&1 || true
|
|
||||||
apt-get install -y -qq python3-asyncssh >/dev/null 2>&1 || true
|
|
||||||
apt-get install -y -qq python3-websocket >/dev/null 2>&1 || true
|
|
||||||
python3 - <<'PY' >/dev/null 2>&1 || python3 -m pip install flask-sock asyncssh websocket-client >/dev/null 2>&1 || python3 -m pip install --break-system-packages flask-sock asyncssh websocket-client >/dev/null
|
|
||||||
import flask_sock, asyncssh, websocket
|
|
||||||
PY
|
|
||||||
mkdir -p "$PANEL_DIR/static"
|
|
||||||
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css -o "$PANEL_DIR/static/xterm.min.css" 2>/dev/null || true
|
|
||||||
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js -o "$PANEL_DIR/static/xterm.min.js" 2>/dev/null || true
|
|
||||||
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js -o "$PANEL_DIR/static/xterm-addon-fit.min.js" 2>/dev/null || true
|
|
||||||
systemctl enable cron -q 2>/dev/null || true; systemctl start cron 2>/dev/null || true
|
|
||||||
|
|
||||||
# ── 2. wg-obfuscator binary (Ground-Zerro) ──
|
|
||||||
echo "[2/9] wg-obfuscator..."
|
|
||||||
mkdir -p "$PHOBOS_DIR"/{server,clients,bin,tokens,www/init,www/packages,packages}
|
|
||||||
OBF_ARCH="$ARCH"
|
|
||||||
[ "$OBF_ARCH" = "amd64" ] && OBF_ARCH="x86_64"
|
|
||||||
[ "$OBF_ARCH" = "arm64" ] && OBF_ARCH="aarch64"
|
|
||||||
if [ ! -x /usr/local/bin/wg-obfuscator ]; then
|
|
||||||
R=/tmp/phobos-obf; rm -rf "$R"; mkdir -p "$R"; cd "$R"
|
|
||||||
git init -q; git remote add origin https://github.com/Ground-Zerro/Phobos.git
|
|
||||||
git config core.sparseCheckout true; echo "wg-obfuscator" > .git/info/sparse-checkout
|
|
||||||
git pull origin main -q
|
|
||||||
cp -f "wg-obfuscator/bin/"wg-obfuscator-* "$PHOBOS_DIR/bin/" 2>/dev/null || true
|
|
||||||
chmod +x "$PHOBOS_DIR/bin/"wg-obfuscator-* 2>/dev/null || true
|
|
||||||
ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${OBF_ARCH}" /usr/local/bin/wg-obfuscator 2>/dev/null || true
|
|
||||||
cd /; rm -rf "$R"
|
|
||||||
fi
|
|
||||||
_OBF_TAG=$(curl -fsSL -m10 https://api.github.com/repos/ClusterM/wg-obfuscator/releases/latest 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*/\1/')
|
|
||||||
_CB="https://github.com/ClusterM/wg-obfuscator/releases/download/${_OBF_TAG}"
|
|
||||||
for _am in "x86_64:linux-x64" "aarch64:linux-arm64" "mipsel:linux-mipsel-mips32" "mips:linux-mips-mips32" "armv7:linux-armv7-hf"; do
|
|
||||||
_da="${_am%%:*}"; _ss="${_am##*:}"
|
|
||||||
if [ ! -f "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" ] && [ -n "$_OBF_TAG" ]; then
|
|
||||||
_t=$(mktemp -d)
|
|
||||||
curl -fsSL -m30 "${_CB}/wg-obfuscator-${_OBF_TAG}-${_ss}.tar.gz" -o "$_t/o.tgz" 2>/dev/null &&
|
|
||||||
tar -xzf "$_t/o.tgz" -C "$_t" 2>/dev/null &&
|
|
||||||
_b=$(find "$_t" -name "wg-obfuscator" -type f | head -1) &&
|
|
||||||
[ -n "$_b" ] && cp "$_b" "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" && chmod +x "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" && echo " +wg-obfuscator-${_da} (ClusterM ${_OBF_TAG})"
|
|
||||||
rm -rf "$_t"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ -x /usr/local/bin/wg-obfuscator ] || ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${OBF_ARCH}" /usr/local/bin/wg-obfuscator 2>/dev/null || true
|
|
||||||
[ -x /usr/local/bin/wg-obfuscator ] || { echo "ERROR: obfuscator binary for $ARCH missing"; exit 1; }
|
|
||||||
echo " bins: $(ls $PHOBOS_DIR/bin/ | tr '\n' ' ')"
|
|
||||||
|
|
||||||
# ── 3. Phobos repo (onboarding scripts) ──
|
|
||||||
echo "[3/9] Phobos repo (onboarding scripts)..."
|
|
||||||
R="$PHOBOS_DIR/repo"; rm -rf "$R"; mkdir -p "$R"; cd "$R"
|
|
||||||
git init -q; git remote add origin https://github.com/Ground-Zerro/Phobos.git
|
|
||||||
git config core.sparseCheckout true
|
|
||||||
printf 'server\nclient\n' > .git/info/sparse-checkout
|
|
||||||
git pull origin main -q; rm -rf .git
|
|
||||||
find "$R" -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true
|
|
||||||
cd /
|
|
||||||
|
|
||||||
# ── 4. WireGuard wg0 (primary) ──
|
|
||||||
echo "[4/9] WireGuard wg0..."
|
|
||||||
if [ ! -f /etc/wireguard/wg0.conf ]; then
|
|
||||||
WG_PRIV=$(wg genkey); WG_PUB=$(echo "$WG_PRIV" | wg pubkey)
|
|
||||||
cat > /etc/wireguard/wg0.conf <<WG
|
|
||||||
[Interface]
|
|
||||||
Address = ${WG_SERVER_IPV4}/${WG_PREFIXLEN}
|
|
||||||
ListenPort = 51820
|
|
||||||
PrivateKey = $WG_PRIV
|
|
||||||
PostUp = iptables -I FORWARD 1 -i wg0 -j ACCEPT; iptables -I FORWARD 1 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE
|
|
||||||
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o $IFACE -j MASQUERADE
|
|
||||||
WG
|
|
||||||
chmod 600 /etc/wireguard/wg0.conf
|
|
||||||
else
|
|
||||||
WG_PRIV=$(grep '^PrivateKey' /etc/wireguard/wg0.conf | cut -d= -f2- | tr -d ' ')
|
|
||||||
WG_PUB=$(echo "$WG_PRIV" | wg pubkey)
|
|
||||||
fi
|
|
||||||
sysctl -w net.ipv4.ip_forward=1 -q
|
|
||||||
grep -q '^net.ipv4.ip_forward = 1' /etc/sysctl.conf || echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
|
|
||||||
systemctl enable wg-quick@wg0 -q 2>/dev/null || true
|
|
||||||
systemctl restart wg-quick@wg0
|
|
||||||
|
|
||||||
# ── 5. obfuscator services (multi-port) ──
|
|
||||||
echo "[5/9] obfuscator services..."
|
|
||||||
OBF_KEY=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 32)
|
|
||||||
# Block direct WG (force obfuscation) unless ALLOW_PLAIN_WG=1 (e.g. for iOS WireGuard)
|
|
||||||
if [ -z "${ALLOW_PLAIN_WG:-}" ]; then
|
|
||||||
iptables -C INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null \
|
|
||||||
|| iptables -A INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP
|
|
||||||
else
|
|
||||||
iptables -D INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
IFS=',' read -ra PORTS <<< "$OBF_PORTS"
|
|
||||||
for PORT in "${PORTS[@]}"; do
|
|
||||||
cat > "$PHOBOS_DIR/server/wg-obfuscator-${PORT}.conf" <<EOF
|
|
||||||
[instance]
|
|
||||||
source-if = 0.0.0.0
|
|
||||||
source-lport = ${PORT}
|
|
||||||
target = 127.0.0.1:51820
|
|
||||||
key = ${OBF_KEY}
|
|
||||||
masking = AUTO
|
|
||||||
verbose = INFO
|
|
||||||
idle-timeout = 300
|
|
||||||
max-dummy = 50
|
|
||||||
EOF
|
|
||||||
cat > /etc/systemd/system/wg-obfuscator-${PORT}.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=WireGuard Obfuscator (port ${PORT})
|
|
||||||
After=network.target wg-quick@wg0.service
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/bin/wg-obfuscator --config ${PHOBOS_DIR}/server/wg-obfuscator-${PORT}.conf
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
done
|
|
||||||
systemctl daemon-reload
|
|
||||||
for PORT in "${PORTS[@]}"; do systemctl enable wg-obfuscator-${PORT} -q; systemctl restart wg-obfuscator-${PORT}; done
|
|
||||||
|
|
||||||
# server.env (primary)
|
|
||||||
cat > "$PHOBOS_DIR/server/server.env" <<EOF
|
|
||||||
SERVER_WG_PRIVATE_KEY=$WG_PRIV
|
|
||||||
SERVER_WG_PUBLIC_KEY=$WG_PUB
|
|
||||||
SERVER_PUBLIC_IP_V4=$SERVER_IP
|
|
||||||
SERVER_WG_IPV4_NETWORK=$SERVER_WG_IPV4_NETWORK
|
|
||||||
SERVER_WG_IPV4_ADDRESS=${WG_SERVER_IPV4}/${WG_PREFIXLEN}
|
|
||||||
OBFUSCATOR_KEY=$OBF_KEY
|
|
||||||
OBFUSCATOR_PORTS=$OBF_PORTS
|
|
||||||
CLIENT_WG_PORT=51820
|
|
||||||
ROLE=primary
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# ── 6. PCA patches over onboarding scripts + server-side helpers ──
|
|
||||||
echo "[6/9] PCA patches (tunnel-pull, self-heal, watchdog, 403 fix)..."
|
|
||||||
fetch() {
|
|
||||||
mkdir -p "$(dirname "$2")"
|
|
||||||
# ponytail: файл из архива рядом с install.sh — не ходим в сеть.
|
|
||||||
if [ -f "$SRC/$1" ]; then cp -f "$SRC/$1" "$2" && return 0; fi
|
|
||||||
curl -fsSL -m20 "$RAW/$1?cb=$(date +%s)" -o "$2" && return 0
|
|
||||||
curl -fsSL -m20 "$GH_RAW/$PCA_BRANCH/$1?cb=$(date +%s)" -o "$2" && return 0
|
|
||||||
echo " WARN: fetch $1 failed"; return 1
|
|
||||||
}
|
|
||||||
fetch overlay/lib-core.sh "$PHOBOS_DIR/repo/server/scripts/lib-core.sh" && chmod +x "$PHOBOS_DIR/repo/server/scripts/lib-core.sh"
|
|
||||||
fetch overlay/phobos-client.sh "$PHOBOS_DIR/repo/server/scripts/phobos-client.sh" && chmod +x "$PHOBOS_DIR/repo/server/scripts/phobos-client.sh"
|
|
||||||
fetch overlay/install-router.sh.template "$PHOBOS_DIR/repo/client/templates/install-router.sh.template"
|
|
||||||
fetch overlay/lib-client.sh "$PHOBOS_DIR/repo/client/templates/lib-client.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/lib-client.sh"
|
|
||||||
fetch overlay/install-obfuscator.sh "$PHOBOS_DIR/repo/client/templates/install-obfuscator.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/install-obfuscator.sh"
|
|
||||||
fetch overlay/install-wireguard.sh "$PHOBOS_DIR/repo/client/templates/install-wireguard.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/install-wireguard.sh"
|
|
||||||
fetch overlay/router-configure-wireguard.sh "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard.sh"
|
|
||||||
fetch overlay/router-configure-wireguard-openwrt.sh "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard-openwrt.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard-openwrt.sh"
|
|
||||||
fetch overlay/phobos-pull.sh "$PHOBOS_DIR/repo/client/templates/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-pull.sh"
|
|
||||||
fetch overlay/phobos-ttyd.sh "$PHOBOS_DIR/repo/client/templates/phobos-ttyd.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-ttyd.sh" && cp "$PHOBOS_DIR/repo/client/templates/phobos-ttyd.sh" "$PHOBOS_DIR/www/phobos-ttyd.sh" 2>/dev/null || true
|
|
||||||
fetch overlay/phobos-uninstall.sh "$PHOBOS_DIR/repo/client/templates/phobos-uninstall.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-uninstall.sh"
|
|
||||||
fetch overlay/3xui.sh "$PHOBOS_DIR/repo/client/templates/3xui.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/3xui.sh"
|
|
||||||
fetch server/phobos-warp.sh "$PHOBOS_DIR/server/phobos-warp.sh" && chmod +x "$PHOBOS_DIR/server/phobos-warp.sh" && ln -sf "$PHOBOS_DIR/server/phobos-warp.sh" /usr/local/bin/phobos-warp
|
|
||||||
fetch server/phobos-guard.sh "$PHOBOS_DIR/server/phobos-guard.sh" && chmod +x "$PHOBOS_DIR/server/phobos-guard.sh" && ln -sf "$PHOBOS_DIR/server/phobos-guard.sh" /usr/local/bin/phobos-guard
|
|
||||||
fetch server/phobos-cascade.sh "$PHOBOS_DIR/server/phobos-cascade.sh" && chmod +x "$PHOBOS_DIR/server/phobos-cascade.sh" && ln -sf "$PHOBOS_DIR/server/phobos-cascade.sh" /usr/local/bin/phobos-cascade
|
|
||||||
fetch server/phobos-domain.sh "$PHOBOS_DIR/server/phobos-domain.sh" && chmod +x "$PHOBOS_DIR/server/phobos-domain.sh" && ln -sf "$PHOBOS_DIR/server/phobos-domain.sh" /usr/local/bin/phobos-domain
|
|
||||||
fetch server/phobos-limit.sh "$PHOBOS_DIR/server/phobos-limit.sh" && chmod +x "$PHOBOS_DIR/server/phobos-limit.sh"
|
|
||||||
fetch server/phobos-speedtest.sh "$PHOBOS_DIR/server/phobos-speedtest.sh" && chmod +x "$PHOBOS_DIR/server/phobos-speedtest.sh"
|
|
||||||
fetch server/phobos-tcp.sh "$PHOBOS_DIR/server/phobos-tcp.sh" && chmod +x "$PHOBOS_DIR/server/phobos-tcp.sh" && ln -sf "$PHOBOS_DIR/server/phobos-tcp.sh" /usr/local/bin/phobos-tcp
|
|
||||||
fetch "udptcp/bin/udptcp-$OBF_ARCH" "$PHOBOS_DIR/bin/udptcp" && chmod +x "$PHOBOS_DIR/bin/udptcp"
|
|
||||||
fetch server/phobos-health.sh "$PHOBOS_DIR/server/phobos-health.sh" && chmod +x "$PHOBOS_DIR/server/phobos-health.sh"
|
|
||||||
fetch server/phobos-self-check.sh "$PHOBOS_DIR/server/phobos-self-check.sh" && chmod +x "$PHOBOS_DIR/server/phobos-self-check.sh" && ln -sf "$PHOBOS_DIR/server/phobos-self-check.sh" /usr/local/bin/phobos-self-check
|
|
||||||
fetch server/phobos-pull.sh "$PHOBOS_DIR/server/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/server/phobos-pull.sh"
|
|
||||||
fetch server/phobos-router-watchdog.py "$PHOBOS_DIR/server/phobos-router-watchdog.py"
|
|
||||||
fetch server/api.py "$PHOBOS_DIR/server/api.py"
|
|
||||||
fetch server/secondary-setup.sh "$PHOBOS_DIR/server/secondary-setup.sh" && chmod +x "$PHOBOS_DIR/server/secondary-setup.sh"
|
|
||||||
fetch update.sh "$PHOBOS_DIR/server/update.sh" && chmod +x "$PHOBOS_DIR/server/update.sh" && ln -sf "$PHOBOS_DIR/server/update.sh" /usr/local/bin/phobos-update
|
|
||||||
fetch server/phobos-backup.sh "$PHOBOS_DIR/server/phobos-backup.sh" && chmod +x "$PHOBOS_DIR/server/phobos-backup.sh" && ln -sf "$PHOBOS_DIR/server/phobos-backup.sh" /usr/local/bin/phobos-backup
|
|
||||||
mkdir -p "$PANEL_DIR"; fetch VERSION "$PANEL_DIR/.version" 2>/dev/null || true
|
|
||||||
[ -f "$PHOBOS_DIR/tokens/tokens.json" ] || echo '[]' > "$PHOBOS_DIR/tokens/tokens.json"
|
|
||||||
"$PHOBOS_DIR/server/phobos-self-check.sh" --fix
|
|
||||||
|
|
||||||
# ── 7. web panel ──
|
|
||||||
echo "[7/9] web panel..."
|
|
||||||
mkdir -p "$PANEL_DIR"
|
|
||||||
fetch app.py "$PANEL_DIR/app.py" || { echo "ERROR: panel app.py fetch failed"; exit 1; }
|
|
||||||
if [ ! -f "$PANEL_DIR/settings.json" ]; then
|
|
||||||
cat > "$PANEL_DIR/settings.json" <<EOF
|
|
||||||
{
|
|
||||||
"admin_pass": "$PANEL_PASS",
|
|
||||||
"tg_bot_token": "$TG_TOKEN",
|
|
||||||
"tg_chat_id": "$TG_CHAT",
|
|
||||||
"monitor_interval": 30,
|
|
||||||
"server_api_key": "$API_KEY",
|
|
||||||
"labels": {},
|
|
||||||
"subscriptions": {},
|
|
||||||
"router_access": {},
|
|
||||||
"client_assignments": {}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
echo "$PANEL_PORT" > "$PANEL_DIR/.port"
|
|
||||||
echo "$PANEL_SCHEME" > "$PANEL_DIR/.scheme"
|
|
||||||
TLS_ARGS=""
|
|
||||||
if [ "$PANEL_SCHEME" = "https" ]; then
|
|
||||||
TLS_ARGS="--certfile $PANEL_TLS_CERT --keyfile $PANEL_TLS_KEY"
|
|
||||||
fi
|
|
||||||
cat > /etc/systemd/system/phobos-panel.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Phobos VPN Web Panel
|
|
||||||
After=network.target wg-quick@wg0.service
|
|
||||||
Wants=wg-quick@wg0.service
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
WorkingDirectory=$PANEL_DIR
|
|
||||||
ExecStart=/usr/bin/gunicorn -w 1 --threads 20 $TLS_ARGS -b 0.0.0.0:$PANEL_PORT app:app
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
Environment=PYTHONUNBUFFERED=1
|
|
||||||
Environment=PANEL_SCHEME=$PANEL_SCHEME
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload; systemctl enable phobos-panel -q; systemctl restart phobos-panel
|
|
||||||
|
|
||||||
# TCP fallback for clients whose UDP is cut (port 443/8443/... picked automatically)
|
|
||||||
"$PHOBOS_DIR/server/phobos-tcp.sh" enable || echo " WARN: TCP fallback not enabled"
|
|
||||||
|
|
||||||
# ── 8. nginx (serve /init + /packages over plain HTTP for routers) ──
|
|
||||||
echo "[8/9] nginx..."
|
|
||||||
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
|
|
||||||
cat > /etc/nginx/sites-available/phobos <<'NGINX'
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
location /init/ { alias /opt/Phobos/www/init/; default_type application/x-sh; }
|
|
||||||
location /packages/ { alias /opt/Phobos/www/packages/; default_type application/octet-stream; }
|
|
||||||
location /app/ { alias /opt/Phobos/www/app/; default_type application/octet-stream; }
|
|
||||||
location / { return 404; }
|
|
||||||
}
|
|
||||||
NGINX
|
|
||||||
ln -sf /etc/nginx/sites-available/phobos /etc/nginx/sites-enabled/phobos
|
|
||||||
mkdir -p /opt/Phobos/www/app; chmod 755 /opt/Phobos/www /opt/Phobos/www/init /opt/Phobos/www/packages /opt/Phobos/www/app
|
|
||||||
nginx -t >/dev/null 2>&1 && systemctl enable nginx -q 2>/dev/null && systemctl restart nginx || echo " WARN: nginx config test failed"
|
|
||||||
|
|
||||||
# ── 9. router watchdog (auto reboot-recovery) ──
|
|
||||||
echo "[9/9] router watchdog..."
|
|
||||||
if [ -f "$PHOBOS_DIR/server/phobos-router-watchdog.py" ]; then
|
|
||||||
# set -e safe: grep -v on an empty crontab returns 1, so guard with || true
|
|
||||||
CRON_CUR=$(crontab -l 2>/dev/null | grep -v phobos-router-watchdog || true)
|
|
||||||
printf '%s\n%s\n' "$CRON_CUR" \
|
|
||||||
"*/3 * * * * /usr/bin/python3 $PHOBOS_DIR/server/phobos-router-watchdog.py >/dev/null 2>&1" \
|
|
||||||
| grep -v '^[[:space:]]*$' | crontab -
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# persist channel so phobos-update follows it
|
|
||||||
mkdir -p "$PANEL_DIR"
|
|
||||||
echo "$CHANNEL" > "$PANEL_DIR/.channel" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "============================================"
|
|
||||||
echo " Installation complete"
|
|
||||||
echo " Panel : $PANEL_SCHEME://$SERVER_IP:$PANEL_PORT"
|
|
||||||
echo " Login : admin"
|
|
||||||
echo " Pass : $PANEL_PASS"
|
|
||||||
echo " API key (agents+pull): $API_KEY"
|
|
||||||
echo " WG pub: $WG_PUB"
|
|
||||||
echo "============================================"
|
|
||||||
echo "Status:"
|
|
||||||
for s in wg-quick@wg0 phobos-panel nginx; do printf " %-18s %s\n" "$s" "$(systemctl is-active $s 2>/dev/null)"; done
|
|
||||||
for PORT in "${PORTS[@]}"; do printf " %-18s %s\n" "wg-obfuscator-$PORT" "$(systemctl is-active wg-obfuscator-$PORT 2>/dev/null)"; done
|
|
||||||
3
ios/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
build/
|
|
||||||
*.xcuserstate
|
|
||||||
xcuserdata/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"images":[{"filename":"icon-1024.png","idiom":"universal","platform":"ios","size":"1024x1024"}],"info":{"author":"xcode","version":1}}
|
|
||||||
|
Before Width: | Height: | Size: 19 KiB |
@@ -1 +0,0 @@
|
|||||||
{"info":{"author":"xcode","version":1}}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import NetworkExtension
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
@EnvironmentObject var vpn: VPNController
|
|
||||||
@State private var pasteText = ""
|
|
||||||
@State private var showImport = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 18) {
|
|
||||||
profileCard
|
|
||||||
powerButton
|
|
||||||
statusLine
|
|
||||||
optionsCard
|
|
||||||
Button { showImport = true } label: {
|
|
||||||
Label("Импорт ссылки phobos://", systemImage: "square.and.arrow.down")
|
|
||||||
.frame(maxWidth: .infinity).padding(.vertical, 12)
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent).tint(.indigo)
|
|
||||||
Link(destination: URL(string: "https://speed.cloudflare.com/")!) {
|
|
||||||
Label("Проверить скорость", systemImage: "speedometer").frame(maxWidth: .infinity).padding(.vertical, 12)
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
if !vpn.message.isEmpty {
|
|
||||||
Text(vpn.message).font(.footnote).foregroundStyle(.secondary).multilineTextAlignment(.center)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.navigationTitle("PCA Phobos VPN")
|
|
||||||
.alert("Ссылка из панели", isPresented: $showImport) {
|
|
||||||
TextField("phobos://…", text: $pasteText)
|
|
||||||
Button("Импорт") { vpn.importLink(pasteText); pasteText = "" }
|
|
||||||
Button("Из буфера") { vpn.importLink(UIPasteboard.general.string ?? "") }
|
|
||||||
Button("Отмена", role: .cancel) {}
|
|
||||||
} message: {
|
|
||||||
Text("В панели: клиент → 📱 → ссылка phobos://. Можно просто открыть ссылку на iPhone — профиль импортируется сам.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var profileCard: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
if let p = vpn.profile {
|
|
||||||
Text(vpn.name).font(.title3.bold())
|
|
||||||
Text("Сервер: \(p.target ?? "?")").font(.subheadline).foregroundStyle(.secondary)
|
|
||||||
if let tcp = p.tcpPort { Text("Резервный TCP: порт \(tcp)").font(.caption).foregroundStyle(.secondary) }
|
|
||||||
} else {
|
|
||||||
Text("Профиль не импортирован").font(.headline)
|
|
||||||
Text("Откройте на iPhone ссылку phobos:// из панели PCA Phobos или вставьте её кнопкой ниже.")
|
|
||||||
.font(.subheadline).foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding().background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var powerButton: some View {
|
|
||||||
Button(action: vpn.toggle) {
|
|
||||||
Text(vpn.isOn ? "Отключить" : "Подключить")
|
|
||||||
.font(.title2.bold()).foregroundStyle(.white)
|
|
||||||
.frame(width: 180, height: 180)
|
|
||||||
.background(Circle().fill(vpn.isOn ? Color.green : Color.indigo))
|
|
||||||
}
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var statusLine: some View {
|
|
||||||
let text: String = {
|
|
||||||
switch vpn.status {
|
|
||||||
case .connected: return "🟢 VPN включён" + (vpn.transport.isEmpty ? "" : " · \(vpn.transport)")
|
|
||||||
case .connecting, .reasserting: return "🟡 Подключение…"
|
|
||||||
case .disconnecting: return "Отключение…"
|
|
||||||
default: return "⚪ VPN выключен"
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return Text(text).font(.headline)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var optionsCard: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Toggle("Российские сайты — напрямую, без VPN", isOn: $vpn.ruDirect)
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
Text("Канал до сервера").font(.subheadline.bold())
|
|
||||||
Picker("Канал", selection: $vpn.transportMode) {
|
|
||||||
Text("Авто").tag("auto"); Text("UDP").tag("udp"); Text("TCP").tag("tcp")
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
Text("Авто: если UDP режут, через 15 секунд переходит на TCP. Изменения применяются сразу.")
|
|
||||||
.font(.caption).foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding().background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?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>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>PCA Phobos</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>com.bobyrev.pcaphobos.link</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>phobos</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>UILaunchScreen</key>
|
|
||||||
<dict/>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?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>com.apple.developer.networking.networkextension</key>
|
|
||||||
<array>
|
|
||||||
<string>packet-tunnel-provider</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import NetworkExtension
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct PCAPhobosApp: App {
|
|
||||||
@StateObject private var vpn = VPNController()
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
ContentView()
|
|
||||||
.environmentObject(vpn)
|
|
||||||
.onOpenURL { vpn.importLink($0.absoluteString) } // phobos:// from the panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Owns the NETunnelProviderManager: profile, options, connect/disconnect, live status.
|
|
||||||
@MainActor
|
|
||||||
final class VPNController: ObservableObject {
|
|
||||||
@Published var name = UserDefaults.standard.string(forKey: "name") ?? ""
|
|
||||||
@Published var conf = UserDefaults.standard.string(forKey: "conf") ?? ""
|
|
||||||
@Published var ruDirect = UserDefaults.standard.object(forKey: "ruDirect") as? Bool ?? true { didSet { saveOptions() } }
|
|
||||||
@Published var transportMode = UserDefaults.standard.string(forKey: "transport") ?? "auto" { didSet { saveOptions() } }
|
|
||||||
@Published var status: NEVPNStatus = .invalid
|
|
||||||
@Published var transport = ""
|
|
||||||
@Published var message = ""
|
|
||||||
|
|
||||||
private var manager: NETunnelProviderManager?
|
|
||||||
private var timer: Timer?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
Task { await load() }
|
|
||||||
NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: .main) { [weak self] _ in
|
|
||||||
Task { @MainActor in self?.status = self?.manager?.connection.status ?? .invalid }
|
|
||||||
}
|
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
|
|
||||||
Task { @MainActor in self?.pollTransport() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var profile: PhobosProfile? { conf.isEmpty ? nil : PhobosProfile(conf) }
|
|
||||||
var isOn: Bool { status == .connected || status == .connecting || status == .reasserting }
|
|
||||||
|
|
||||||
func load() async {
|
|
||||||
let all = (try? await NETunnelProviderManager.loadAllFromPreferences()) ?? []
|
|
||||||
manager = all.first
|
|
||||||
status = manager?.connection.status ?? .invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
func importLink(_ link: String) {
|
|
||||||
guard let parsed = PhobosProfile.fromLink(link) else { message = "Это не ссылка phobos:// из панели"; return }
|
|
||||||
conf = parsed.profile.raw
|
|
||||||
name = parsed.name
|
|
||||||
UserDefaults.standard.set(conf, forKey: "conf")
|
|
||||||
UserDefaults.standard.set(name, forKey: "name")
|
|
||||||
message = "Профиль «\(name)» импортирован"
|
|
||||||
Task { await save(reconnect: isOn) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveOptions() {
|
|
||||||
UserDefaults.standard.set(ruDirect, forKey: "ruDirect")
|
|
||||||
UserDefaults.standard.set(transportMode, forKey: "transport")
|
|
||||||
Task { await save(reconnect: isOn) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the VPN configuration to iOS settings (asks the user once).
|
|
||||||
func save(reconnect: Bool = false) async {
|
|
||||||
guard let profile else { return }
|
|
||||||
let m = manager ?? NETunnelProviderManager()
|
|
||||||
let proto = NETunnelProviderProtocol()
|
|
||||||
proto.providerBundleIdentifier = "com.bobyrev.pcaphobos.tunnel"
|
|
||||||
proto.serverAddress = profile.targetHost ?? "Phobos"
|
|
||||||
proto.providerConfiguration = ["conf": conf, "ruDirect": ruDirect, "transport": transportMode]
|
|
||||||
m.protocolConfiguration = proto
|
|
||||||
m.localizedDescription = "PCA Phobos"
|
|
||||||
m.isEnabled = true
|
|
||||||
do {
|
|
||||||
try await m.saveToPreferences()
|
|
||||||
try await m.loadFromPreferences()
|
|
||||||
manager = m
|
|
||||||
if reconnect { m.connection.stopVPNTunnel(); try await Task.sleep(nanoseconds: 1_000_000_000); try m.connection.startVPNTunnel() }
|
|
||||||
} catch {
|
|
||||||
message = "Не удалось сохранить VPN: \(error.localizedDescription)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toggle() {
|
|
||||||
Task {
|
|
||||||
if isOn { manager?.connection.stopVPNTunnel(); return }
|
|
||||||
guard profile != nil else { message = "Сначала импортируйте ссылку phobos:// из панели"; return }
|
|
||||||
await save()
|
|
||||||
do { try manager?.connection.startVPNTunnel() } catch { message = "Ошибка: \(error.localizedDescription)" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func pollTransport() {
|
|
||||||
guard status == .connected, let session = manager?.connection as? NETunnelProviderSession else { transport = ""; return }
|
|
||||||
try? session.sendProviderMessage(Data()) { [weak self] data in
|
|
||||||
guard let data, let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
|
|
||||||
Task { @MainActor in self?.transport = obj["transport"] as? String ?? "" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,572 +0,0 @@
|
|||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 77;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
|
||||||
008DA398D56DFBDB16D97CE1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF14952A4263FF0E004A3F04 /* ContentView.swift */; };
|
|
||||||
08C4842C3185E04B2154C971 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA829997FAB330F6EFA104C9 /* WireGuardKit */; };
|
|
||||||
11F2A6A8D281AB36B45D4603 /* TunnelConfiguration+WgQuickConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1AA6DED9B45949864A9B7D /* TunnelConfiguration+WgQuickConfig.swift */; };
|
|
||||||
1C05D29ACA95548374AD6FD9 /* logging.c in Sources */ = {isa = PBXBuildFile; fileRef = 9FC04650634F03E6DB09AB6B /* logging.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
|
|
||||||
38870650A7C2B1F0927A1230 /* wg-obfuscator.c in Sources */ = {isa = PBXBuildFile; fileRef = 1FADBB2A65AF77D4ECCAFE78 /* wg-obfuscator.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
|
|
||||||
4E4255B9E0F8626F6E209B0F /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7500F0B31797D49B5BBEC5CC /* String+ArrayConversion.swift */; };
|
|
||||||
56F35C68C6A387E499703CFF /* masking.c in Sources */ = {isa = PBXBuildFile; fileRef = CCE5C98CD905BDE16A8D83D4 /* masking.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
|
|
||||||
5F5D376513C0B27C45A3E7B6 /* ru_bypass.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2572D9A6F822B038D0652619 /* ru_bypass.txt */; };
|
|
||||||
78B74F81E0042D9AD94B7A6B /* config.c in Sources */ = {isa = PBXBuildFile; fileRef = 0E5C1673464AAC3EF81DD2FE /* config.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
|
|
||||||
7D8AB66421409CBFCCF2769A /* PhobosProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC1765DEC5F326E2B5F9C4 /* PhobosProfile.swift */; };
|
|
||||||
99F5DB52833E3B5505EEA727 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E36BA501FDCB2034F09415A /* PacketTunnelProvider.swift */; };
|
|
||||||
B3BB8BE3409D63FEB59B5449 /* obfuscation.c in Sources */ = {isa = PBXBuildFile; fileRef = EF9115CA0D5EC52C8679A12A /* obfuscation.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
|
|
||||||
C3C41D3C419E8D17F20A38B5 /* PCAPhobosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E470E13EF603CB26991683 /* PCAPhobosApp.swift */; };
|
|
||||||
C6116E8B9CDFEE363BCBD2D6 /* PhobosTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A81F96BF4131FDBB816623BE /* PhobosTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
|
||||||
CBC338B91B5FCE1E207B36AC /* PhobosProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC1765DEC5F326E2B5F9C4 /* PhobosProfile.swift */; };
|
|
||||||
D8CCC1E6CC8164EDD4E34D25 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6359B8954E69147DA50EDB46 /* Assets.xcassets */; };
|
|
||||||
DF6D62C526E908CA5CDDFCAE /* pcarelay.c in Sources */ = {isa = PBXBuildFile; fileRef = 211950F8776D3D14D7B9C49A /* pcarelay.c */; };
|
|
||||||
E88105F9325A93041D617213 /* masking_stun.c in Sources */ = {isa = PBXBuildFile; fileRef = 05DD44245829A52D32B42AF8 /* masking_stun.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
|
|
||||||
/* End PBXBuildFile section */
|
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
|
||||||
B9875EF5DEB6362919B43F22 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = E59D1112D2B128DF6FE7A1E9 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 0FD066BC2C51A1E092914FE8;
|
|
||||||
remoteInfo = PhobosTunnel;
|
|
||||||
};
|
|
||||||
/* End PBXContainerItemProxy section */
|
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
|
||||||
9FC25420ED5EF973CE140584 /* Embed Foundation Extensions */ = {
|
|
||||||
isa = PBXCopyFilesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
dstPath = "";
|
|
||||||
dstSubfolderSpec = 13;
|
|
||||||
files = (
|
|
||||||
C6116E8B9CDFEE363BCBD2D6 /* PhobosTunnel.appex in Embed Foundation Extensions */,
|
|
||||||
);
|
|
||||||
name = "Embed Foundation Extensions";
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
027878C5E0654DB27FAE88A3 /* PCAPhobos.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = PCAPhobos.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
05DD44245829A52D32B42AF8 /* masking_stun.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = masking_stun.c; sourceTree = "<group>"; };
|
|
||||||
0E5C1673464AAC3EF81DD2FE /* config.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = config.c; sourceTree = "<group>"; };
|
|
||||||
1FADBB2A65AF77D4ECCAFE78 /* wg-obfuscator.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = "wg-obfuscator.c"; sourceTree = "<group>"; };
|
|
||||||
211950F8776D3D14D7B9C49A /* pcarelay.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = pcarelay.c; sourceTree = "<group>"; };
|
|
||||||
2572D9A6F822B038D0652619 /* ru_bypass.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ru_bypass.txt; sourceTree = "<group>"; };
|
|
||||||
2C1AA6DED9B45949864A9B7D /* TunnelConfiguration+WgQuickConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+WgQuickConfig.swift"; sourceTree = "<group>"; };
|
|
||||||
6359B8954E69147DA50EDB46 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
||||||
722DE8EFC35A88BC68637FB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
|
||||||
7500F0B31797D49B5BBEC5CC /* String+ArrayConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ArrayConversion.swift"; sourceTree = "<group>"; };
|
|
||||||
7E66B3AD4BB22D2AB5676E10 /* PCAPhobos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PCAPhobos.entitlements; sourceTree = "<group>"; };
|
|
||||||
8E36BA501FDCB2034F09415A /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
|
||||||
98DC1765DEC5F326E2B5F9C4 /* PhobosProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhobosProfile.swift; sourceTree = "<group>"; };
|
|
||||||
9FC04650634F03E6DB09AB6B /* logging.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = logging.c; sourceTree = "<group>"; };
|
|
||||||
A81F96BF4131FDBB816623BE /* PhobosTunnel.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = PhobosTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
B3E470E13EF603CB26991683 /* PCAPhobosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PCAPhobosApp.swift; sourceTree = "<group>"; };
|
|
||||||
CA6480536DA633DB94B6930F /* WireGuardKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = WireGuardKit; path = WireGuardKit; sourceTree = SOURCE_ROOT; };
|
|
||||||
CCE5C98CD905BDE16A8D83D4 /* masking.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = masking.c; sourceTree = "<group>"; };
|
|
||||||
EF9115CA0D5EC52C8679A12A /* obfuscation.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = obfuscation.c; sourceTree = "<group>"; };
|
|
||||||
FF14952A4263FF0E004A3F04 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
D366EA9C7C0EC88CAE5EA47E /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
08C4842C3185E04B2154C971 /* WireGuardKit in Frameworks */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
1EBBB94A186F7080FE3609C8 /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
027878C5E0654DB27FAE88A3 /* PCAPhobos.app */,
|
|
||||||
A81F96BF4131FDBB816623BE /* PhobosTunnel.appex */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
2500D202DC9E96E7BBCE29B1 /* Tunnel */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
8E36BA501FDCB2034F09415A /* PacketTunnelProvider.swift */,
|
|
||||||
211950F8776D3D14D7B9C49A /* pcarelay.c */,
|
|
||||||
2572D9A6F822B038D0652619 /* ru_bypass.txt */,
|
|
||||||
7500F0B31797D49B5BBEC5CC /* String+ArrayConversion.swift */,
|
|
||||||
2C1AA6DED9B45949864A9B7D /* TunnelConfiguration+WgQuickConfig.swift */,
|
|
||||||
);
|
|
||||||
path = Tunnel;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
527268DF962A8B7EC120C536 /* obfuscator */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
0E5C1673464AAC3EF81DD2FE /* config.c */,
|
|
||||||
9FC04650634F03E6DB09AB6B /* logging.c */,
|
|
||||||
05DD44245829A52D32B42AF8 /* masking_stun.c */,
|
|
||||||
CCE5C98CD905BDE16A8D83D4 /* masking.c */,
|
|
||||||
EF9115CA0D5EC52C8679A12A /* obfuscation.c */,
|
|
||||||
1FADBB2A65AF77D4ECCAFE78 /* wg-obfuscator.c */,
|
|
||||||
);
|
|
||||||
name = obfuscator;
|
|
||||||
path = Tunnel/obfuscator;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D4CB33FA621F53C8F82ED115 /* Packages */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
CA6480536DA633DB94B6930F /* WireGuardKit */,
|
|
||||||
);
|
|
||||||
name = Packages;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
DD1FD95C0B2DE48623872799 /* App */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
6359B8954E69147DA50EDB46 /* Assets.xcassets */,
|
|
||||||
FF14952A4263FF0E004A3F04 /* ContentView.swift */,
|
|
||||||
722DE8EFC35A88BC68637FB4 /* Info.plist */,
|
|
||||||
7E66B3AD4BB22D2AB5676E10 /* PCAPhobos.entitlements */,
|
|
||||||
B3E470E13EF603CB26991683 /* PCAPhobosApp.swift */,
|
|
||||||
);
|
|
||||||
path = App;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
E31449B836D353F74991D905 = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
DD1FD95C0B2DE48623872799 /* App */,
|
|
||||||
527268DF962A8B7EC120C536 /* obfuscator */,
|
|
||||||
D4CB33FA621F53C8F82ED115 /* Packages */,
|
|
||||||
F6FC482145D1EFAD5A15F63B /* Shared */,
|
|
||||||
2500D202DC9E96E7BBCE29B1 /* Tunnel */,
|
|
||||||
1EBBB94A186F7080FE3609C8 /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
F6FC482145D1EFAD5A15F63B /* Shared */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
98DC1765DEC5F326E2B5F9C4 /* PhobosProfile.swift */,
|
|
||||||
);
|
|
||||||
path = Shared;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
0FD066BC2C51A1E092914FE8 /* PhobosTunnel */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 329F6A5A13B95B1525F94841 /* Build configuration list for PBXNativeTarget "PhobosTunnel" */;
|
|
||||||
buildPhases = (
|
|
||||||
173BA4C39EE769A34EBB19CB /* Sources */,
|
|
||||||
A5352C1E8C48FFB2CCEC38F3 /* Resources */,
|
|
||||||
D366EA9C7C0EC88CAE5EA47E /* Frameworks */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
name = PhobosTunnel;
|
|
||||||
packageProductDependencies = (
|
|
||||||
FA829997FAB330F6EFA104C9 /* WireGuardKit */,
|
|
||||||
);
|
|
||||||
productName = PhobosTunnel;
|
|
||||||
productReference = A81F96BF4131FDBB816623BE /* PhobosTunnel.appex */;
|
|
||||||
productType = "com.apple.product-type.app-extension";
|
|
||||||
};
|
|
||||||
E1F7E69029753CF102C6CE2E /* PCAPhobos */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 61EF055D39FD21A29FA039D5 /* Build configuration list for PBXNativeTarget "PCAPhobos" */;
|
|
||||||
buildPhases = (
|
|
||||||
F83DEE91B85335727CA6B39E /* Sources */,
|
|
||||||
D18596DD6D613F6BC9647644 /* Resources */,
|
|
||||||
9FC25420ED5EF973CE140584 /* Embed Foundation Extensions */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
0B334C04E5AD898D8736FFAA /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
name = PCAPhobos;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = PCAPhobos;
|
|
||||||
productReference = 027878C5E0654DB27FAE88A3 /* PCAPhobos.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
E59D1112D2B128DF6FE7A1E9 /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
BuildIndependentTargetsInParallel = YES;
|
|
||||||
LastUpgradeCheck = 1430;
|
|
||||||
TargetAttributes = {
|
|
||||||
0FD066BC2C51A1E092914FE8 = {
|
|
||||||
DevelopmentTeam = "";
|
|
||||||
ProvisioningStyle = Automatic;
|
|
||||||
};
|
|
||||||
E1F7E69029753CF102C6CE2E = {
|
|
||||||
DevelopmentTeam = "";
|
|
||||||
ProvisioningStyle = Automatic;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = F73680B923D76C65D03B442C /* Build configuration list for PBXProject "PCAPhobos" */;
|
|
||||||
developmentRegion = en;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
Base,
|
|
||||||
en,
|
|
||||||
);
|
|
||||||
mainGroup = E31449B836D353F74991D905;
|
|
||||||
minimizedProjectReferenceProxies = 1;
|
|
||||||
packageReferences = (
|
|
||||||
9F8EC62EA6724F5FA6B8BAE0 /* XCLocalSwiftPackageReference "WireGuardKit" */,
|
|
||||||
);
|
|
||||||
preferredProjectObjectVersion = 77;
|
|
||||||
productRefGroup = 1EBBB94A186F7080FE3609C8 /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
E1F7E69029753CF102C6CE2E /* PCAPhobos */,
|
|
||||||
0FD066BC2C51A1E092914FE8 /* PhobosTunnel */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
A5352C1E8C48FFB2CCEC38F3 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
5F5D376513C0B27C45A3E7B6 /* ru_bypass.txt in Resources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
D18596DD6D613F6BC9647644 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
D8CCC1E6CC8164EDD4E34D25 /* Assets.xcassets in Resources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
173BA4C39EE769A34EBB19CB /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
99F5DB52833E3B5505EEA727 /* PacketTunnelProvider.swift in Sources */,
|
|
||||||
7D8AB66421409CBFCCF2769A /* PhobosProfile.swift in Sources */,
|
|
||||||
4E4255B9E0F8626F6E209B0F /* String+ArrayConversion.swift in Sources */,
|
|
||||||
11F2A6A8D281AB36B45D4603 /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
|
|
||||||
78B74F81E0042D9AD94B7A6B /* config.c in Sources */,
|
|
||||||
1C05D29ACA95548374AD6FD9 /* logging.c in Sources */,
|
|
||||||
56F35C68C6A387E499703CFF /* masking.c in Sources */,
|
|
||||||
E88105F9325A93041D617213 /* masking_stun.c in Sources */,
|
|
||||||
B3BB8BE3409D63FEB59B5449 /* obfuscation.c in Sources */,
|
|
||||||
DF6D62C526E908CA5CDDFCAE /* pcarelay.c in Sources */,
|
|
||||||
38870650A7C2B1F0927A1230 /* wg-obfuscator.c in Sources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
F83DEE91B85335727CA6B39E /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
008DA398D56DFBDB16D97CE1 /* ContentView.swift in Sources */,
|
|
||||||
C3C41D3C419E8D17F20A38B5 /* PCAPhobosApp.swift in Sources */,
|
|
||||||
CBC338B91B5FCE1E207B36AC /* PhobosProfile.swift in Sources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
|
||||||
0B334C04E5AD898D8736FFAA /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 0FD066BC2C51A1E092914FE8 /* PhobosTunnel */;
|
|
||||||
targetProxy = B9875EF5DEB6362919B43F22 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
/* End PBXTargetDependency section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
35473CF1EA2AAAC350F346D0 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = App/PCAPhobos.entitlements;
|
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.bobyrev.pcaphobos;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
62739003B63EE4EE3B3FF1FB /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGN_ENTITLEMENTS = Tunnel/PhobosTunnel.entitlements;
|
|
||||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
|
|
||||||
HEADER_SEARCH_PATHS = (
|
|
||||||
"$(PROJECT_DIR)/Tunnel",
|
|
||||||
"$(PROJECT_DIR)/Tunnel/obfuscator",
|
|
||||||
);
|
|
||||||
INFOPLIST_FILE = Tunnel/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
|
||||||
"$(PROJECT_DIR)/WireGuardGo/iphoneos",
|
|
||||||
);
|
|
||||||
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = (
|
|
||||||
"$(PROJECT_DIR)/WireGuardGo/iphonesimulator",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.bobyrev.pcaphobos.tunnel;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Tunnel/Tunnel-Bridging-Header.h";
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
7ABA8220043DEF78154F9F38 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
CURRENT_PROJECT_VERSION = 141;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
DEVELOPMENT_TEAM = "";
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
|
||||||
MARKETING_VERSION = 1.4.1;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
B5D2F9B70DD37D3A57737EED /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
CURRENT_PROJECT_VERSION = 141;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
DEVELOPMENT_TEAM = "";
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"DEBUG=1",
|
|
||||||
);
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
|
||||||
MARKETING_VERSION = 1.4.1;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
D0AEA32C3C7BFB88918AB206 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGN_ENTITLEMENTS = Tunnel/PhobosTunnel.entitlements;
|
|
||||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
|
|
||||||
HEADER_SEARCH_PATHS = (
|
|
||||||
"$(PROJECT_DIR)/Tunnel",
|
|
||||||
"$(PROJECT_DIR)/Tunnel/obfuscator",
|
|
||||||
);
|
|
||||||
INFOPLIST_FILE = Tunnel/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
|
||||||
"$(PROJECT_DIR)/WireGuardGo/iphoneos",
|
|
||||||
);
|
|
||||||
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = (
|
|
||||||
"$(PROJECT_DIR)/WireGuardGo/iphonesimulator",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.bobyrev.pcaphobos.tunnel;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Tunnel/Tunnel-Bridging-Header.h";
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
D42BF07983FD43EBB6D84762 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = App/PCAPhobos.entitlements;
|
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.bobyrev.pcaphobos;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
329F6A5A13B95B1525F94841 /* Build configuration list for PBXNativeTarget "PhobosTunnel" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
D0AEA32C3C7BFB88918AB206 /* Debug */,
|
|
||||||
62739003B63EE4EE3B3FF1FB /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Debug;
|
|
||||||
};
|
|
||||||
61EF055D39FD21A29FA039D5 /* Build configuration list for PBXNativeTarget "PCAPhobos" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
D42BF07983FD43EBB6D84762 /* Debug */,
|
|
||||||
35473CF1EA2AAAC350F346D0 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Debug;
|
|
||||||
};
|
|
||||||
F73680B923D76C65D03B442C /* Build configuration list for PBXProject "PCAPhobos" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
B5D2F9B70DD37D3A57737EED /* Debug */,
|
|
||||||
7ABA8220043DEF78154F9F38 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Debug;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
|
||||||
9F8EC62EA6724F5FA6B8BAE0 /* XCLocalSwiftPackageReference "WireGuardKit" */ = {
|
|
||||||
isa = XCLocalSwiftPackageReference;
|
|
||||||
relativePath = WireGuardKit;
|
|
||||||
};
|
|
||||||
/* End XCLocalSwiftPackageReference section */
|
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
|
||||||
FA829997FAB330F6EFA104C9 /* WireGuardKit */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = WireGuardKit;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
|
||||||
};
|
|
||||||
rootObject = E59D1112D2B128DF6FE7A1E9 /* Project object */;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# PCA Phobos VPN — iPhone
|
|
||||||
|
|
||||||
То же, что Android-клиент: импорт `phobos://` из панели, обфускация (wg-obfuscator внутри
|
|
||||||
расширения), **российские сайты напрямую**, канал **Авто/UDP/TCP** (при блокировке UDP — сам на TCP).
|
|
||||||
Выбор отдельных приложений на iOS невозможен (Apple даёт это только через MDM) — делим по адресам.
|
|
||||||
|
|
||||||
Устройство:
|
|
||||||
- `App/` — SwiftUI-приложение (профиль, кнопка, настройки), `Shared/PhobosProfile.swift` — разбор ссылки.
|
|
||||||
- `Tunnel/` — Packet Tunnel: WireGuardKit → 127.0.0.1:13255 wg-obfuscator (`-Dmain=wgobf_main`, один
|
|
||||||
экземпляр: iOS запрещает fork) → 127.0.0.1:13258 `pcarelay.c` → сервер по UDP или TCP (формат udptcp).
|
|
||||||
- `WireGuardKit/` — wireguard-apple (MIT), `WireGuardGo/<sdk>/libwg-go.a` — собран `make` (iphoneos) и
|
|
||||||
`go build -buildmode c-archive` с `-target arm64-apple-ios15.0-simulator` (симулятор).
|
|
||||||
|
|
||||||
Сборка: `xcodegen generate` → `PCAPhobos.xcodeproj`. Проверка без телефона: `Tests/ProfileCheck`.
|
|
||||||
|
|
||||||
## Установка на iPhone (нужен ПЛАТНЫЙ Apple Developer, $99/год)
|
|
||||||
Network Extension (packet-tunnel-provider) Apple выдаёт только платным аккаунтам — с бесплатным
|
|
||||||
Xcode не подпишет даже одно приложение. Когда аккаунт есть:
|
|
||||||
1. В `project.yml` вписать `DEVELOPMENT_TEAM: <Team ID>`, `xcodegen generate`.
|
|
||||||
2. Xcode → Signing: включить capability Network Extensions (Packet Tunnel) для обоих таргетов.
|
|
||||||
3. iPhone по кабелю → Run, либо Product → Archive → TestFlight (тестировщики по e-mail, 90 дней).
|
|
||||||
4. В панели клиента → 📱 → открыть ссылку `phobos://` на iPhone → «Открыть в PCA Phobos» → Подключить
|
|
||||||
→ разрешить добавление VPN.
|
|
||||||
|
|
||||||
Проверено 23.10.2026: сборка под iphoneos и симулятор, UI и импорт реальной ссылки в симуляторе.
|
|
||||||
Сам туннель в симуляторе iOS не работает — проверяется только на телефоне.
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
/// Parsed phobos:// payload from the PCA Phobos panel:
|
|
||||||
/// [Interface] + [Peer] (WireGuard) + [instance] (wg-obfuscator) + "# pca-tcp = <port>".
|
|
||||||
struct PhobosProfile {
|
|
||||||
let raw: String
|
|
||||||
|
|
||||||
init(_ raw: String) { self.raw = raw }
|
|
||||||
|
|
||||||
/// phobos://<urlsafe base64>#name
|
|
||||||
static func fromLink(_ link: String) -> (profile: PhobosProfile, name: String)? {
|
|
||||||
var s = link.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard s.hasPrefix("phobos://") else { return nil }
|
|
||||||
s.removeFirst("phobos://".count)
|
|
||||||
var name = "Phobos"
|
|
||||||
if let hash = s.firstIndex(of: "#") {
|
|
||||||
name = String(s[s.index(after: hash)...]).removingPercentEncoding ?? name
|
|
||||||
s = String(s[..<hash])
|
|
||||||
}
|
|
||||||
var b64 = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
|
||||||
while b64.count % 4 != 0 { b64 += "=" }
|
|
||||||
guard let data = Data(base64Encoded: b64), let text = String(data: data, encoding: .utf8),
|
|
||||||
text.contains("[Interface]"), text.contains("[instance]") else { return nil }
|
|
||||||
return (PhobosProfile(text), name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lines of a section, without "= none" placeholders and comments.
|
|
||||||
func lines(_ header: String) -> [(key: String, line: String)] {
|
|
||||||
var inSection = false
|
|
||||||
var out: [(String, String)] = []
|
|
||||||
for rawLine in raw.split(separator: "\n") {
|
|
||||||
let line = rawLine.trimmingCharacters(in: .whitespaces)
|
|
||||||
if line.hasPrefix("[") && line.hasSuffix("]") { inSection = line.lowercased() == header.lowercased(); continue }
|
|
||||||
guard inSection, !line.hasPrefix("#"), let eq = line.firstIndex(of: "=") else { continue }
|
|
||||||
let value = line[line.index(after: eq)...].trimmingCharacters(in: .whitespaces)
|
|
||||||
if value.lowercased() == "none" { continue }
|
|
||||||
out.append((line[..<eq].trimmingCharacters(in: .whitespaces).lowercased(), line))
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
private func value(_ header: String, _ key: String) -> String? {
|
|
||||||
lines(header).first { $0.key == key }.map { line in
|
|
||||||
line.line[line.line.index(after: line.line.firstIndex(of: "=")!)...].trimmingCharacters(in: .whitespaces)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var target: String? { value("[instance]", "target") }
|
|
||||||
var targetHost: String? { target.map { String($0[..<($0.lastIndex(of: ":") ?? $0.endIndex)]) } }
|
|
||||||
var targetPort: Int? { target.flatMap { $0.split(separator: ":").last.flatMap { Int($0) } } }
|
|
||||||
|
|
||||||
/// "# pca-tcp = 443" written by the panel when the server has the TCP fallback.
|
|
||||||
var tcpPort: Int? {
|
|
||||||
for rawLine in raw.split(separator: "\n") {
|
|
||||||
let line = rawLine.trimmingCharacters(in: .whitespaces)
|
|
||||||
if line.hasPrefix("#"), line.contains("pca-tcp"), let eq = line.firstIndex(of: "=") {
|
|
||||||
return Int(line[line.index(after: eq)...].trimmingCharacters(in: .whitespaces))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func obfuscatorConfig(listen: Int, target: String) -> String {
|
|
||||||
var out = "[instance]\nsource-if = 127.0.0.1\nsource-lport = \(listen)\ntarget = \(target)\n"
|
|
||||||
for item in lines("[instance]") where !["source-if", "source-lport", "target", "verbose"].contains(item.key) {
|
|
||||||
out += item.line + "\n"
|
|
||||||
}
|
|
||||||
return out + "verbose = INFO\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
func wireGuardConfig(endpoint: String, allowedIPs: String?) -> String {
|
|
||||||
var out = "[Interface]\n"
|
|
||||||
for item in lines("[Interface]") where !item.key.hasSuffix("applications") { out += item.line + "\n" }
|
|
||||||
out += "\n[Peer]\n"
|
|
||||||
for item in lines("[Peer]") {
|
|
||||||
switch item.key {
|
|
||||||
case "endpoint": out += "Endpoint = \(endpoint)\n"
|
|
||||||
case "allowedips" where allowedIPs != nil: out += "AllowedIPs = \(allowedIPs!)\n"
|
|
||||||
default: out += item.line + "\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Self-check of phobos:// parsing (no device needed):
|
|
||||||
// cd ios/Tests/ProfileCheck && xcrun swiftc -o /tmp/pc ../../Shared/PhobosProfile.swift main.swift && /tmp/pc
|
|
||||||
import Foundation
|
|
||||||
let conf = """
|
|
||||||
[Interface]
|
|
||||||
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
|
||||||
Address = 10.25.0.9/32
|
|
||||||
DNS = 1.1.1.1
|
|
||||||
MTU = 1420
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
|
|
||||||
PresharedKey = none
|
|
||||||
Endpoint = 127.0.0.1:51820
|
|
||||||
AllowedIPs = 0.0.0.0/0, ::/0
|
|
||||||
PersistentKeepalive = 25
|
|
||||||
|
|
||||||
[instance]
|
|
||||||
source-if = 127.0.0.1
|
|
||||||
source-lport = 51820
|
|
||||||
target = 203.0.113.10:2083
|
|
||||||
key = testkey
|
|
||||||
masking = STUN
|
|
||||||
verbose = INFO
|
|
||||||
idle-timeout = 300
|
|
||||||
max-dummy = 4
|
|
||||||
# pca-tcp = 443
|
|
||||||
"""
|
|
||||||
let link = "phobos://" + Data(conf.utf8).base64EncodedString()
|
|
||||||
.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_")
|
|
||||||
.replacingOccurrences(of: "=", with: "") + "#demo"
|
|
||||||
let (p, name) = PhobosProfile.fromLink(link)!
|
|
||||||
precondition(name == "demo" && p.targetHost == "203.0.113.10" && p.targetPort == 2083 && p.tcpPort == 443)
|
|
||||||
let obf = p.obfuscatorConfig(listen: 13255, target: "127.0.0.1:13258")
|
|
||||||
precondition(obf.contains("target = 127.0.0.1:13258") && obf.contains("key = testkey") && !obf.contains("203.0.113.10"))
|
|
||||||
let wg = p.wireGuardConfig(endpoint: "127.0.0.1:13255", allowedIPs: "1.0.0.0/8")
|
|
||||||
precondition(wg.contains("Endpoint = 127.0.0.1:13255") && wg.contains("AllowedIPs = 1.0.0.0/8") && !wg.contains("= none"))
|
|
||||||
print("profile_check ok")
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?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>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>PhobosTunnel</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>XPC!</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>NSExtension</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSExtensionPointIdentifier</key>
|
|
||||||
<string>com.apple.networkextension.packet-tunnel</string>
|
|
||||||
<key>NSExtensionPrincipalClass</key>
|
|
||||||
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import NetworkExtension
|
|
||||||
import WireGuardKit
|
|
||||||
|
|
||||||
/// PCA Phobos packet tunnel:
|
|
||||||
/// WireGuard (userspace) -> 127.0.0.1:13255 wg-obfuscator -> 127.0.0.1:13258 pcarelay -> server (UDP or TCP).
|
|
||||||
/// providerConfiguration: "conf" (phobos:// payload), "ruDirect" (Bool), "transport" ("auto"|"udp"|"tcp").
|
|
||||||
final class PacketTunnelProvider: NEPacketTunnelProvider {
|
|
||||||
private static let wgPort = 13255, relayPort = 13258
|
|
||||||
private static var obfuscatorStarted = false // one wg-obfuscator per process (it forks for more)
|
|
||||||
|
|
||||||
private lazy var adapter = WireGuardAdapter(with: self) { _, message in NSLog("wg: %@", message) }
|
|
||||||
|
|
||||||
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
|
||||||
let pc = (protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration ?? [:]
|
|
||||||
guard let conf = pc["conf"] as? String, !conf.isEmpty else {
|
|
||||||
completionHandler(NSError(domain: "PCAPhobos", code: 1, userInfo: [NSLocalizedDescriptionKey: "нет профиля"]))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let ruDirect = pc["ruDirect"] as? Bool ?? true
|
|
||||||
let transport = pc["transport"] as? String ?? "auto"
|
|
||||||
let profile = PhobosProfile(conf)
|
|
||||||
|
|
||||||
guard let host = profile.targetHost, let udpPort = profile.targetPort else {
|
|
||||||
completionHandler(NSError(domain: "PCAPhobos", code: 2, userInfo: [NSLocalizedDescriptionKey: "в профиле нет адреса сервера"]))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let tcpPort = profile.tcpPort ?? 0
|
|
||||||
if transport == "tcp" && tcpPort == 0 {
|
|
||||||
completionHandler(NSError(domain: "PCAPhobos", code: 3, userInfo: [NSLocalizedDescriptionKey: "на сервере нет TCP-канала"]))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pcarelay_use_tcp(transport == "tcp" ? 1 : 0)
|
|
||||||
if !Self.obfuscatorStarted {
|
|
||||||
Self.obfuscatorStarted = true
|
|
||||||
pcarelay_start(Int32(Self.relayPort), host, Int32(udpPort), Int32(tcpPort))
|
|
||||||
let path = NSTemporaryDirectory() + "obf.conf"
|
|
||||||
try? profile.obfuscatorConfig(listen: Self.wgPort, target: "127.0.0.1:\(Self.relayPort)")
|
|
||||||
.write(toFile: path, atomically: true, encoding: .utf8)
|
|
||||||
Thread.detachNewThread {
|
|
||||||
var args: [UnsafeMutablePointer<CChar>?] = ["wg-obfuscator", "-c", path].map { strdup($0) }
|
|
||||||
_ = wgobf_main(Int32(args.count), &args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let allowed = ruDirect ? Self.ruBypassAllowedIPs() : nil
|
|
||||||
let wgText = profile.wireGuardConfig(endpoint: "127.0.0.1:\(Self.wgPort)", allowedIPs: allowed)
|
|
||||||
let tunnelConfig: TunnelConfiguration
|
|
||||||
do {
|
|
||||||
tunnelConfig = try TunnelConfiguration(fromWgQuickConfig: wgText, called: "phobos")
|
|
||||||
} catch {
|
|
||||||
completionHandler(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
adapter.start(tunnelConfiguration: tunnelConfig) { [weak self] error in
|
|
||||||
completionHandler(error)
|
|
||||||
if error == nil && transport == "auto" && tcpPort != 0 { self?.watchUdp() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Auto: nothing came back over UDP within 15 s -> UDP is cut, move the relay to TCP.
|
|
||||||
private func watchUdp() {
|
|
||||||
DispatchQueue.global().asyncAfter(deadline: .now() + 15) {
|
|
||||||
if pcarelay_is_tcp() == 0 && pcarelay_rx_bytes() == 0 {
|
|
||||||
NSLog("PCAPhobos: no UDP answer in 15 s -> TCP")
|
|
||||||
pcarelay_use_tcp(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
|
||||||
adapter.stop { _ in
|
|
||||||
completionHandler()
|
|
||||||
// the obfuscator thread cannot be stopped cleanly; a fresh process starts next time
|
|
||||||
exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// App asks for status: {"transport": "UDP"|"TCP", "rx": bytes}
|
|
||||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
|
|
||||||
let status: [String: Any] = ["transport": pcarelay_is_tcp() != 0 ? "TCP" : "UDP",
|
|
||||||
"rx": pcarelay_rx_bytes()]
|
|
||||||
completionHandler?(try? JSONSerialization.data(withJSONObject: status))
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func ruBypassAllowedIPs() -> String? {
|
|
||||||
guard let url = Bundle.main.url(forResource: "ru_bypass", withExtension: "txt"),
|
|
||||||
let text = try? String(contentsOf: url, encoding: .utf8) else { return nil }
|
|
||||||
return text.split(separator: "\n").first { !$0.hasPrefix("#") && $0.contains("/") }.map(String.init)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?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>com.apple.developer.networking.networkextension</key>
|
|
||||||
<array>
|
|
||||||
<string>packet-tunnel-provider</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension String {
|
|
||||||
|
|
||||||
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
|
|
||||||
return split(separator: separator)
|
|
||||||
.map {
|
|
||||||
if let charSet = trimmingCharacters {
|
|
||||||
return $0.trimmingCharacters(in: charSet)
|
|
||||||
} else {
|
|
||||||
return String($0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Optional where Wrapped == String {
|
|
||||||
|
|
||||||
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
|
|
||||||
switch self {
|
|
||||||
case .none:
|
|
||||||
return []
|
|
||||||
case .some(let wrapped):
|
|
||||||
return wrapped.splitToArray(separator: separator, trimmingCharacters: trimmingCharacters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
#include "pcarelay.h"
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import WireGuardKit
|
|
||||||
|
|
||||||
extension TunnelConfiguration {
|
|
||||||
|
|
||||||
enum ParserState {
|
|
||||||
case inInterfaceSection
|
|
||||||
case inPeerSection
|
|
||||||
case notInASection
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ParseError: Error {
|
|
||||||
case invalidLine(String.SubSequence)
|
|
||||||
case noInterface
|
|
||||||
case multipleInterfaces
|
|
||||||
case interfaceHasNoPrivateKey
|
|
||||||
case interfaceHasInvalidPrivateKey(String)
|
|
||||||
case interfaceHasInvalidListenPort(String)
|
|
||||||
case interfaceHasInvalidAddress(String)
|
|
||||||
case interfaceHasInvalidDNS(String)
|
|
||||||
case interfaceHasInvalidMTU(String)
|
|
||||||
case interfaceHasUnrecognizedKey(String)
|
|
||||||
case peerHasNoPublicKey
|
|
||||||
case peerHasInvalidPublicKey(String)
|
|
||||||
case peerHasInvalidPreSharedKey(String)
|
|
||||||
case peerHasInvalidAllowedIP(String)
|
|
||||||
case peerHasInvalidEndpoint(String)
|
|
||||||
case peerHasInvalidPersistentKeepAlive(String)
|
|
||||||
case peerHasInvalidTransferBytes(String)
|
|
||||||
case peerHasInvalidLastHandshakeTime(String)
|
|
||||||
case peerHasUnrecognizedKey(String)
|
|
||||||
case multiplePeersWithSamePublicKey
|
|
||||||
case multipleEntriesForKey(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(fromWgQuickConfig wgQuickConfig: String, called name: String? = nil) throws {
|
|
||||||
var interfaceConfiguration: InterfaceConfiguration?
|
|
||||||
var peerConfigurations = [PeerConfiguration]()
|
|
||||||
|
|
||||||
let lines = wgQuickConfig.split { $0.isNewline }
|
|
||||||
|
|
||||||
var parserState = ParserState.notInASection
|
|
||||||
var attributes = [String: String]()
|
|
||||||
|
|
||||||
for (lineIndex, line) in lines.enumerated() {
|
|
||||||
var trimmedLine: String
|
|
||||||
if let commentRange = line.range(of: "#") {
|
|
||||||
trimmedLine = String(line[..<commentRange.lowerBound])
|
|
||||||
} else {
|
|
||||||
trimmedLine = String(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
let lowercasedLine = trimmedLine.lowercased()
|
|
||||||
|
|
||||||
if !trimmedLine.isEmpty {
|
|
||||||
if let equalsIndex = trimmedLine.firstIndex(of: "=") {
|
|
||||||
// Line contains an attribute
|
|
||||||
let keyWithCase = trimmedLine[..<equalsIndex].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
let key = keyWithCase.lowercased()
|
|
||||||
let value = trimmedLine[trimmedLine.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
|
|
||||||
if let presentValue = attributes[key] {
|
|
||||||
if keysWithMultipleEntriesAllowed.contains(key) {
|
|
||||||
attributes[key] = presentValue + "," + value
|
|
||||||
} else {
|
|
||||||
throw ParseError.multipleEntriesForKey(keyWithCase)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
attributes[key] = value
|
|
||||||
}
|
|
||||||
let interfaceSectionKeys: Set<String> = ["privatekey", "listenport", "address", "dns", "mtu"]
|
|
||||||
let peerSectionKeys: Set<String> = ["publickey", "presharedkey", "allowedips", "endpoint", "persistentkeepalive"]
|
|
||||||
if parserState == .inInterfaceSection {
|
|
||||||
guard interfaceSectionKeys.contains(key) else {
|
|
||||||
throw ParseError.interfaceHasUnrecognizedKey(keyWithCase)
|
|
||||||
}
|
|
||||||
} else if parserState == .inPeerSection {
|
|
||||||
guard peerSectionKeys.contains(key) else {
|
|
||||||
throw ParseError.peerHasUnrecognizedKey(keyWithCase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if lowercasedLine != "[interface]" && lowercasedLine != "[peer]" {
|
|
||||||
throw ParseError.invalidLine(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let isLastLine = lineIndex == lines.count - 1
|
|
||||||
|
|
||||||
if isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]" {
|
|
||||||
// Previous section has ended; process the attributes collected so far
|
|
||||||
if parserState == .inInterfaceSection {
|
|
||||||
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
|
|
||||||
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
|
|
||||||
interfaceConfiguration = interface
|
|
||||||
} else if parserState == .inPeerSection {
|
|
||||||
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
|
|
||||||
peerConfigurations.append(peer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lowercasedLine == "[interface]" {
|
|
||||||
parserState = .inInterfaceSection
|
|
||||||
attributes.removeAll()
|
|
||||||
} else if lowercasedLine == "[peer]" {
|
|
||||||
parserState = .inPeerSection
|
|
||||||
attributes.removeAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
|
||||||
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
|
|
||||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
|
||||||
throw ParseError.multiplePeersWithSamePublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if let interfaceConfiguration = interfaceConfiguration {
|
|
||||||
self.init(name: name, interface: interfaceConfiguration, peers: peerConfigurations)
|
|
||||||
} else {
|
|
||||||
throw ParseError.noInterface
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func asWgQuickConfig() -> String {
|
|
||||||
var output = "[Interface]\n"
|
|
||||||
output.append("PrivateKey = \(interface.privateKey.base64Key)\n")
|
|
||||||
if let listenPort = interface.listenPort {
|
|
||||||
output.append("ListenPort = \(listenPort)\n")
|
|
||||||
}
|
|
||||||
if !interface.addresses.isEmpty {
|
|
||||||
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
|
||||||
output.append("Address = \(addressString)\n")
|
|
||||||
}
|
|
||||||
if !interface.dns.isEmpty || !interface.dnsSearch.isEmpty {
|
|
||||||
var dnsLine = interface.dns.map { $0.stringRepresentation }
|
|
||||||
dnsLine.append(contentsOf: interface.dnsSearch)
|
|
||||||
let dnsString = dnsLine.joined(separator: ", ")
|
|
||||||
output.append("DNS = \(dnsString)\n")
|
|
||||||
}
|
|
||||||
if let mtu = interface.mtu {
|
|
||||||
output.append("MTU = \(mtu)\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
for peer in peers {
|
|
||||||
output.append("\n[Peer]\n")
|
|
||||||
output.append("PublicKey = \(peer.publicKey.base64Key)\n")
|
|
||||||
if let preSharedKey = peer.preSharedKey?.base64Key {
|
|
||||||
output.append("PresharedKey = \(preSharedKey)\n")
|
|
||||||
}
|
|
||||||
if !peer.allowedIPs.isEmpty {
|
|
||||||
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
|
|
||||||
output.append("AllowedIPs = \(allowedIPsString)\n")
|
|
||||||
}
|
|
||||||
if let endpoint = peer.endpoint {
|
|
||||||
output.append("Endpoint = \(endpoint.stringRepresentation)\n")
|
|
||||||
}
|
|
||||||
if let persistentKeepAlive = peer.persistentKeepAlive {
|
|
||||||
output.append("PersistentKeepalive = \(persistentKeepAlive)\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
|
|
||||||
guard let privateKeyString = attributes["privatekey"] else {
|
|
||||||
throw ParseError.interfaceHasNoPrivateKey
|
|
||||||
}
|
|
||||||
guard let privateKey = PrivateKey(base64Key: privateKeyString) else {
|
|
||||||
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
|
||||||
}
|
|
||||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
|
||||||
if let listenPortString = attributes["listenport"] {
|
|
||||||
guard let listenPort = UInt16(listenPortString) else {
|
|
||||||
throw ParseError.interfaceHasInvalidListenPort(listenPortString)
|
|
||||||
}
|
|
||||||
interface.listenPort = listenPort
|
|
||||||
}
|
|
||||||
if let addressesString = attributes["address"] {
|
|
||||||
var addresses = [IPAddressRange]()
|
|
||||||
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
|
||||||
guard let address = IPAddressRange(from: addressString) else {
|
|
||||||
throw ParseError.interfaceHasInvalidAddress(addressString)
|
|
||||||
}
|
|
||||||
addresses.append(address)
|
|
||||||
}
|
|
||||||
interface.addresses = addresses
|
|
||||||
}
|
|
||||||
if let dnsString = attributes["dns"] {
|
|
||||||
var dnsServers = [DNSServer]()
|
|
||||||
var dnsSearch = [String]()
|
|
||||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
|
||||||
if let dnsServer = DNSServer(from: dnsServerString) {
|
|
||||||
dnsServers.append(dnsServer)
|
|
||||||
} else {
|
|
||||||
dnsSearch.append(dnsServerString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interface.dns = dnsServers
|
|
||||||
interface.dnsSearch = dnsSearch
|
|
||||||
}
|
|
||||||
if let mtuString = attributes["mtu"] {
|
|
||||||
guard let mtu = UInt16(mtuString) else {
|
|
||||||
throw ParseError.interfaceHasInvalidMTU(mtuString)
|
|
||||||
}
|
|
||||||
interface.mtu = mtu
|
|
||||||
}
|
|
||||||
return interface
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
|
|
||||||
guard let publicKeyString = attributes["publickey"] else {
|
|
||||||
throw ParseError.peerHasNoPublicKey
|
|
||||||
}
|
|
||||||
guard let publicKey = PublicKey(base64Key: publicKeyString) else {
|
|
||||||
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
|
||||||
}
|
|
||||||
var peer = PeerConfiguration(publicKey: publicKey)
|
|
||||||
if let preSharedKeyString = attributes["presharedkey"] {
|
|
||||||
guard let preSharedKey = PreSharedKey(base64Key: preSharedKeyString) else {
|
|
||||||
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
|
||||||
}
|
|
||||||
peer.preSharedKey = preSharedKey
|
|
||||||
}
|
|
||||||
if let allowedIPsString = attributes["allowedips"] {
|
|
||||||
var allowedIPs = [IPAddressRange]()
|
|
||||||
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
|
||||||
guard let allowedIP = IPAddressRange(from: allowedIPString) else {
|
|
||||||
throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
|
|
||||||
}
|
|
||||||
allowedIPs.append(allowedIP)
|
|
||||||
}
|
|
||||||
peer.allowedIPs = allowedIPs
|
|
||||||
}
|
|
||||||
if let endpointString = attributes["endpoint"] {
|
|
||||||
guard let endpoint = Endpoint(from: endpointString) else {
|
|
||||||
throw ParseError.peerHasInvalidEndpoint(endpointString)
|
|
||||||
}
|
|
||||||
peer.endpoint = endpoint
|
|
||||||
}
|
|
||||||
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
|
|
||||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
|
|
||||||
throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
|
|
||||||
}
|
|
||||||
peer.persistentKeepAlive = persistentKeepAlive
|
|
||||||
}
|
|
||||||
return peer
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,535 +0,0 @@
|
|||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <ctype.h>
|
|
||||||
#include "config.h"
|
|
||||||
#include "wg-obfuscator.h"
|
|
||||||
#include "mini_argp.h"
|
|
||||||
#include "masking.h"
|
|
||||||
|
|
||||||
// Executable name
|
|
||||||
static const char *arg0;
|
|
||||||
|
|
||||||
/* The options we understand. */
|
|
||||||
static const mini_argp_opt options[] = {
|
|
||||||
{ "help", '?', 0 },
|
|
||||||
{ "config", 'c', 1 },
|
|
||||||
{ "source-if", 'i', 1 },
|
|
||||||
{ "source", 's', 1 },
|
|
||||||
{ "source-lport", 'p', 1 },
|
|
||||||
{ "target-if", 'o', 1 },
|
|
||||||
{ "target", 't', 1 },
|
|
||||||
{ "target-lport", 'r', 1 },
|
|
||||||
{ "key", 'k', 1 },
|
|
||||||
{ "masking", 'a', 1 },
|
|
||||||
{ "static-bindings", 'b', 1 },
|
|
||||||
{ "max-clients" , 'm', 1 },
|
|
||||||
{ "idle-timeout", 'l', 1 },
|
|
||||||
{ "in-timeout", 'n', 1 },
|
|
||||||
{ "max-dummy", 'd', 1 },
|
|
||||||
{ "fwmark", 'f', 1 },
|
|
||||||
{ "allow-clean", 'e', 0 },
|
|
||||||
{ "verbose", 'v', 1 },
|
|
||||||
{ "log-file", 'L', 1 },
|
|
||||||
{ "log-timestamps", 'T', 1 },
|
|
||||||
{ "resolve-interval", 'R', 1 },
|
|
||||||
{ 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
static void show_usage(void)
|
|
||||||
{
|
|
||||||
printf("Usage: %s [options]\n%s", arg0,
|
|
||||||
" -?, --help Give this help list\n"
|
|
||||||
"\n"
|
|
||||||
"Main settings:\n"
|
|
||||||
" -c, --config=<config_file> Read configuration from file\n"
|
|
||||||
" (can be used instead of the rest arguments)\n"
|
|
||||||
" -i, --source-if=<ip> Source interface to listen on\n"
|
|
||||||
" (optional, default - 0.0.0.0, e.g. all)\n"
|
|
||||||
" -p, --source-lport=<port> Source port to listen\n"
|
|
||||||
" -t, --target=<ip>:<port> Target IP and port\n"
|
|
||||||
" -k, --key=<key> Obfuscation key \n"
|
|
||||||
" (required, must be 1-255 characters long)\n"
|
|
||||||
" -a, --masking=<type> Masking type (optional, default - AUTO)\n"
|
|
||||||
" Supported values: STUN, AUTO, NONE\n"
|
|
||||||
" -b, --static-bindings=<ip>:<port>:<port>,...\n"
|
|
||||||
" Comma-separated static bindings for two-way mode\n"
|
|
||||||
" as <client_ip>:<client_port>:<forward_port>\n"
|
|
||||||
" You can also repeat this option.\n"
|
|
||||||
" -f, --fwmark=<mark> Firewall mark to set on all packets\n"
|
|
||||||
" (optional, default - 0, e.g. disabled)\n"
|
|
||||||
" -v, --verbose=<level> Verbosity level (optional, default - INFO)\n"
|
|
||||||
" ERRORS (critical errors only)\n"
|
|
||||||
" WARNINGS (important messages)\n"
|
|
||||||
" 2 - INFO (informational messages: status messages,\n"
|
|
||||||
" connection established, etc.)\n"
|
|
||||||
" 3 - DEBUG (detailed debug messages)\n"
|
|
||||||
" 4 - TRACE (very detailed debug messages, including\n"
|
|
||||||
" packet dumps)\n"
|
|
||||||
" -L, --log-file=<path> Write the log to this file instead of stderr\n"
|
|
||||||
" (optional, the file is reopened on SIGHUP)\n"
|
|
||||||
" -T, --log-timestamps=<val> Prefix log lines with a timestamp\n"
|
|
||||||
" (optional, default - AUTO, e.g. only when\n"
|
|
||||||
" writing to a log file)\n"
|
|
||||||
" Supported values: AUTO, TRUE, FALSE\n"
|
|
||||||
"\n"
|
|
||||||
"Additional options:\n"
|
|
||||||
" -m, --max-clients=<number> Maximum number of clients (default: 1024)\n"
|
|
||||||
" -l, --idle-timeout=<sec> Idle timeout in seconds (default: 300)\n"
|
|
||||||
" -n, --in-timeout=<sec> Incoming timeout in seconds (default: 0 - disabled)\n"
|
|
||||||
" -d, --max-dummy=<bytes> Maximum length of dummy bytes for data packets\n"
|
|
||||||
" (default: 4)\n"
|
|
||||||
" -e, --allow-clean For servers, allow non-obfuscated incoming connections\n"
|
|
||||||
" -R, --resolve-interval=<sec>\n"
|
|
||||||
" Re-resolve target and static-binding hostnames\n"
|
|
||||||
" every N seconds (default: 0 - disabled).\n"
|
|
||||||
" SIGHUP always triggers a refresh.\n"
|
|
||||||
" If non-zero, a failed resolve at startup is retried\n"
|
|
||||||
" instead of exiting.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
static int parse_opt(const char *lname, char sname, const char *val, void *ctx);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Resets the configuration structure to its default values.
|
|
||||||
*
|
|
||||||
* This function clears the configuration structure by setting all fields to zero
|
|
||||||
* and resetting the verbosity level to the default value.
|
|
||||||
*
|
|
||||||
* @param config Pointer to the obfuscator_config structure to be reset.
|
|
||||||
*/
|
|
||||||
static void reset_config(obfuscator_config_t *config)
|
|
||||||
{
|
|
||||||
if (config->static_bindings) {
|
|
||||||
free(config->static_bindings);
|
|
||||||
}
|
|
||||||
memset(config, 0, sizeof(*config));
|
|
||||||
config->max_clients = MAX_CLIENTS_DEFAULT;
|
|
||||||
config->idle_timeout = IDLE_TIMEOUT_DEFAULT;
|
|
||||||
config->in_timeout = IN_TIMEOUT_DEFAULT;
|
|
||||||
config->max_dummy_length_data = MAX_DUMMY_LENGTH_DATA_DEFAULT;
|
|
||||||
config->log_timestamps = -1; // auto
|
|
||||||
verbose = LL_DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given string represents a valid integer.
|
|
||||||
*
|
|
||||||
* @param str Pointer to the null-terminated string to check.
|
|
||||||
* @return Non-zero value if the string is a valid integer, 0 otherwise.
|
|
||||||
*/
|
|
||||||
static uint8_t is_integer(const char *str)
|
|
||||||
{
|
|
||||||
if (!str || !*str) {
|
|
||||||
return 0; // Empty string is not an integer
|
|
||||||
}
|
|
||||||
while (*str) {
|
|
||||||
if (!isdigit((unsigned char)*str)) {
|
|
||||||
return 0; // Non-digit character found
|
|
||||||
}
|
|
||||||
str++;
|
|
||||||
}
|
|
||||||
return 1; // All characters are digits
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a boolean value from a string.
|
|
||||||
*
|
|
||||||
* @param str Pointer to the null-terminated string to parse.
|
|
||||||
* @return 1 for true values ("true", "yes", "on", "1"),
|
|
||||||
* 0 for false values ("false", "no", "off", "0"),
|
|
||||||
* -1 if the string is not a valid boolean value.
|
|
||||||
*/
|
|
||||||
static int parse_bool(const char *str)
|
|
||||||
{
|
|
||||||
char lower[16];
|
|
||||||
if (!str || !*str || strlen(str) >= sizeof(lower)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
strcpy(lower, str);
|
|
||||||
for (char *p = lower; *p; ++p) *p = tolower((unsigned char)*p);
|
|
||||||
if (!strcmp(lower, "true") || !strcmp(lower, "yes") || !strcmp(lower, "on") || !strcmp(lower, "1")) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!strcmp(lower, "false") || !strcmp(lower, "no") || !strcmp(lower, "off") || !strcmp(lower, "0")) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Reads and processes the configuration file.
|
|
||||||
*
|
|
||||||
* This function opens the specified configuration file and parses its contents
|
|
||||||
* to initialize or update the application's configuration settings.
|
|
||||||
*
|
|
||||||
* @param filename The path to the configuration file to be read.
|
|
||||||
* @param config Pointer to the obfuscator_config structure where the parsed settings will be stored.
|
|
||||||
*/
|
|
||||||
static void read_config_file(const char *filename, obfuscator_config_t *config)
|
|
||||||
{
|
|
||||||
// Read configuration from the file
|
|
||||||
uint8_t first_section = 1; // Flag to indicate if this is the first section being processed
|
|
||||||
char line[10 * 1024]; // Buffer to hold each line from the config file
|
|
||||||
|
|
||||||
FILE *config_file = fopen(filename, "r");
|
|
||||||
if (config_file == NULL) {
|
|
||||||
perror("Can't open config file");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (fgets(line, sizeof(line), config_file)) {
|
|
||||||
// Remove trailing newlines, carriage returns, spaces and tabs
|
|
||||||
while (strlen(line) && (line[strlen(line) - 1] == '\n' || line[strlen(line) - 1] == '\r'
|
|
||||||
|| line[strlen(line) - 1] == ' ' || line[strlen(line) - 1] == '\t')) {
|
|
||||||
line[strlen(line) - 1] = 0;
|
|
||||||
}
|
|
||||||
// Remove leading spaces and tabs
|
|
||||||
while (strlen(line) && (line[0] == ' ' || line[0] == '\t')) {
|
|
||||||
memmove(line, line + 1, strlen(line));
|
|
||||||
}
|
|
||||||
// Ignore comments
|
|
||||||
char *comment_index = strstr(line, "#");
|
|
||||||
if (comment_index != NULL) {
|
|
||||||
*comment_index = 0;
|
|
||||||
}
|
|
||||||
// Skip empty lines or with spaces only
|
|
||||||
size_t line_len = strlen(line);
|
|
||||||
if (strspn(line, " \t\r\n") == line_len) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It can be new section
|
|
||||||
if (line_len > 0 && line[0] == '[' && line[line_len - 1] == ']') {
|
|
||||||
if (!first_section) {
|
|
||||||
// new config, need to fork the process
|
|
||||||
pid_t pid = fork();
|
|
||||||
if (pid < 0) {
|
|
||||||
log(LL_ERROR, "Can't fork a new instance - %s (%d)", strerror(errno), errno);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
if (pid == 0) {
|
|
||||||
// Close in the child process
|
|
||||||
fclose(config_file);
|
|
||||||
// Stop config file processing for this instance
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Remember the instance to be able to forward signals to it
|
|
||||||
register_child_instance(pid);
|
|
||||||
}
|
|
||||||
size_t len = strlen(line) - 2;
|
|
||||||
if (len > sizeof(section_name) - 1) {
|
|
||||||
len = sizeof(section_name) - 1;
|
|
||||||
}
|
|
||||||
strncpy(section_name, line + 1, len);
|
|
||||||
section_name[len] = 0;
|
|
||||||
|
|
||||||
// Reset all the parameters
|
|
||||||
reset_config(config);
|
|
||||||
|
|
||||||
first_section = 0; // We have processed the first section
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse key-value pairs
|
|
||||||
char *key = strtok(line, "=");
|
|
||||||
key = trim(key);
|
|
||||||
while (strlen(key) && (key[strlen(key) - 1] == ' ' || key[strlen(key) - 1] == '\t' || key[strlen(key) - 1] == '\r' || key[strlen(key) - 1] == '\n')) {
|
|
||||||
key[strlen(key) - 1] = 0;
|
|
||||||
}
|
|
||||||
char *value = strtok(NULL, "=");
|
|
||||||
if (value == NULL) {
|
|
||||||
log(LL_ERROR, "Invalid configuration line: %s", line);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
value = trim(value);
|
|
||||||
if (!*value) {
|
|
||||||
log(LL_ERROR, "Invalid configuration line: %s", line);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
const mini_argp_opt *o = margp_find(options, key, 0);
|
|
||||||
if (o == NULL) {
|
|
||||||
log(LL_ERROR, "Unknown configuration key: %s", key);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
if (!o->has_arg) {
|
|
||||||
// Flag options are represented as booleans in the config file
|
|
||||||
int b = parse_bool(value);
|
|
||||||
if (b < 0) {
|
|
||||||
log(LL_ERROR, "Configuration key '%s' accepts only boolean values (true/yes/on/1 or false/no/off/0)", key);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
if (!b) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
value = NULL;
|
|
||||||
}
|
|
||||||
parse_opt(o->long_name, o->short_name, value, config);
|
|
||||||
}
|
|
||||||
fclose(config_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Parse a single option. */
|
|
||||||
static int parse_opt(const char *lname, char sname, const char *val, void *ctx)
|
|
||||||
{
|
|
||||||
obfuscator_config_t *config = (obfuscator_config_t *)ctx;
|
|
||||||
char val_lower[16];
|
|
||||||
|
|
||||||
switch (sname)
|
|
||||||
{
|
|
||||||
case '?':
|
|
||||||
// Show usage and exit
|
|
||||||
show_usage();
|
|
||||||
exit(EXIT_SUCCESS);
|
|
||||||
case 'c':
|
|
||||||
read_config_file(val, config);
|
|
||||||
break;
|
|
||||||
case 'i':
|
|
||||||
strncpy(config->client_interface, val, sizeof(config->client_interface) - 1);
|
|
||||||
config->client_interface[sizeof(config->client_interface) - 1] = 0; // Ensure null-termination
|
|
||||||
config->client_interface_set = 1;
|
|
||||||
break;
|
|
||||||
case 'p':
|
|
||||||
if (!is_integer(val)) {
|
|
||||||
log(LL_ERROR, "Invalid source port: %s (must be an integer)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->listen_port = atoi(val);
|
|
||||||
if (config->listen_port <= 0 || config->listen_port > 65535) {
|
|
||||||
log(LL_ERROR, "Invalid listen port: %s (must be between 1 and 65535)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->listen_port_set = 1;
|
|
||||||
break;
|
|
||||||
case 't':
|
|
||||||
strncpy(config->forward_host_port, val, sizeof(config->forward_host_port) - 1);
|
|
||||||
config->forward_host_port[sizeof(config->forward_host_port) - 1] = 0; // Ensure null-termination
|
|
||||||
config->forward_host_port_set = 1;
|
|
||||||
break;
|
|
||||||
case 'b':
|
|
||||||
{
|
|
||||||
char *new_bindings;
|
|
||||||
if (!config->static_bindings) {
|
|
||||||
new_bindings = strdup(val);
|
|
||||||
} else {
|
|
||||||
size_t old_len = strlen(config->static_bindings);
|
|
||||||
new_bindings = realloc(config->static_bindings, old_len + strlen(val) + 2);
|
|
||||||
if (new_bindings) {
|
|
||||||
new_bindings[old_len] = ',';
|
|
||||||
strcpy(new_bindings + old_len + 1, val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!new_bindings) {
|
|
||||||
log(LL_ERROR, "Out of memory while parsing static-bindings");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->static_bindings = new_bindings;
|
|
||||||
config->static_bindings_set = 1;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'k':
|
|
||||||
strncpy(config->xor_key, val, sizeof(config->xor_key));
|
|
||||||
config->xor_key[sizeof(config->xor_key) - 1] = 0; // Ensure null-termination
|
|
||||||
if (strlen(config->xor_key) == 0) {
|
|
||||||
log(LL_ERROR, "XOR key cannot be empty");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->xor_key_set = 1;
|
|
||||||
break;
|
|
||||||
case 'm':
|
|
||||||
if (!is_integer(val)) {
|
|
||||||
log(LL_ERROR, "Invalid maximum number of clients: %s (must be an integer)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->max_clients = atoi(val);
|
|
||||||
if (config->max_clients <= 0) {
|
|
||||||
log(LL_ERROR, "Invalid maximum number of clients: %s (must be greater than 0)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'l':
|
|
||||||
if (!is_integer(val)) {
|
|
||||||
log(LL_ERROR, "Invalid idle timeout: %s (must be an integer)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->idle_timeout = atol(val);
|
|
||||||
if (config->idle_timeout <= 0) {
|
|
||||||
log(LL_ERROR, "Invalid idle timeout: %s (must be greater than 0)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->idle_timeout *= 1000; // Convert to milliseconds
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
if (!is_integer(val)) {
|
|
||||||
log(LL_ERROR, "Invalid incoming timeout: %s (must be an integer)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->in_timeout = atol(val);
|
|
||||||
if (config->in_timeout <= 0) {
|
|
||||||
log(LL_ERROR, "Invalid incoming timeout: %s (must be greater than 0)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->in_timeout *= 1000; // Convert to milliseconds
|
|
||||||
break;
|
|
||||||
case 'd':
|
|
||||||
if (!is_integer(val)) {
|
|
||||||
log(LL_ERROR, "Invalid maximum dummy length for data packets: %s (must be an integer)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->max_dummy_length_data = atoi(val);
|
|
||||||
if (config->max_dummy_length_data < 0 || config->max_dummy_length_data > MAX_DUMMY_LENGTH_TOTAL) {
|
|
||||||
log(LL_ERROR, "Invalid maximum dummy length for data packets: %s (must be between 0 and %d)", val, MAX_DUMMY_LENGTH_TOTAL);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'e':
|
|
||||||
config->allow_clean = 1;
|
|
||||||
break;
|
|
||||||
case 'f':
|
|
||||||
// parse string with decimal and hexadecimal support
|
|
||||||
#ifdef __linux__
|
|
||||||
{
|
|
||||||
long int v = strtol(val, NULL, 0);
|
|
||||||
if (v <= 0 || v > UINT16_MAX) {
|
|
||||||
log(LL_ERROR, "Invalid firewall mark: %s", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->fwmark = (uint16_t)v;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
log(LL_WARN, "Firewall mark is not supported on this platform");
|
|
||||||
#endif
|
|
||||||
break;
|
|
||||||
case 'a':
|
|
||||||
{
|
|
||||||
strncpy(val_lower, val, sizeof(val_lower) - 1);
|
|
||||||
val_lower[sizeof(val_lower) - 1] = 0;
|
|
||||||
for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
|
|
||||||
if (strcmp(val_lower, "none") == 0) {
|
|
||||||
config->masking_handler = NULL;
|
|
||||||
config->masking_handler_set = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (strcmp(val_lower, "auto") == 0) {
|
|
||||||
config->masking_handler = NULL;
|
|
||||||
config->masking_handler_set = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
masking_handler_t *handler = get_masking_handler_by_name(val_lower);
|
|
||||||
if (handler == NULL) {
|
|
||||||
log(LL_ERROR, "Unknown masking type: %s", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->masking_handler = handler;
|
|
||||||
config->masking_handler_set = 1;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'v':
|
|
||||||
strncpy(val_lower, val, sizeof(val_lower) - 1);
|
|
||||||
val_lower[sizeof(val_lower) - 1] = 0;
|
|
||||||
for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
|
|
||||||
if (strcmp(val_lower, "error") == 0) {
|
|
||||||
verbose = LL_ERROR;
|
|
||||||
} else if (strcmp(val_lower, "warn") == 0) {
|
|
||||||
verbose = LL_WARN;
|
|
||||||
} else if (strcmp(val_lower, "info") == 0) {
|
|
||||||
verbose = LL_INFO;
|
|
||||||
} else if (strcmp(val_lower, "debug") == 0) {
|
|
||||||
verbose = LL_DEBUG;
|
|
||||||
} else if (strcmp(val_lower, "trace") == 0) {
|
|
||||||
verbose = LL_TRACE;
|
|
||||||
} else {
|
|
||||||
// check if it's a number
|
|
||||||
if (is_integer(val)) {
|
|
||||||
verbose = atoi(val);
|
|
||||||
if (verbose < 0 || verbose > 4) {
|
|
||||||
log(LL_ERROR, "Invalid verbosity level: %s (must be one of 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE')", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log(LL_ERROR, "Invalid verbosity level: %s (must be one of 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE')", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'L':
|
|
||||||
strncpy(config->log_file, val, sizeof(config->log_file) - 1);
|
|
||||||
config->log_file[sizeof(config->log_file) - 1] = 0; // Ensure null-termination
|
|
||||||
if (strlen(config->log_file) == 0) {
|
|
||||||
log(LL_ERROR, "Log file path cannot be empty");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->log_file_set = 1;
|
|
||||||
break;
|
|
||||||
case 'T':
|
|
||||||
{
|
|
||||||
strncpy(val_lower, val, sizeof(val_lower) - 1);
|
|
||||||
val_lower[sizeof(val_lower) - 1] = 0;
|
|
||||||
for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
|
|
||||||
if (strcmp(val_lower, "auto") == 0) {
|
|
||||||
config->log_timestamps = -1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
int b = parse_bool(val_lower);
|
|
||||||
if (b < 0) {
|
|
||||||
log(LL_ERROR, "Invalid log timestamps mode: %s (must be one of 'AUTO', 'TRUE', 'FALSE')", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->log_timestamps = b;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'R':
|
|
||||||
if (!is_integer(val)) {
|
|
||||||
log(LL_ERROR, "Invalid resolve interval: %s (must be an integer)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->resolve_interval = atol(val);
|
|
||||||
if (config->resolve_interval < 0) {
|
|
||||||
log(LL_ERROR, "Invalid resolve interval: %s (must be 0 or greater)", val);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
config->resolve_interval *= 1000; // Convert to milliseconds
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// should never happen
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int parse_config(int argc, char **argv, obfuscator_config_t *config)
|
|
||||||
{
|
|
||||||
/* Parse command line arguments */
|
|
||||||
reset_config(config);
|
|
||||||
arg0 = argv[0]; // Save the executable name
|
|
||||||
if (argc == 1) {
|
|
||||||
fprintf(stderr, "No arguments provided, use \"%s --help\" command for usage information\n", argv[0]);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (mini_argp_parse(argc, argv, options, config, parse_opt) != 0) {
|
|
||||||
fprintf(stderr, "Failed to parse command line arguments\n");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Removes leading and trailing whitespace characters from the input string.
|
|
||||||
*
|
|
||||||
* This function modifies the input string in place by trimming any whitespace
|
|
||||||
* characters (such as spaces, tabs, or newlines) from both the beginning and end.
|
|
||||||
*
|
|
||||||
* @param s Pointer to the null-terminated string to be trimmed.
|
|
||||||
* @return Pointer to the trimmed string.
|
|
||||||
*/
|
|
||||||
char *trim(char *s) {
|
|
||||||
char *end;
|
|
||||||
// Trim leading spaces, tabs, carriage returns and newlines
|
|
||||||
while (*s && (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')) s++;
|
|
||||||
if (!*s) return s;
|
|
||||||
// Trim trailing spaces, tabs, carriage returns and newlines
|
|
||||||
end = s + strlen(s) - 1;
|
|
||||||
while (end > s && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) *end-- = 0;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#ifndef _CONFIG_H_
|
|
||||||
#define _CONFIG_H_
|
|
||||||
|
|
||||||
#include "wg-obfuscator.h"
|
|
||||||
|
|
||||||
char *trim(char *s);
|
|
||||||
int parse_config(int argc, char **argv, obfuscator_config_t *config);
|
|
||||||
|
|
||||||
#endif // _CONFIG_H_
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <time.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include "wg-obfuscator.h"
|
|
||||||
|
|
||||||
#ifndef O_BINARY
|
|
||||||
#define O_BINARY 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef STDERR_FILENO
|
|
||||||
#define STDERR_FILENO 2
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Log files may contain client addresses, so they are not world-readable
|
|
||||||
#define LOG_FILE_MODE 0640
|
|
||||||
|
|
||||||
// File descriptor of the log file, -1 when logging to stderr
|
|
||||||
static int log_fd = -1;
|
|
||||||
// Path of the log file, kept to be able to reopen it on SIGHUP
|
|
||||||
static char *log_path = NULL;
|
|
||||||
// 1 if every line must be prefixed with a timestamp
|
|
||||||
static int log_timestamps = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the single character representing a logging level.
|
|
||||||
*/
|
|
||||||
static char level_char(int level)
|
|
||||||
{
|
|
||||||
switch (level) {
|
|
||||||
case LL_ERROR: return 'E';
|
|
||||||
case LL_WARN: return 'W';
|
|
||||||
case LL_INFO: return 'I';
|
|
||||||
case LL_DEBUG: return 'D';
|
|
||||||
case LL_TRACE: return 'T';
|
|
||||||
default: return '?';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Writes the whole buffer to the descriptor, retrying on partial writes.
|
|
||||||
*
|
|
||||||
* @param fd Descriptor to write to.
|
|
||||||
* @param buf Data to write.
|
|
||||||
* @param len Number of bytes to write.
|
|
||||||
*/
|
|
||||||
static void write_all(int fd, const char *buf, size_t len)
|
|
||||||
{
|
|
||||||
while (len) {
|
|
||||||
ssize_t written = write(fd, buf, len);
|
|
||||||
if (written <= 0) {
|
|
||||||
if (written < 0 && errno == EINTR) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Nothing sensible can be done about a broken log, drop the rest of the line
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
buf += written;
|
|
||||||
len -= (size_t)written;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Appends formatted text to the buffer without ever overflowing it.
|
|
||||||
*
|
|
||||||
* @param buf Buffer to append to.
|
|
||||||
* @param size Usable size of the buffer, including the terminating zero.
|
|
||||||
* @param len Current length of the text in the buffer.
|
|
||||||
* @param fmt Format string.
|
|
||||||
* @param args Arguments for the format string.
|
|
||||||
* @return New length of the text in the buffer.
|
|
||||||
*/
|
|
||||||
static size_t append_vfmt(char *buf, size_t size, size_t len, const char *fmt, va_list args)
|
|
||||||
{
|
|
||||||
int n;
|
|
||||||
|
|
||||||
if (len + 1 >= size) {
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
n = vsnprintf(buf + len, size - len, fmt, args);
|
|
||||||
if (n < 0) {
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
if ((size_t)n >= size - len) {
|
|
||||||
return size - 1; // truncated
|
|
||||||
}
|
|
||||||
return len + (size_t)n;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Appends formatted text to the buffer without ever overflowing it.
|
|
||||||
*
|
|
||||||
* @param buf Buffer to append to.
|
|
||||||
* @param size Usable size of the buffer, including the terminating zero.
|
|
||||||
* @param len Current length of the text in the buffer.
|
|
||||||
* @param fmt Format string.
|
|
||||||
* @return New length of the text in the buffer.
|
|
||||||
*/
|
|
||||||
static size_t append_fmt(char *buf, size_t size, size_t len, const char *fmt, ...)
|
|
||||||
{
|
|
||||||
va_list args;
|
|
||||||
|
|
||||||
va_start(args, fmt);
|
|
||||||
len = append_vfmt(buf, size, len, fmt, args);
|
|
||||||
va_end(args);
|
|
||||||
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Writes the common line prefix (timestamp and instance name) to the buffer.
|
|
||||||
*
|
|
||||||
* @param buf Buffer to write to.
|
|
||||||
* @param size Usable size of the buffer, including the terminating zero.
|
|
||||||
* @param level Logging level of the line.
|
|
||||||
* @return Length of the prefix.
|
|
||||||
*/
|
|
||||||
static size_t log_prefix(char *buf, size_t size, int level)
|
|
||||||
{
|
|
||||||
size_t len = 0;
|
|
||||||
|
|
||||||
if (log_timestamps) {
|
|
||||||
struct timespec ts;
|
|
||||||
struct tm *tm;
|
|
||||||
if (clock_gettime(CLOCK_REALTIME, &ts) == 0 && (tm = localtime(&ts.tv_sec)) != NULL) {
|
|
||||||
len = strftime(buf, size, "%Y-%m-%d %H:%M:%S", tm);
|
|
||||||
len = append_fmt(buf, size, len, ".%03ld ", (long)(ts.tv_nsec / 1000000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return append_fmt(buf, size, len, "[%s][%c] ", section_name, level_char(level));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Terminates the line and writes it out with a single write() call.
|
|
||||||
*
|
|
||||||
* A single write() to a descriptor opened with O_APPEND keeps lines of different
|
|
||||||
* instances from interleaving when they share the same log file.
|
|
||||||
*
|
|
||||||
* @param buf Buffer holding the line, must have one spare byte for the newline.
|
|
||||||
* @param len Length of the line.
|
|
||||||
*/
|
|
||||||
static void log_emit(char *buf, size_t len)
|
|
||||||
{
|
|
||||||
buf[len++] = '\n';
|
|
||||||
write_all(log_fd >= 0 ? log_fd : STDERR_FILENO, buf, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Writes a single log line.
|
|
||||||
*
|
|
||||||
* This is the implementation behind the log() macro, which is the one that
|
|
||||||
* checks the verbosity level, so the line written here is unconditional.
|
|
||||||
*
|
|
||||||
* @param level Logging level of the line.
|
|
||||||
* @param fmt Format string.
|
|
||||||
*/
|
|
||||||
void log_printf(int level, const char *fmt, ...)
|
|
||||||
{
|
|
||||||
char buf[LOG_LINE_MAX];
|
|
||||||
va_list args;
|
|
||||||
size_t len = log_prefix(buf, sizeof(buf) - 1, level);
|
|
||||||
|
|
||||||
va_start(args, fmt);
|
|
||||||
len = append_vfmt(buf, sizeof(buf) - 1, len, fmt, args);
|
|
||||||
va_end(args);
|
|
||||||
|
|
||||||
log_emit(buf, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Writes a hexadecimal dump of a buffer, one or more prefixed lines.
|
|
||||||
*
|
|
||||||
* @param level Logging level of the dump.
|
|
||||||
* @param prefix Text to put right after the line prefix, e.g. a direction marker.
|
|
||||||
* @param data Data to dump.
|
|
||||||
* @param length Number of bytes to dump.
|
|
||||||
*/
|
|
||||||
void log_hexdump(int level, const char *prefix, const uint8_t *data, int length)
|
|
||||||
{
|
|
||||||
char buf[LOG_LINE_MAX];
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
if (verbose < level) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Long dumps are split into several lines, each one with its own prefix
|
|
||||||
do {
|
|
||||||
size_t len = log_prefix(buf, sizeof(buf) - 1, level);
|
|
||||||
len = append_fmt(buf, sizeof(buf) - 1, len, "%s", prefix);
|
|
||||||
while ((i < length) && (len + 3 < sizeof(buf) - 1)) {
|
|
||||||
len = append_fmt(buf, sizeof(buf) - 1, len, "%02X ", data[i++]);
|
|
||||||
}
|
|
||||||
log_emit(buf, len);
|
|
||||||
} while (i < length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initializes logging.
|
|
||||||
*
|
|
||||||
* @param path Path of the log file or NULL to keep logging to stderr.
|
|
||||||
* @param timestamps_mode 1 to force timestamps on, 0 to force them off,
|
|
||||||
* negative to enable them only when logging to a file.
|
|
||||||
*/
|
|
||||||
void log_init(const char *path, int8_t timestamps_mode)
|
|
||||||
{
|
|
||||||
log_timestamps = (timestamps_mode >= 0) ? (timestamps_mode != 0) : (path != NULL);
|
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log_path = strdup(path);
|
|
||||||
if (!log_path) {
|
|
||||||
fprintf(stderr, "Out of memory while initializing the log file\n");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
log_fd = open(path, O_WRONLY | O_CREAT | O_APPEND | O_BINARY, LOG_FILE_MODE);
|
|
||||||
if (log_fd < 0) {
|
|
||||||
// Logging into nowhere is worse than not starting at all
|
|
||||||
fprintf(stderr, "Can't open log file '%s' - %s (%d)\n", path, strerror(errno), errno);
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make every log file start with the version, no matter the verbosity level
|
|
||||||
log_printf(LL_INFO, "Starting %s", version_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Reopens the log file, to be called after the log has been rotated.
|
|
||||||
*/
|
|
||||||
void log_reopen(void)
|
|
||||||
{
|
|
||||||
int fd;
|
|
||||||
|
|
||||||
if (!log_path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_BINARY, LOG_FILE_MODE);
|
|
||||||
if (fd < 0) {
|
|
||||||
// Keep the old descriptor, losing the log completely would be worse
|
|
||||||
fprintf(stderr, "Can't reopen log file '%s' - %s (%d)\n", log_path, strerror(errno), errno);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (log_fd >= 0) {
|
|
||||||
close(log_fd);
|
|
||||||
}
|
|
||||||
log_fd = fd;
|
|
||||||
|
|
||||||
log_printf(LL_INFO, "Log file reopened, %s", version_string());
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <ctype.h>
|
|
||||||
#include "wg-obfuscator.h"
|
|
||||||
#include "masking.h"
|
|
||||||
#include "masking_handlers.h"
|
|
||||||
|
|
||||||
static _Thread_local struct {
|
|
||||||
int listen_sock;
|
|
||||||
struct sockaddr_in *sender_addr;
|
|
||||||
int server_sock;
|
|
||||||
} g_send_ctx;
|
|
||||||
|
|
||||||
static ssize_t send_to_client_cb(uint8_t *buffer, int length) {
|
|
||||||
return sendto(g_send_ctx.listen_sock, buffer, length, 0, (struct sockaddr *)g_send_ctx.sender_addr, sizeof(*g_send_ctx.sender_addr));
|
|
||||||
}
|
|
||||||
|
|
||||||
static ssize_t send_to_server_cb(uint8_t *buffer, int length) {
|
|
||||||
return send(g_send_ctx.server_sock, buffer, length, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
masking_handler_t * get_masking_handler_by_name(const char *name) {
|
|
||||||
if (!name) return NULL;
|
|
||||||
|
|
||||||
for (int i = 0; masking_handlers[i]; ++i) {
|
|
||||||
char handler_name_lower[MASKING_HANDLER_NAME_LEN];
|
|
||||||
strncpy(handler_name_lower, masking_handlers[i]->name, sizeof(handler_name_lower) - 1);
|
|
||||||
handler_name_lower[sizeof(handler_name_lower) - 1] = 0;
|
|
||||||
for (char *p = handler_name_lower; *p; ++p) *p = tolower((unsigned char)*p);
|
|
||||||
|
|
||||||
if (strcmp(name, handler_name_lower) == 0) {
|
|
||||||
return masking_handlers[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
void masking_on_handshake_req_from_client(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *client_addr,
|
|
||||||
struct sockaddr_in *server_addr) {
|
|
||||||
if (!client->masking_handler || !client->masking_handler->on_handshake_req) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_send_ctx.listen_sock = listen_sock;
|
|
||||||
g_send_ctx.sender_addr = &client->client_addr;
|
|
||||||
g_send_ctx.server_sock = client->server_sock;
|
|
||||||
client->masking_handler->on_handshake_req(config, client, DIR_CLIENT_TO_SERVER, client_addr, server_addr, send_to_client_cb, send_to_server_cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
void masking_on_handshake_req_from_server(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *client_addr,
|
|
||||||
struct sockaddr_in *server_addr) {
|
|
||||||
if (!client->masking_handler || !client->masking_handler->on_handshake_req) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_send_ctx.listen_sock = listen_sock;
|
|
||||||
g_send_ctx.sender_addr = &client->client_addr;
|
|
||||||
g_send_ctx.server_sock = client->server_sock;
|
|
||||||
client->masking_handler->on_handshake_req(config, client, DIR_SERVER_TO_CLIENT, server_addr, client_addr, send_to_server_cb, send_to_client_cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
int masking_unwrap_from_client(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client, // can be NULL!
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *client_addr,
|
|
||||||
struct sockaddr_in *server_addr,
|
|
||||||
masking_handler_t **masking_handler_out) {
|
|
||||||
g_send_ctx.listen_sock = listen_sock;
|
|
||||||
g_send_ctx.sender_addr = client_addr;
|
|
||||||
g_send_ctx.server_sock = client ? client->server_sock : 0;
|
|
||||||
|
|
||||||
if (!client && !config->masking_handler_set) {
|
|
||||||
// Brueteforce detection of masking type if no client entry and no default masking handler
|
|
||||||
for (int i = 0; masking_handlers[i]; ++i) {
|
|
||||||
// A handler may move *buffer_ptr while unwrapping. If it does not match
|
|
||||||
// (returns < 0), restore the pointer so the next handler sees the original data.
|
|
||||||
uint8_t *saved_buffer = *buffer_ptr;
|
|
||||||
int r = masking_handlers[i]->on_data_unwrap(buffer_ptr, length, config, NULL, DIR_CLIENT_TO_SERVER, client_addr, server_addr, send_to_client_cb, send_to_server_cb);
|
|
||||||
if (r < 0) {
|
|
||||||
*buffer_ptr = saved_buffer;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Found a matching masking handler
|
|
||||||
log(LL_TRACE, "Autodetected masking handler for packet from %s:%d: %s", inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port), masking_handlers[i]->name);
|
|
||||||
if (masking_handler_out) {
|
|
||||||
*masking_handler_out = masking_handlers[i];
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No matching masking handler found
|
|
||||||
if (masking_handler_out) {
|
|
||||||
*masking_handler_out = NULL;
|
|
||||||
}
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
masking_handler_t *handler = client ? client->masking_handler : config->masking_handler;
|
|
||||||
if (!handler || !handler->on_data_unwrap) {
|
|
||||||
return length; // no masking handler, nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler->on_data_unwrap(buffer_ptr, length, config, client, DIR_CLIENT_TO_SERVER, client_addr, server_addr, send_to_client_cb, client ? send_to_server_cb : NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
int masking_unwrap_from_server(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *server_addr) {
|
|
||||||
if (!client->masking_handler || !client->masking_handler->on_data_unwrap) {
|
|
||||||
return length; // no masking handler, nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
g_send_ctx.listen_sock = listen_sock;
|
|
||||||
g_send_ctx.sender_addr = &client->client_addr;
|
|
||||||
g_send_ctx.server_sock = client->server_sock;
|
|
||||||
return client->masking_handler->on_data_unwrap(buffer_ptr, length, config, client, DIR_SERVER_TO_CLIENT, server_addr, &client->client_addr, send_to_server_cb, send_to_client_cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
int masking_data_wrap_to_client(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *server_addr) {
|
|
||||||
if (!client->masking_handler || !client->masking_handler->on_data_wrap) {
|
|
||||||
return length; // no masking handler, nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
g_send_ctx.listen_sock = listen_sock;
|
|
||||||
g_send_ctx.sender_addr = &client->client_addr;
|
|
||||||
g_send_ctx.server_sock = client->server_sock;
|
|
||||||
return client->masking_handler->on_data_wrap(buffer_ptr, length, config, client, DIR_SERVER_TO_CLIENT, server_addr, &client->client_addr, send_to_server_cb, send_to_client_cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
int masking_data_wrap_to_server(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *server_addr) {
|
|
||||||
if (!client->masking_handler || !client->masking_handler->on_data_wrap) {
|
|
||||||
return length; // no masking handler, nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
g_send_ctx.listen_sock = listen_sock;
|
|
||||||
g_send_ctx.sender_addr = &client->client_addr;
|
|
||||||
g_send_ctx.server_sock = client->server_sock;
|
|
||||||
return client->masking_handler->on_data_wrap(buffer_ptr, length, config, client, DIR_CLIENT_TO_SERVER, &client->client_addr, server_addr, send_to_client_cb, send_to_server_cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
void masking_on_timer(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *server_addr) {
|
|
||||||
if (!client->masking_handler || !client->masking_handler->on_timer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_send_ctx.listen_sock = listen_sock;
|
|
||||||
g_send_ctx.sender_addr = &client->client_addr;
|
|
||||||
g_send_ctx.server_sock = client->server_sock;
|
|
||||||
client->masking_handler->on_timer(config, client, &client->client_addr, server_addr, send_to_client_cb, send_to_server_cb);
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#ifndef _MASKING_H_
|
|
||||||
#define _MASKING_H_
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include "wg-obfuscator.h"
|
|
||||||
|
|
||||||
#define MASKING_HANDLER_NAME_LEN 32
|
|
||||||
|
|
||||||
typedef ssize_t (*send_data_callback_t)(uint8_t *buffer, int length);
|
|
||||||
|
|
||||||
typedef void (*masking_event_handler_t)(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
direction_t direction,
|
|
||||||
const struct sockaddr_in *src_addr,
|
|
||||||
const struct sockaddr_in *dest_addr,
|
|
||||||
send_data_callback_t send_back_callback,
|
|
||||||
send_data_callback_t send_forward_callback);
|
|
||||||
|
|
||||||
typedef int (*masking_data_handler_t)(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
direction_t direction,
|
|
||||||
const struct sockaddr_in *src_addr,
|
|
||||||
const struct sockaddr_in *dest_addr,
|
|
||||||
send_data_callback_t send_back_callback,
|
|
||||||
send_data_callback_t send_forward_callback);
|
|
||||||
|
|
||||||
typedef void (*masking_timer_handler_t)(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
const struct sockaddr_in *client_addr,
|
|
||||||
const struct sockaddr_in *server_addr,
|
|
||||||
send_data_callback_t send_to_client_callback,
|
|
||||||
send_data_callback_t send_to_server_callback);
|
|
||||||
|
|
||||||
struct masking_handler {
|
|
||||||
char name[MASKING_HANDLER_NAME_LEN];
|
|
||||||
masking_event_handler_t on_handshake_req;
|
|
||||||
masking_data_handler_t on_data_wrap;
|
|
||||||
masking_data_handler_t on_data_unwrap;
|
|
||||||
masking_timer_handler_t on_timer;
|
|
||||||
uint32_t timer_interval_s;
|
|
||||||
};
|
|
||||||
typedef struct masking_handler masking_handler_t;
|
|
||||||
|
|
||||||
masking_handler_t * get_masking_handler_by_name(const char *name);
|
|
||||||
|
|
||||||
void masking_on_handshake_req_from_client(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *client_addr,
|
|
||||||
struct sockaddr_in *server_addr);
|
|
||||||
|
|
||||||
void masking_on_handshake_req_from_server(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *client_addr,
|
|
||||||
struct sockaddr_in *server_addr);
|
|
||||||
|
|
||||||
int masking_data_wrap_to_client(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *server_addr);
|
|
||||||
|
|
||||||
int masking_data_wrap_to_server(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *server_addr);
|
|
||||||
|
|
||||||
int masking_unwrap_from_client(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *client_addr,
|
|
||||||
struct sockaddr_in *server_addr,
|
|
||||||
masking_handler_t **masking_handler_out);
|
|
||||||
|
|
||||||
int masking_unwrap_from_server(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *server_addr);
|
|
||||||
|
|
||||||
void masking_on_timer(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
int listen_sock,
|
|
||||||
struct sockaddr_in *server_addr);
|
|
||||||
|
|
||||||
#endif // _MASKING_H_
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#ifndef _MASKING_HANDLERS_H_
|
|
||||||
#define _MASKING_HANDLERS_H_
|
|
||||||
|
|
||||||
#include "masking.h"
|
|
||||||
|
|
||||||
/* List of available masking handlers */
|
|
||||||
#include "masking_stun.h"
|
|
||||||
|
|
||||||
static masking_handler_t * const masking_handlers[] = {
|
|
||||||
&stun_masking_handler,
|
|
||||||
NULL
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <assert.h>
|
|
||||||
#include "wg-obfuscator.h"
|
|
||||||
#include "masking.h"
|
|
||||||
#include "masking_stun.h"
|
|
||||||
|
|
||||||
// Bytes prepended in front of the payload when wrapping data in a STUN Data Indication
|
|
||||||
// (20-byte STUN header + 4-byte attribute header). Must fit into PREBUFFER_SIZE so that
|
|
||||||
// wrapping can be done by shifting the buffer pointer instead of moving the payload.
|
|
||||||
#define STUN_WRAP_OVERHEAD 24
|
|
||||||
_Static_assert(STUN_WRAP_OVERHEAD <= PREBUFFER_SIZE,
|
|
||||||
"PREBUFFER_SIZE is too small to hold the STUN wrap header");
|
|
||||||
|
|
||||||
static void rand_bytes(uint8_t* p, size_t n){ for(size_t i=0;i<n;i++) p[i]=rand()&0xFF; }
|
|
||||||
|
|
||||||
int stun_check_magic(const uint8_t *buf, size_t len) {
|
|
||||||
if (!buf || len < 8) return 0;
|
|
||||||
return !memcmp(buf + 4, COOKIE_BE, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t stun_peek_type(const uint8_t *buf){
|
|
||||||
return (buf[0] << 8) | buf[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_write_header(uint8_t *b, uint16_t type, uint16_t mlen, const uint8_t txid[12]) {
|
|
||||||
b[0] = type >> 8;
|
|
||||||
b[1] = type & 0xFF;
|
|
||||||
b[2] = mlen >> 8;
|
|
||||||
b[3] = mlen & 0xFF;
|
|
||||||
memcpy(b + 4, COOKIE_BE, 4);
|
|
||||||
memcpy(b + 8, txid, 12);
|
|
||||||
return 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_attr_xor_mapped_addr(uint8_t *b, const struct sockaddr_in *src) {
|
|
||||||
// type,len
|
|
||||||
b[0] = STUN_ATTR_XORMAPPED >> 8;
|
|
||||||
b[1] = STUN_ATTR_XORMAPPED & 0xFF;
|
|
||||||
b[2] = 0;
|
|
||||||
b[3] = 8; // len=8 (family + port + addr)
|
|
||||||
b[4] = 0;
|
|
||||||
b[5] = 0x01; // family IPv4
|
|
||||||
memcpy(b + 6, &src->sin_port, 2); // network-order bytes
|
|
||||||
((uint8_t*)b)[6] ^= COOKIE_BE[0];
|
|
||||||
((uint8_t*)b)[7] ^= COOKIE_BE[1];
|
|
||||||
memcpy(b + 8, &src->sin_addr.s_addr, 4); // network-order bytes
|
|
||||||
((uint8_t*)b)[8] ^= COOKIE_BE[0];
|
|
||||||
((uint8_t*)b)[9] ^= COOKIE_BE[1];
|
|
||||||
((uint8_t*)b)[10] ^= COOKIE_BE[2];
|
|
||||||
((uint8_t*)b)[11] ^= COOKIE_BE[3];
|
|
||||||
return 12; // 4 hdr + 8 val
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
static int stun_attr_software(uint8_t *b, const char *s) {
|
|
||||||
uint16_t n = (uint16_t)strlen(s);
|
|
||||||
uint16_t pad = (4 - (n & 3)) & 3;
|
|
||||||
if (b) {
|
|
||||||
b[0] = STUN_ATTR_SOFTWARE >> 8;
|
|
||||||
b[1] = STUN_ATTR_SOFTWARE&0xFF;
|
|
||||||
b[2] = n>>8;
|
|
||||||
b[3] = n&0xFF;
|
|
||||||
memcpy(b+4,s,n);
|
|
||||||
if (pad) memset(b+4+n,0,pad);
|
|
||||||
}
|
|
||||||
return 4 + n + pad;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
static uint32_t crc32(const uint8_t *p, size_t n) {
|
|
||||||
uint32_t crc = ~0u;
|
|
||||||
for(size_t i = 0; i < n; i++){
|
|
||||||
crc ^= p[i];
|
|
||||||
for(int k = 0; k < 8; k++)
|
|
||||||
crc = (crc >> 1) ^ (0xEDB88320u & (-(int)(crc & 1)));
|
|
||||||
}
|
|
||||||
return ~crc;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_attr_fingerprint(uint8_t *pkt, size_t cur_len) {
|
|
||||||
uint8_t *b = pkt + cur_len;
|
|
||||||
b[0] = STUN_ATTR_FINGERPR >> 8;
|
|
||||||
b[1] = STUN_ATTR_FINGERPR & 0xFF;
|
|
||||||
b[2] = 0;
|
|
||||||
b[3] = 4;
|
|
||||||
uint32_t fp = htonl(crc32(pkt, cur_len) ^ 0x5354554eu);
|
|
||||||
memcpy(b + 4, &fp, 4);
|
|
||||||
return 8; // 4 hdr + 4 val
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_build_binding_request(uint8_t *out) {
|
|
||||||
uint8_t txid[12];
|
|
||||||
rand_bytes(txid, 12);
|
|
||||||
stun_write_header(out, STUN_BINDING_REQ, 0, txid);
|
|
||||||
size_t mlen = 0;
|
|
||||||
// optional SOFTWARE
|
|
||||||
//mlen += stun_attr_software(out+20+mlen, "wgo/1.0");
|
|
||||||
mlen += stun_attr_fingerprint(out, 20 + mlen);
|
|
||||||
out[2] = (mlen >> 8) & 0xFF;
|
|
||||||
out[3] = mlen & 0xFF;
|
|
||||||
return (int)(20 + mlen);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_build_binding_success(uint8_t *out,
|
|
||||||
const uint8_t txid[12],
|
|
||||||
const struct sockaddr_in *src) {
|
|
||||||
if (BUFFER_SIZE < 20 + 12 + 8) return -1; // header + XOR-MAPPED-ADDRESS + fingerprint
|
|
||||||
stun_write_header(out, STUN_BINDING_RESP, 0, (uint8_t*)txid);
|
|
||||||
size_t mlen = 0;
|
|
||||||
mlen += stun_attr_xor_mapped_addr(out + 20 + mlen, src);
|
|
||||||
// optional SOFTWARE
|
|
||||||
//mlen += stun_attr_software(out+20+mlen, "wgo/1.0");
|
|
||||||
mlen += stun_attr_fingerprint(out, 20 + mlen);
|
|
||||||
out[2] = (mlen >> 8) & 0xFF;
|
|
||||||
out[3] = mlen & 0xFF;
|
|
||||||
return (int)(20 + mlen);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_wrap(uint8_t **buf_ptr, size_t data_len) {
|
|
||||||
size_t mlen = 0;
|
|
||||||
|
|
||||||
// The caller guarantees the payload already fits into the main buffer (it was
|
|
||||||
// received/encoded within BUFFER_SIZE), so the wrapped packet cannot overflow
|
|
||||||
// the tail of full_buffer. The attribute length field is also 16-bit only.
|
|
||||||
// Note: BUFFER_SIZE (65535) equals the maximum value of a 16-bit unsigned
|
|
||||||
// integer (0xFFFF), so this single check covers both the buffer limit and
|
|
||||||
// the STUN attribute length field limit.
|
|
||||||
if (data_len > BUFFER_SIZE) {
|
|
||||||
log(LL_WARN, "Can't wrap data in STUN, data too large (%zu bytes)", data_len);
|
|
||||||
return -ENOMEM;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header is prepended into the prebuffer (checked at compile time via _Static_assert).
|
|
||||||
*buf_ptr -= STUN_WRAP_OVERHEAD;
|
|
||||||
uint8_t *buf = *buf_ptr;
|
|
||||||
|
|
||||||
uint8_t txid[12];
|
|
||||||
rand_bytes(txid,12);
|
|
||||||
mlen += stun_write_header(buf, STUN_TYPE_DATA_IND, 0, txid);
|
|
||||||
|
|
||||||
buf[mlen] = STUN_ATTR_DATA >> 8;
|
|
||||||
buf[mlen + 1] = STUN_ATTR_DATA & 0xFF;
|
|
||||||
buf[mlen + 2] = data_len >> 8;
|
|
||||||
buf[mlen + 3] = data_len & 0xFF;
|
|
||||||
|
|
||||||
return STUN_WRAP_OVERHEAD + data_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_unwrap(uint8_t **buf_ptr, size_t len) {
|
|
||||||
uint8_t *buf = *buf_ptr;
|
|
||||||
if (len < 20 + 4) return -1; // header + attr
|
|
||||||
|
|
||||||
uint16_t msg_type = (buf[0] << 8) | buf[1];
|
|
||||||
if (msg_type != STUN_TYPE_DATA_IND) return -1;
|
|
||||||
|
|
||||||
uint16_t msg_len = (buf[2] << 8) | buf[3];
|
|
||||||
if (msg_len + 20 > len) return -1;
|
|
||||||
|
|
||||||
uint16_t attr_type = (buf[20] << 8) | buf[21];
|
|
||||||
if (attr_type != STUN_ATTR_DATA) return -1;
|
|
||||||
|
|
||||||
uint16_t data_len = (buf[22] << 8) | buf[23];
|
|
||||||
if (data_len + 24 > len) return -1;
|
|
||||||
|
|
||||||
// Move the buffer pointer to avoid moving the data
|
|
||||||
*buf_ptr += 24;
|
|
||||||
|
|
||||||
return data_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void stun_on_handshake_req(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
direction_t direction,
|
|
||||||
const struct sockaddr_in *src_addr,
|
|
||||||
const struct sockaddr_in *dest_addr,
|
|
||||||
send_data_callback_t send_back_callback,
|
|
||||||
send_data_callback_t send_forward_callback) {
|
|
||||||
uint8_t buffer[128];
|
|
||||||
int len = stun_build_binding_request(buffer);
|
|
||||||
if (len < 0) return;
|
|
||||||
|
|
||||||
int sent = send_forward_callback(buffer, len);
|
|
||||||
if (sent < 0) {
|
|
||||||
serror("Can't send STUN binding request to %s:%d", inet_ntoa(dest_addr->sin_addr), ntohs(dest_addr->sin_port));
|
|
||||||
} else if (sent != len) {
|
|
||||||
log(LL_WARN, "Partial send of STUN binding request to %s:%d (%d of %d bytes)", inet_ntoa(dest_addr->sin_addr), ntohs(dest_addr->sin_port), sent, len);
|
|
||||||
} else {
|
|
||||||
log(LL_TRACE, "Sent STUN binding request (%d bytes) to %s:%d", len, inet_ntoa(dest_addr->sin_addr), ntohs(dest_addr->sin_port));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_on_data_unwrap(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
direction_t direction,
|
|
||||||
const struct sockaddr_in *src_addr,
|
|
||||||
const struct sockaddr_in *dest_addr,
|
|
||||||
send_data_callback_t send_back_callback,
|
|
||||||
send_data_callback_t send_forward_callback) {
|
|
||||||
if (!stun_check_magic(*buffer_ptr, length)) {
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t stun_type = stun_peek_type(*buffer_ptr);
|
|
||||||
|
|
||||||
switch (stun_type) {
|
|
||||||
case STUN_BINDING_REQ: {
|
|
||||||
// Received STUN Binding Request from client, send Binding Success Response
|
|
||||||
log(LL_TRACE, "Received STUN Binding Request from %s:%d", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
|
|
||||||
uint8_t txid[12];
|
|
||||||
memcpy(txid, (*buffer_ptr) + 8, 12);
|
|
||||||
int resp_len = stun_build_binding_success(*buffer_ptr, txid, src_addr);
|
|
||||||
if (resp_len > 0) {
|
|
||||||
int sent = send_back_callback(*buffer_ptr, resp_len);
|
|
||||||
if (sent < 0) {
|
|
||||||
serror("sendto STUN response to %s:%d", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
|
|
||||||
} else if (sent != resp_len) {
|
|
||||||
log(LL_WARN, "Partial send of STUN Binding Success Response to %s:%d (%d of %d bytes)", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port), sent, resp_len);
|
|
||||||
} else {
|
|
||||||
log(LL_TRACE, "Sent STUN Binding Success Response (%d bytes) to %s:%d", resp_len, inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log(LL_ERROR, "Failed to build STUN Binding Success Response");
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
case STUN_BINDING_RESP:
|
|
||||||
log(LL_TRACE, "Received STUN Binding Success Response from %s:%d, ignoring", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
|
|
||||||
return 0;
|
|
||||||
case STUN_TYPE_DATA_IND:
|
|
||||||
length = stun_unwrap(buffer_ptr, length);
|
|
||||||
if (length < 0) {
|
|
||||||
log(LL_DEBUG, "Failed to unwrap STUN Data Indication from %s:%d", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
log(LL_TRACE, "Unwrapped STUN Data Indication from %s:%d (%d bytes)", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port), length);
|
|
||||||
return length;
|
|
||||||
default:
|
|
||||||
log(LL_DEBUG, "Received unknown STUN type %04X from %s:%d, ignoring", stun_type, inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static int stun_on_data_wrap(uint8_t **buffer_ptr, int length,
|
|
||||||
obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
direction_t direction,
|
|
||||||
const struct sockaddr_in *src_addr,
|
|
||||||
const struct sockaddr_in *dest_addr,
|
|
||||||
send_data_callback_t send_back_callback,
|
|
||||||
send_data_callback_t send_forward_callback) {
|
|
||||||
return stun_wrap(buffer_ptr, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void stun_on_timer(obfuscator_config_t *config,
|
|
||||||
client_entry_t *client,
|
|
||||||
const struct sockaddr_in *client_addr,
|
|
||||||
const struct sockaddr_in *server_addr,
|
|
||||||
send_data_callback_t send_to_client_callback,
|
|
||||||
send_data_callback_t send_to_server_callback) {
|
|
||||||
uint8_t buffer[128];
|
|
||||||
int len = stun_build_binding_request(buffer);
|
|
||||||
if (len < 0) return;
|
|
||||||
|
|
||||||
if (client->client_obfuscated) {
|
|
||||||
int sent = send_to_client_callback(buffer, len);
|
|
||||||
if (sent < 0) {
|
|
||||||
serror("STUN binding request to client");
|
|
||||||
} else if (sent != len) {
|
|
||||||
log(LL_WARN, "Partial send of STUN binding request to client (%d of %d bytes)", sent, len);
|
|
||||||
} else {
|
|
||||||
log(LL_TRACE, "Sent STUN binding request (%d bytes) to %s:%d", len, inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (client->server_obfuscated) {
|
|
||||||
int sent = send_to_server_callback(buffer, len);
|
|
||||||
if (sent < 0) {
|
|
||||||
serror("STUN binding request to server");
|
|
||||||
} else if (sent != len) {
|
|
||||||
log(LL_WARN, "Partial send of STUN binding request to server (%d of %d bytes)", sent, len);
|
|
||||||
} else {
|
|
||||||
log(LL_TRACE, "Sent STUN binding request (%d bytes) to %s:%d", len, inet_ntoa(server_addr->sin_addr), ntohs(server_addr->sin_port));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
masking_handler_t stun_masking_handler = {
|
|
||||||
.name = "STUN",
|
|
||||||
.on_handshake_req = stun_on_handshake_req,
|
|
||||||
.on_data_wrap = stun_on_data_wrap,
|
|
||||||
.on_data_unwrap = stun_on_data_unwrap,
|
|
||||||
.on_timer = stun_on_timer,
|
|
||||||
.timer_interval_s = 10, // 10 seconds
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#ifndef _MASKING_STUN_H_
|
|
||||||
#define _MASKING_STUN_H_
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include "wg-obfuscator.h"
|
|
||||||
|
|
||||||
static const uint8_t COOKIE_BE[4] = {0x21,0x12,0xA4,0x42};
|
|
||||||
#define STUN_TYPE_DATA_IND 0x0115
|
|
||||||
#define STUN_BINDING_REQ 0x0001
|
|
||||||
#define STUN_BINDING_RESP 0x0101
|
|
||||||
#define STUN_ATTR_XORMAPPED 0x0020
|
|
||||||
#define STUN_ATTR_SOFTWARE 0x8022
|
|
||||||
#define STUN_ATTR_FINGERPR 0x8028
|
|
||||||
#define STUN_ATTR_DATA 0x0013
|
|
||||||
|
|
||||||
extern masking_handler_t stun_masking_handler;
|
|
||||||
|
|
||||||
#endif // _MASKING_STUN_H_
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/* mini_argp.h
|
|
||||||
* Tiny callback-based CLI parser */
|
|
||||||
|
|
||||||
#ifndef _MINI_ARGP_H_
|
|
||||||
#define _MINI_ARGP_H_
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
typedef int (*mini_argp_cb)(const char *lname, char sname,
|
|
||||||
const char *val, void *ctx);
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
const char *long_name; /* "--target" or NULL */
|
|
||||||
char short_name; /* 't' or 0 */
|
|
||||||
int has_arg; /* 0 flag, 1 needs val */
|
|
||||||
mini_argp_cb cb; /* your handler */
|
|
||||||
} mini_argp_opt;
|
|
||||||
|
|
||||||
/* internal: find desc by name */
|
|
||||||
static const mini_argp_opt *
|
|
||||||
margp_find(const mini_argp_opt *o, char *lname, char sname)
|
|
||||||
{
|
|
||||||
for (; o->long_name || o->short_name; o++)
|
|
||||||
if ((lname && o->long_name && !strcmp(lname,o->long_name)) ||
|
|
||||||
(sname && o->short_name==sname))
|
|
||||||
return o;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int mini_argp_parse(int argc, char **argv,
|
|
||||||
const mini_argp_opt *opts,
|
|
||||||
void *ctx,
|
|
||||||
mini_argp_cb cb)
|
|
||||||
{
|
|
||||||
for (int i = 1; i < argc; ++i) {
|
|
||||||
char *arg = argv[i];
|
|
||||||
|
|
||||||
/* long option: --foo or --foo=bar */
|
|
||||||
if (!strncmp(arg, "--", 2)) {
|
|
||||||
char *name = arg + 2, *val = NULL;
|
|
||||||
if ((val = strchr(name, '='))) { *val = 0; ++val; }
|
|
||||||
|
|
||||||
const mini_argp_opt *o = margp_find(opts, name, 0);
|
|
||||||
if (!o) { fprintf(stderr,"unknown --%s\n", name); return -1; }
|
|
||||||
if (o->has_arg && !val) {
|
|
||||||
if (++i == argc) { fprintf(stderr,"--%s needs value\n",name);return -1; }
|
|
||||||
val = argv[i];
|
|
||||||
}
|
|
||||||
if (!o->has_arg) val = NULL;
|
|
||||||
mini_argp_cb to_call = o->cb ? o->cb : cb;
|
|
||||||
if (to_call(o->long_name, o->short_name, val, ctx)) return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* short(s): -a, -fVAL, -xzvf */
|
|
||||||
else if (arg[0]=='-' && arg[1]) {
|
|
||||||
for (size_t pos=1; arg[pos]; ++pos) {
|
|
||||||
char c = arg[pos];
|
|
||||||
const mini_argp_opt *o = margp_find(opts,NULL,c);
|
|
||||||
if (!o){ fprintf(stderr,"unknown -%c\n",c); return -1;}
|
|
||||||
const char *val = NULL;
|
|
||||||
|
|
||||||
if (o->has_arg) {
|
|
||||||
if (arg[pos+1]) val = &arg[pos+1]; /* -fVAL */
|
|
||||||
else if (++i < argc) val = argv[i]; /* -f VAL */
|
|
||||||
else { fprintf(stderr,"-%c needs value\n",c); return -1;}
|
|
||||||
/* Stop further char-scanning: the rest of the arg is the value */
|
|
||||||
pos = strlen(arg)-1;
|
|
||||||
}
|
|
||||||
mini_argp_cb to_call = o->cb ? o->cb : cb;
|
|
||||||
if (to_call(o->long_name, o->short_name, val, ctx)) return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* positional not allowed */
|
|
||||||
else {
|
|
||||||
fprintf(stderr,"unexpected arg: %s\n",arg); return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif/* _MINI_ARGP_H_ */
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
#include <stdint.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include "wg-obfuscator.h"
|
|
||||||
#include "obfuscation.h"
|
|
||||||
|
|
||||||
static uint8_t crc_a[256]; // step(crc, 0): contribution of the current CRC state
|
|
||||||
static uint8_t crc_b[256]; // step(0, inbyte): contribution of the input byte
|
|
||||||
static uint8_t tables_ready = 0;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t *data; // cached keystream bytes, NULL until first allocation
|
|
||||||
int valid; // number of valid bytes in data
|
|
||||||
int cap; // allocated capacity of data
|
|
||||||
uint8_t crc; // CRC state right after the 'valid' bytes
|
|
||||||
} keystream_row_t;
|
|
||||||
|
|
||||||
static keystream_row_t rows[256];
|
|
||||||
|
|
||||||
// Single step of the original bit-by-bit CRC8 update, kept only to build the tables.
|
|
||||||
static uint8_t crc8_step(uint8_t crc, uint8_t inbyte) {
|
|
||||||
for (uint8_t j = 0; j < 8; j++) {
|
|
||||||
uint8_t mix = (crc ^ inbyte) & 0x01;
|
|
||||||
crc >>= 1;
|
|
||||||
if (mix) {
|
|
||||||
crc ^= 0x8C;
|
|
||||||
}
|
|
||||||
inbyte >>= 1;
|
|
||||||
}
|
|
||||||
return crc;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ensure_tables(void) {
|
|
||||||
if (tables_ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 256; i++) {
|
|
||||||
crc_a[i] = crc8_step((uint8_t)i, 0);
|
|
||||||
crc_b[i] = crc8_step(0, (uint8_t)i);
|
|
||||||
}
|
|
||||||
tables_ready = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the given row holds at least 'need' valid keystream bytes (capped at
|
|
||||||
// KEYSTREAM_ROW_MAX). On allocation failure the row is left as large as it could
|
|
||||||
// grow; the caller then computes the missing tail on the fly.
|
|
||||||
static void grow_row(keystream_row_t *row, int cls, int need, const char *key, int key_length) {
|
|
||||||
if (need > KEYSTREAM_ROW_MAX) {
|
|
||||||
need = KEYSTREAM_ROW_MAX;
|
|
||||||
}
|
|
||||||
if (row->valid >= need) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (row->cap < need) {
|
|
||||||
int newcap = (need + 255) & ~255; // round up to a 256-byte chunk
|
|
||||||
if (newcap > KEYSTREAM_ROW_MAX) {
|
|
||||||
newcap = KEYSTREAM_ROW_MAX;
|
|
||||||
}
|
|
||||||
uint8_t *p = realloc(row->data, newcap);
|
|
||||||
if (!p) {
|
|
||||||
return; // keep whatever we already have; tail handled by caller
|
|
||||||
}
|
|
||||||
row->data = p;
|
|
||||||
row->cap = newcap;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t crc = row->crc;
|
|
||||||
int ki = row->valid % key_length;
|
|
||||||
for (int i = row->valid; i < need; i++) {
|
|
||||||
uint8_t inbyte = (uint8_t)(key[ki] + cls + key_length);
|
|
||||||
crc = crc_a[crc] ^ crc_b[inbyte];
|
|
||||||
row->data[i] = crc;
|
|
||||||
if (++ki == key_length) {
|
|
||||||
ki = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
row->valid = need;
|
|
||||||
row->crc = crc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// XOR 'n' keystream bytes into dst, 8 bytes at a time. memcpy avoids both
|
|
||||||
// strict-aliasing violations and unaligned-access assumptions.
|
|
||||||
static void xor_block(uint8_t *dst, const uint8_t *ks, int n) {
|
|
||||||
int i = 0;
|
|
||||||
for (; i + 8 <= n; i += 8) {
|
|
||||||
uint64_t a, b;
|
|
||||||
memcpy(&a, dst + i, 8);
|
|
||||||
memcpy(&b, ks + i, 8);
|
|
||||||
a ^= b;
|
|
||||||
memcpy(dst + i, &a, 8);
|
|
||||||
}
|
|
||||||
for (; i < n; i++) {
|
|
||||||
dst[i] ^= ks[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void xor_data(uint8_t *buffer, int length, char *key, int key_length) {
|
|
||||||
if (length <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ensure_tables();
|
|
||||||
|
|
||||||
int cls = length & 0xFF;
|
|
||||||
keystream_row_t *row = &rows[cls];
|
|
||||||
|
|
||||||
int cached = (length <= KEYSTREAM_ROW_MAX) ? length : KEYSTREAM_ROW_MAX;
|
|
||||||
if (row->valid < cached) {
|
|
||||||
grow_row(row, cls, cached, key, key_length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int n = (length < row->valid) ? length : row->valid;
|
|
||||||
if (n > 0) {
|
|
||||||
xor_block(buffer, row->data, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tail beyond the cache limit (or beyond what we could allocate): compute on
|
|
||||||
// the fly, continuing the CRC chain from the last cached state.
|
|
||||||
if (n < length) {
|
|
||||||
uint8_t crc = row->crc;
|
|
||||||
int ki = n % key_length;
|
|
||||||
for (int i = n; i < length; i++) {
|
|
||||||
uint8_t inbyte = (uint8_t)(key[ki] + cls + key_length);
|
|
||||||
crc = crc_a[crc] ^ crc_b[inbyte];
|
|
||||||
buffer[i] ^= crc;
|
|
||||||
if (++ki == key_length) {
|
|
||||||
ki = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
#ifndef _OBFUSCATION_H_
|
|
||||||
#define _OBFUSCATION_H_
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
// Current obfuscation version
|
|
||||||
#define OBFUSCATION_VERSION 1
|
|
||||||
|
|
||||||
// Maximum length (in bytes) of a single cached keystream row.
|
|
||||||
// The keystream depends only on (key, length mod 256), so it is cached in 256
|
|
||||||
// lazily-grown rows. Packets longer than this limit are still handled correctly:
|
|
||||||
// the part beyond the limit is computed on the fly. Worst-case cache memory is
|
|
||||||
// 256 * KEYSTREAM_ROW_MAX bytes. 2048 covers a typical MTU plus masking overhead.
|
|
||||||
#define KEYSTREAM_ROW_MAX 2048
|
|
||||||
|
|
||||||
// WireGuard packet types
|
|
||||||
#define WG_TYPE_HANDSHAKE 0x01
|
|
||||||
#define WG_TYPE_HANDSHAKE_RESP 0x02
|
|
||||||
#define WG_TYPE_COOKIE 0x03
|
|
||||||
#define WG_TYPE_DATA 0x04
|
|
||||||
|
|
||||||
#define WG_TYPE(data) ((uint32_t)(data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24)))
|
|
||||||
#ifndef MIN
|
|
||||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given data is obfuscated.
|
|
||||||
*
|
|
||||||
* @param data Pointer to the data buffer to check.
|
|
||||||
* @return uint8_t Returns a non-zero value if the data is obfuscated, 0 otherwise.
|
|
||||||
*/
|
|
||||||
static inline uint8_t is_obfuscated(uint8_t *data) {
|
|
||||||
uint32_t packet_type = WG_TYPE(data);
|
|
||||||
return !(packet_type >= 1 && packet_type <= 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief XORs the data in the given buffer with the key-derived keystream.
|
|
||||||
*
|
|
||||||
* The keystream is a per-byte CRC8 value derived from the key, the byte position
|
|
||||||
* and the packet length; it depends only on (key, length mod 256), so it is
|
|
||||||
* cached (see obfuscation.c). The on-the-wire result is byte-for-byte identical
|
|
||||||
* to the original bit-by-bit implementation.
|
|
||||||
*
|
|
||||||
* @param buffer Pointer to the data buffer to be XORed.
|
|
||||||
* @param length Length of the data buffer in bytes.
|
|
||||||
* @param key Pointer to the key used for XOR operation.
|
|
||||||
* @param key_length Length of the key in bytes.
|
|
||||||
*/
|
|
||||||
void xor_data(uint8_t *buffer, int length, char *key, int key_length);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Encodes the given buffer using the specified key and version.
|
|
||||||
*
|
|
||||||
* This function applies an encoding algorithm to the input buffer using the provided key and version.
|
|
||||||
* WARNING: buffer must be at least 4 bytes long and aligned to 4 bytes.
|
|
||||||
*
|
|
||||||
* @param buffer Pointer to the data buffer to encode.
|
|
||||||
* @param length Length of the data buffer in bytes.
|
|
||||||
* @param key Pointer to the key used for encoding.
|
|
||||||
* @param key_length Length of the key in bytes.
|
|
||||||
* @param version Encoding version to use.
|
|
||||||
* @param max_dummy_length_data Maximum length of dummy data for data packets.
|
|
||||||
* @return 0 on success, or a negative value on error.
|
|
||||||
*/
|
|
||||||
static inline int encode(uint8_t *buffer, int length, char *key, int key_length, uint8_t version, int max_dummy_length_data) {
|
|
||||||
if (version >= 1) {
|
|
||||||
uint32_t packet_type = WG_TYPE(buffer);
|
|
||||||
// Add some randomness to the packet
|
|
||||||
uint8_t rnd = 1 + (rand() % 255);
|
|
||||||
buffer[0] ^= rnd; // Xor the first byte to a random value
|
|
||||||
buffer[1] = rnd; // Set the second byte to a random value
|
|
||||||
// Add dummy data to the packet
|
|
||||||
if (length < MAX_DUMMY_LENGTH_TOTAL) {
|
|
||||||
uint16_t dummy_length = 0;
|
|
||||||
uint16_t max_dummy_length = MAX_DUMMY_LENGTH_TOTAL - length;
|
|
||||||
switch (packet_type) {
|
|
||||||
case WG_TYPE_HANDSHAKE:
|
|
||||||
case WG_TYPE_HANDSHAKE_RESP:
|
|
||||||
// length to MAX_DUMMY_LENGTH_HANDSHAKE
|
|
||||||
dummy_length = rand() % MIN(max_dummy_length, MAX_DUMMY_LENGTH_HANDSHAKE);
|
|
||||||
break;
|
|
||||||
case WG_TYPE_COOKIE:
|
|
||||||
case WG_TYPE_DATA:
|
|
||||||
// length to MAX_DUMMY_LENGTH_HANDSHAKE
|
|
||||||
if (max_dummy_length_data) {
|
|
||||||
dummy_length = rand() % MIN(max_dummy_length, max_dummy_length_data);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
//assert(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buffer[2] = dummy_length & 0xFF; // Set the dummy length in the packet
|
|
||||||
buffer[3] = dummy_length >> 8; // Set the dummy length in
|
|
||||||
if (dummy_length > 0) {
|
|
||||||
int i = length;
|
|
||||||
length += dummy_length;
|
|
||||||
for (; i < length; ++i) {
|
|
||||||
buffer[i] = 0xFF; // Fill with FFs, random data is not needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xor_data(buffer, length, key, key_length);
|
|
||||||
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the given buffer using the provided key.
|
|
||||||
*
|
|
||||||
* WARNING: buffer must be at least 4 bytes long and aligned to 4 bytes.
|
|
||||||
*
|
|
||||||
* @param buffer Pointer to the input buffer to decode.
|
|
||||||
* @param length Length of the input buffer.
|
|
||||||
* @param key Pointer to the key used for decoding.
|
|
||||||
* @param key_length Length of the key.
|
|
||||||
* @param version_out Pointer to a variable where the decoded version will be stored.
|
|
||||||
* @return Length of the decoded data (smaller than or equal to the input length).
|
|
||||||
*/
|
|
||||||
static inline int decode(uint8_t *buffer, int length, char *key, int key_length, uint8_t *version_out) {
|
|
||||||
xor_data(buffer, length, key, key_length);
|
|
||||||
|
|
||||||
if (!is_obfuscated(buffer)) {
|
|
||||||
// Looks like an old version
|
|
||||||
*version_out = 0;
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer[0] ^= buffer[1]; // Restore the first byte by XORing it with the second byte
|
|
||||||
length -= (uint16_t)(buffer[2] | (buffer[3] << 8)); // Remove dummy data length from the packet
|
|
||||||
buffer[1] = buffer[2] = buffer[3] = 0; // Reset the dummy length field to 0
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // _OBFUSCATION_H_
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
#ifndef _WG_OBFUSCATOR_H_
|
|
||||||
#define _WG_OBFUSCATOR_H_
|
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include "uthash.h"
|
|
||||||
|
|
||||||
// on Linux, use epoll for better performance
|
|
||||||
#ifdef __linux__
|
|
||||||
#define USE_EPOLL
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_EPOLL
|
|
||||||
#include <sys/epoll.h>
|
|
||||||
#define MAX_EVENTS 1024
|
|
||||||
#else
|
|
||||||
#include <poll.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define WG_OBFUSCATOR_VERSION "1.6"
|
|
||||||
#define WG_OBFUSCATOR_GIT_REPO "https://github.com/ClusterM/wg-obfuscator"
|
|
||||||
|
|
||||||
#define LL_DEFAULT LL_INFO // default logging level
|
|
||||||
|
|
||||||
// Main parameters
|
|
||||||
// TODO: make these configurable via command line arguments or config file
|
|
||||||
#define BUFFER_SIZE 65535 // size of the buffer for receiving data from the clients and server
|
|
||||||
#define PREBUFFER_SIZE 1024 // size of the additional buffer size before the main buffer
|
|
||||||
#define POLL_TIMEOUT 5000 // in milliseconds
|
|
||||||
#define HANDSHAKE_TIMEOUT 5000 // in milliseconds
|
|
||||||
#define ITERATE_INTERVAL 1000 // in milliseconds
|
|
||||||
#define MAX_DUMMY_LENGTH_TOTAL 1024 // maximum length of a packet after dummy data extension
|
|
||||||
#define MAX_DUMMY_LENGTH_HANDSHAKE 512 // maximum length of dummy data for handshake packets
|
|
||||||
|
|
||||||
#define MAX_CLIENTS_DEFAULT 1024 // maximum number of clients
|
|
||||||
#define IDLE_TIMEOUT_DEFAULT 300000 // in milliseconds
|
|
||||||
#define IN_TIMEOUT_DEFAULT 0 // in milliseconds
|
|
||||||
#define MAX_DUMMY_LENGTH_DATA_DEFAULT 4 // maximum length of dummy data for data packets
|
|
||||||
|
|
||||||
// Default instance name
|
|
||||||
#define DEFAULT_INSTANCE_NAME "main"
|
|
||||||
|
|
||||||
// Logging levels
|
|
||||||
#define LL_ERROR 0
|
|
||||||
#define LL_WARN 1
|
|
||||||
#define LL_INFO 2
|
|
||||||
#define LL_DEBUG 3
|
|
||||||
#define LL_TRACE 4
|
|
||||||
|
|
||||||
// Maximum length of a single log line, longer hex dumps are split into several lines
|
|
||||||
#define LOG_LINE_MAX 16384
|
|
||||||
|
|
||||||
#define log(level, fmt, ...) do { if (verbose >= (level)) log_printf((level), fmt, ##__VA_ARGS__); } while (0)
|
|
||||||
#define serror_level(level, fmt, ...) log(level, fmt " - %s (%d)", ##__VA_ARGS__, strerror(errno), errno)
|
|
||||||
#define serror(fmt, ...) serror_level(LL_ERROR, fmt, ##__VA_ARGS__)
|
|
||||||
|
|
||||||
// Direction of... something
|
|
||||||
typedef enum {
|
|
||||||
DIR_CLIENT_TO_SERVER = 0,
|
|
||||||
DIR_SERVER_TO_CLIENT = 1,
|
|
||||||
} direction_t;
|
|
||||||
|
|
||||||
struct masking_handler; // forward declaration
|
|
||||||
typedef struct masking_handler masking_handler_t;
|
|
||||||
|
|
||||||
// Structure to hold obfuscator configuration
|
|
||||||
typedef struct {
|
|
||||||
int listen_port; // Listening port for the obfuscator
|
|
||||||
char forward_host_port[256]; // Host and port to forward the data to
|
|
||||||
char xor_key[256]; // Key for obfuscation
|
|
||||||
char client_interface[256]; // Client interface as a string
|
|
||||||
char *static_bindings; // Static bindings as a string
|
|
||||||
int max_clients; // Maximum number of clients
|
|
||||||
long idle_timeout; // Idle timeout in milliseconds
|
|
||||||
long in_timeout; // Incoming timeout in milliseconds
|
|
||||||
int max_dummy_length_data; // Maximum length of dummy data for data packets
|
|
||||||
uint32_t fwmark; // Firewall mark
|
|
||||||
masking_handler_t *masking_handler; // Masking handler to use
|
|
||||||
uint8_t allow_clean; // 1 if non-obfuscated (clean) clients are allowed, 0 otherwise
|
|
||||||
char log_file[512]; // Path of the log file
|
|
||||||
int8_t log_timestamps; // 1 to force timestamps on, 0 to force them off, -1 for auto
|
|
||||||
long resolve_interval; // Hostname re-resolve interval in milliseconds, 0 to disable periodic refresh
|
|
||||||
|
|
||||||
uint8_t log_file_set; // 1 if the log file is set, 0 otherwise
|
|
||||||
uint8_t listen_port_set; // 1 if the listen port is set, 0 otherwise
|
|
||||||
uint8_t forward_host_port_set; // 1 if the forward host and port are set, 0 otherwise
|
|
||||||
uint8_t xor_key_set; // 1 if the XOR key is set, 0 otherwise
|
|
||||||
uint8_t client_interface_set; // 1 if the client interface is set, 0 otherwise
|
|
||||||
uint8_t static_bindings_set; // 1 if the static bindings are set, 0 otherwise
|
|
||||||
uint8_t masking_handler_set; // 1 if the masking handler is set, 0 otherwise
|
|
||||||
} obfuscator_config_t;
|
|
||||||
|
|
||||||
// Structure to hold client connection information
|
|
||||||
typedef struct {
|
|
||||||
struct sockaddr_in client_addr; // client address and port (key for the hash table)
|
|
||||||
struct sockaddr_in our_addr; // our address and port on the server connection
|
|
||||||
long last_activity_time; // last time we received data from/to this client
|
|
||||||
long last_incoming_time; // last time we received data from the remote server
|
|
||||||
long last_handshake_request_time; // last time we received a handshake request from/to this client
|
|
||||||
long last_handshake_time; // last time we received a handshake response from/to this client
|
|
||||||
long last_masking_timer_time; // last time we called the masking timer handler for this client
|
|
||||||
int server_sock; // socket for the connection to the server
|
|
||||||
uint8_t version; // obfuscation version
|
|
||||||
masking_handler_t *masking_handler; // masking handler in use
|
|
||||||
uint8_t handshaked : 1; // 1 if the handshake is complete, 0 otherwise
|
|
||||||
uint8_t handshake_direction : 1; // 1 if the handshake is from client to server, 0 if from server to client
|
|
||||||
uint8_t client_obfuscated : 1; // 1 if the client is obfuscated, 0 otherwise
|
|
||||||
uint8_t server_obfuscated : 1; // 1 if the server is obfuscated, 0 otherwise
|
|
||||||
uint8_t client_clean : 1; // 1 if the client speaks plain (non-obfuscated) WireGuard, traffic is passed through as is (allow-clean mode)
|
|
||||||
uint8_t is_static : 1; // 1 if this is a static binding entry, 0 otherwise
|
|
||||||
char bind_host[256]; // Original hostname of a static binding, empty if the address is a literal or the entry is dynamic
|
|
||||||
UT_hash_handle hh;
|
|
||||||
} client_entry_t;
|
|
||||||
|
|
||||||
// Verbosity level
|
|
||||||
extern int verbose;
|
|
||||||
// Section name (for multiple instances)
|
|
||||||
extern char section_name[256];
|
|
||||||
|
|
||||||
const char *version_string(void);
|
|
||||||
void print_version(void);
|
|
||||||
void register_child_instance(pid_t pid);
|
|
||||||
|
|
||||||
void log_init(const char *path, int8_t timestamps_mode);
|
|
||||||
void log_reopen(void);
|
|
||||||
#ifdef __GNUC__
|
|
||||||
void log_printf(int level, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
|
||||||
#else
|
|
||||||
void log_printf(int level, const char *fmt, ...);
|
|
||||||
#endif
|
|
||||||
void log_hexdump(int level, const char *prefix, const uint8_t *data, int length);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* pcarelay — in-process transport switch for the iOS packet tunnel.
|
|
||||||
* wg-obfuscator (one instance, iOS forbids fork) sends to 127.0.0.1:<local>;
|
|
||||||
* we forward either over UDP to the server's obfuscator port or over TCP to
|
|
||||||
* the server's udptcp (frames: 2-byte length + packet). Switch with pcarelay_use_tcp().
|
|
||||||
*/
|
|
||||||
#include "pcarelay.h"
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <netdb.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <netinet/tcp.h>
|
|
||||||
#include <poll.h>
|
|
||||||
#include <pthread.h>
|
|
||||||
#include <stdatomic.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <time.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
static atomic_int use_tcp = 0;
|
|
||||||
static atomic_ullong rx_bytes = 0;
|
|
||||||
static char g_host[256], g_udp_port[8], g_tcp_port[8];
|
|
||||||
static int g_local_port;
|
|
||||||
|
|
||||||
static int dial(int socktype, const char *port) {
|
|
||||||
struct addrinfo hints = {0}, *res, *r;
|
|
||||||
hints.ai_family = AF_UNSPEC;
|
|
||||||
hints.ai_socktype = socktype;
|
|
||||||
if (getaddrinfo(g_host, port, &hints, &res) != 0) return -1;
|
|
||||||
int s = -1;
|
|
||||||
for (r = res; r; r = r->ai_next) {
|
|
||||||
s = socket(r->ai_family, r->ai_socktype, 0);
|
|
||||||
if (s < 0) continue;
|
|
||||||
if (connect(s, r->ai_addr, r->ai_addrlen) == 0) break;
|
|
||||||
close(s); s = -1;
|
|
||||||
}
|
|
||||||
freeaddrinfo(res);
|
|
||||||
if (s >= 0 && socktype == SOCK_STREAM) {
|
|
||||||
int one = 1;
|
|
||||||
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
|
|
||||||
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one);
|
|
||||||
setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof one);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int read_full(int fd, unsigned char *b, size_t n) {
|
|
||||||
size_t got = 0;
|
|
||||||
while (got < n) { ssize_t r = read(fd, b + got, n - got); if (r <= 0) return -1; got += (size_t)r; }
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void *loop(void *arg) {
|
|
||||||
(void)arg;
|
|
||||||
struct sockaddr_in la = {0};
|
|
||||||
la.sin_family = AF_INET;
|
|
||||||
la.sin_port = htons((uint16_t)g_local_port);
|
|
||||||
la.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
||||||
int local = socket(AF_INET, SOCK_DGRAM, 0);
|
|
||||||
if (local < 0 || bind(local, (struct sockaddr *)&la, sizeof la) < 0) return NULL;
|
|
||||||
int udp = -1, tcp = -1;
|
|
||||||
struct sockaddr_storage peer; socklen_t pl = 0;
|
|
||||||
unsigned char buf[65537];
|
|
||||||
time_t last_dial = 0;
|
|
||||||
for (;;) {
|
|
||||||
int want_tcp = atomic_load(&use_tcp);
|
|
||||||
if (!want_tcp && tcp >= 0) { close(tcp); tcp = -1; }
|
|
||||||
if (want_tcp && udp >= 0) { close(udp); udp = -1; }
|
|
||||||
if (!want_tcp && udp < 0 && time(NULL) != last_dial) { last_dial = time(NULL); udp = dial(SOCK_DGRAM, g_udp_port); }
|
|
||||||
int up = want_tcp ? tcp : udp;
|
|
||||||
struct pollfd p[2] = {{local, POLLIN, 0}, {up, POLLIN, 0}};
|
|
||||||
poll(p, up >= 0 ? 2 : 1, 1000);
|
|
||||||
if (p[0].revents & POLLIN) {
|
|
||||||
pl = sizeof peer;
|
|
||||||
ssize_t n = recvfrom(local, buf, 65535, 0, (struct sockaddr *)&peer, &pl);
|
|
||||||
if (n <= 0) continue;
|
|
||||||
if (want_tcp) {
|
|
||||||
if (tcp < 0 && time(NULL) != last_dial) { last_dial = time(NULL); tcp = dial(SOCK_STREAM, g_tcp_port); }
|
|
||||||
if (tcp >= 0) {
|
|
||||||
unsigned char h[2] = {(unsigned char)(n >> 8), (unsigned char)n};
|
|
||||||
if (send(tcp, h, 2, 0) != 2 || send(tcp, buf, (size_t)n, 0) != n) { close(tcp); tcp = -1; }
|
|
||||||
}
|
|
||||||
} else if (udp >= 0) {
|
|
||||||
send(udp, buf, (size_t)n, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (up >= 0 && (p[1].revents & (POLLIN | POLLHUP | POLLERR))) {
|
|
||||||
ssize_t n;
|
|
||||||
if (want_tcp) {
|
|
||||||
unsigned char h[2];
|
|
||||||
if (read_full(tcp, h, 2) < 0) { close(tcp); tcp = -1; continue; }
|
|
||||||
n = (h[0] << 8) | h[1];
|
|
||||||
if (n == 0 || read_full(tcp, buf, (size_t)n) < 0) { close(tcp); tcp = -1; continue; }
|
|
||||||
} else {
|
|
||||||
n = recv(udp, buf, 65535, 0);
|
|
||||||
if (n <= 0) { close(udp); udp = -1; continue; }
|
|
||||||
}
|
|
||||||
atomic_fetch_add(&rx_bytes, (unsigned long long)n);
|
|
||||||
if (pl) sendto(local, buf, (size_t)n, 0, (struct sockaddr *)&peer, pl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int pcarelay_start(int local_port, const char *host, int udp_port, int tcp_port) {
|
|
||||||
g_local_port = local_port;
|
|
||||||
snprintf(g_host, sizeof g_host, "%s", host);
|
|
||||||
snprintf(g_udp_port, sizeof g_udp_port, "%d", udp_port);
|
|
||||||
snprintf(g_tcp_port, sizeof g_tcp_port, "%d", tcp_port);
|
|
||||||
pthread_t t;
|
|
||||||
if (pthread_create(&t, NULL, loop, NULL) != 0) return -1;
|
|
||||||
pthread_detach(t);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void pcarelay_use_tcp(int on) { atomic_store(&use_tcp, on ? 1 : 0); }
|
|
||||||
int pcarelay_is_tcp(void) { return atomic_load(&use_tcp); }
|
|
||||||
unsigned long long pcarelay_rx_bytes(void) { return atomic_load(&rx_bytes); }
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#ifndef PCARELAY_H
|
|
||||||
#define PCARELAY_H
|
|
||||||
|
|
||||||
/* Start the transport switch thread: obfuscator -> 127.0.0.1:local_port -> host via UDP or TCP. */
|
|
||||||
int pcarelay_start(int local_port, const char *host, int udp_port, int tcp_port);
|
|
||||||
void pcarelay_use_tcp(int on);
|
|
||||||
int pcarelay_is_tcp(void);
|
|
||||||
/* Bytes received back from the server on any path (auto-detects a dead UDP). */
|
|
||||||
unsigned long long pcarelay_rx_bytes(void);
|
|
||||||
|
|
||||||
/* wg-obfuscator compiled in with -Dmain=wgobf_main (one instance per process). */
|
|
||||||
int wgobf_main(int argc, char **argv);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// swift-tools-version:5.5
|
|
||||||
// Vendored WireGuardKit (github.com/WireGuard/wireguard-apple, MIT). libwg-go.a is
|
|
||||||
// prebuilt in ../WireGuardGo/<sdk>/ — the app target adds it to LIBRARY_SEARCH_PATHS.
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "WireGuardKit",
|
|
||||||
platforms: [.iOS(.v15)],
|
|
||||||
products: [.library(name: "WireGuardKit", targets: ["WireGuardKit"])],
|
|
||||||
targets: [
|
|
||||||
.target(name: "WireGuardKit", dependencies: ["WireGuardKitGo", "WireGuardKitC"]),
|
|
||||||
.target(name: "WireGuardKitC", publicHeadersPath: "."),
|
|
||||||
.target(name: "WireGuardKitGo", publicHeadersPath: ".", linkerSettings: [.linkedLibrary("wg-go")]),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension Array {
|
|
||||||
|
|
||||||
/// Returns an array containing the results of mapping the given closure over the sequence’s
|
|
||||||
/// elements concurrently.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - queue: The queue for performing concurrent computations.
|
|
||||||
/// If the given queue is serial, the values are mapped in a serial fashion.
|
|
||||||
/// Pass `nil` to perform computations on the current queue.
|
|
||||||
/// - transform: the block to perform concurrent computations over the given element.
|
|
||||||
/// - Returns: an array of concurrently computed values.
|
|
||||||
func concurrentMap<U>(queue: DispatchQueue?, _ transform: (Element) -> U) -> [U] {
|
|
||||||
var result = [U?](repeating: nil, count: self.count)
|
|
||||||
let resultQueue = DispatchQueue(label: "ConcurrentMapQueue")
|
|
||||||
|
|
||||||
let execute = queue?.sync ?? { $0() }
|
|
||||||
|
|
||||||
execute {
|
|
||||||
DispatchQueue.concurrentPerform(iterations: self.count) { index in
|
|
||||||
let value = transform(self[index])
|
|
||||||
resultQueue.sync {
|
|
||||||
result[index] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.map { $0! }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Network
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum DNSResolver {}
|
|
||||||
|
|
||||||
extension DNSResolver {
|
|
||||||
|
|
||||||
/// Concurrent queue used for DNS resolutions
|
|
||||||
private static let resolverQueue = DispatchQueue(label: "DNSResolverQueue", qos: .default, attributes: .concurrent)
|
|
||||||
|
|
||||||
static func resolveSync(endpoints: [Endpoint?]) -> [Result<Endpoint, DNSResolutionError>?] {
|
|
||||||
let isAllEndpointsAlreadyResolved = endpoints.allSatisfy { maybeEndpoint -> Bool in
|
|
||||||
return maybeEndpoint?.hasHostAsIPAddress() ?? true
|
|
||||||
}
|
|
||||||
|
|
||||||
if isAllEndpointsAlreadyResolved {
|
|
||||||
return endpoints.map { endpoint in
|
|
||||||
return endpoint.map { .success($0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return endpoints.concurrentMap(queue: resolverQueue) { endpoint -> Result<Endpoint, DNSResolutionError>? in
|
|
||||||
guard let endpoint = endpoint else { return nil }
|
|
||||||
|
|
||||||
if endpoint.hasHostAsIPAddress() {
|
|
||||||
return .success(endpoint)
|
|
||||||
} else {
|
|
||||||
return Result { try DNSResolver.resolveSync(endpoint: endpoint) }
|
|
||||||
.mapError { error -> DNSResolutionError in
|
|
||||||
// swiftlint:disable:next force_cast
|
|
||||||
return error as! DNSResolutionError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func resolveSync(endpoint: Endpoint) throws -> Endpoint {
|
|
||||||
guard case .name(let name, _) = endpoint.host else {
|
|
||||||
return endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
var hints = addrinfo()
|
|
||||||
hints.ai_flags = AI_ALL // We set this to ALL so that we get v4 addresses even on DNS64 networks
|
|
||||||
hints.ai_family = AF_UNSPEC
|
|
||||||
hints.ai_socktype = SOCK_DGRAM
|
|
||||||
hints.ai_protocol = IPPROTO_UDP
|
|
||||||
|
|
||||||
var resultPointer: UnsafeMutablePointer<addrinfo>?
|
|
||||||
defer {
|
|
||||||
resultPointer.flatMap { freeaddrinfo($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorCode = getaddrinfo(name, "\(endpoint.port)", &hints, &resultPointer)
|
|
||||||
if errorCode != 0 {
|
|
||||||
throw DNSResolutionError(errorCode: errorCode, address: name)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ipv4Address: IPv4Address?
|
|
||||||
var ipv6Address: IPv6Address?
|
|
||||||
|
|
||||||
var next: UnsafeMutablePointer<addrinfo>? = resultPointer
|
|
||||||
let iterator = AnyIterator { () -> addrinfo? in
|
|
||||||
let result = next?.pointee
|
|
||||||
next = result?.ai_next
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
for addrInfo in iterator {
|
|
||||||
if let maybeIpv4Address = IPv4Address(addrInfo: addrInfo) {
|
|
||||||
ipv4Address = maybeIpv4Address
|
|
||||||
break // If we found an IPv4 address, we can stop
|
|
||||||
} else if let maybeIpv6Address = IPv6Address(addrInfo: addrInfo) {
|
|
||||||
ipv6Address = maybeIpv6Address
|
|
||||||
continue // If we already have an IPv6 address, we can skip this one
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We prefer an IPv4 address over an IPv6 address
|
|
||||||
if let ipv4Address = ipv4Address {
|
|
||||||
return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
|
|
||||||
} else if let ipv6Address = ipv6Address {
|
|
||||||
return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
|
|
||||||
} else {
|
|
||||||
// Must never happen
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Endpoint {
|
|
||||||
func withReresolvedIP() throws -> Endpoint {
|
|
||||||
#if os(iOS)
|
|
||||||
let hostname: String
|
|
||||||
switch host {
|
|
||||||
case .name(let name, _):
|
|
||||||
hostname = name
|
|
||||||
case .ipv4(let address):
|
|
||||||
hostname = "\(address)"
|
|
||||||
case .ipv6(let address):
|
|
||||||
hostname = "\(address)"
|
|
||||||
@unknown default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
var hints = addrinfo()
|
|
||||||
hints.ai_family = AF_UNSPEC
|
|
||||||
hints.ai_socktype = SOCK_DGRAM
|
|
||||||
hints.ai_protocol = IPPROTO_UDP
|
|
||||||
hints.ai_flags = 0 // We set this to zero so that we actually resolve this using DNS64
|
|
||||||
|
|
||||||
var result: UnsafeMutablePointer<addrinfo>?
|
|
||||||
defer {
|
|
||||||
result.flatMap { freeaddrinfo($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorCode = getaddrinfo(hostname, "\(self.port)", &hints, &result)
|
|
||||||
if errorCode != 0 {
|
|
||||||
throw DNSResolutionError(errorCode: errorCode, address: hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
let addrInfo = result!.pointee
|
|
||||||
if let ipv4Address = IPv4Address(addrInfo: addrInfo) {
|
|
||||||
return Endpoint(host: .ipv4(ipv4Address), port: port)
|
|
||||||
} else if let ipv6Address = IPv6Address(addrInfo: addrInfo) {
|
|
||||||
return Endpoint(host: .ipv6(ipv6Address), port: port)
|
|
||||||
} else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
#elseif os(macOS)
|
|
||||||
return self
|
|
||||||
#else
|
|
||||||
#error("Unimplemented")
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An error type describing DNS resolution error
|
|
||||||
public struct DNSResolutionError: LocalizedError {
|
|
||||||
public let errorCode: Int32
|
|
||||||
public let address: String
|
|
||||||
|
|
||||||
init(errorCode: Int32, address: String) {
|
|
||||||
self.errorCode = errorCode
|
|
||||||
self.address = address
|
|
||||||
}
|
|
||||||
|
|
||||||
public var errorDescription: String? {
|
|
||||||
return String(cString: gai_strerror(errorCode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Network
|
|
||||||
|
|
||||||
public struct DNSServer {
|
|
||||||
public let address: IPAddress
|
|
||||||
|
|
||||||
public init(address: IPAddress) {
|
|
||||||
self.address = address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DNSServer: Equatable {
|
|
||||||
public static func == (lhs: DNSServer, rhs: DNSServer) -> Bool {
|
|
||||||
return lhs.address.rawValue == rhs.address.rawValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DNSServer {
|
|
||||||
public var stringRepresentation: String {
|
|
||||||
return "\(address)"
|
|
||||||
}
|
|
||||||
|
|
||||||
public init?(from addressString: String) {
|
|
||||||
if let addr = IPv4Address(addressString) {
|
|
||||||
address = addr
|
|
||||||
} else if let addr = IPv6Address(addressString) {
|
|
||||||
address = addr
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Network
|
|
||||||
|
|
||||||
public struct Endpoint {
|
|
||||||
public let host: NWEndpoint.Host
|
|
||||||
public let port: NWEndpoint.Port
|
|
||||||
|
|
||||||
public init(host: NWEndpoint.Host, port: NWEndpoint.Port) {
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Endpoint: Equatable {
|
|
||||||
public static func == (lhs: Endpoint, rhs: Endpoint) -> Bool {
|
|
||||||
return lhs.host == rhs.host && lhs.port == rhs.port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Endpoint: Hashable {
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(host)
|
|
||||||
hasher.combine(port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Endpoint {
|
|
||||||
public var stringRepresentation: String {
|
|
||||||
switch host {
|
|
||||||
case .name(let hostname, _):
|
|
||||||
return "\(hostname):\(port)"
|
|
||||||
case .ipv4(let address):
|
|
||||||
return "\(address):\(port)"
|
|
||||||
case .ipv6(let address):
|
|
||||||
return "[\(address)]:\(port)"
|
|
||||||
@unknown default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public init?(from string: String) {
|
|
||||||
// Separation of host and port is based on 'parse_endpoint' function in
|
|
||||||
// https://git.zx2c4.com/wireguard-tools/tree/src/config.c
|
|
||||||
guard !string.isEmpty else { return nil }
|
|
||||||
let startOfPort: String.Index
|
|
||||||
let hostString: String
|
|
||||||
if string.first! == "[" {
|
|
||||||
// Look for IPv6-style endpoint, like [::1]:80
|
|
||||||
let startOfHost = string.index(after: string.startIndex)
|
|
||||||
guard let endOfHost = string.dropFirst().firstIndex(of: "]") else { return nil }
|
|
||||||
let afterEndOfHost = string.index(after: endOfHost)
|
|
||||||
if afterEndOfHost == string.endIndex { return nil }
|
|
||||||
guard string[afterEndOfHost] == ":" else { return nil }
|
|
||||||
startOfPort = string.index(after: afterEndOfHost)
|
|
||||||
hostString = String(string[startOfHost ..< endOfHost])
|
|
||||||
} else {
|
|
||||||
// Look for an IPv4-style endpoint, like 127.0.0.1:80
|
|
||||||
guard let endOfHost = string.firstIndex(of: ":") else { return nil }
|
|
||||||
startOfPort = string.index(after: endOfHost)
|
|
||||||
hostString = String(string[string.startIndex ..< endOfHost])
|
|
||||||
}
|
|
||||||
guard let endpointPort = NWEndpoint.Port(String(string[startOfPort ..< string.endIndex])) else { return nil }
|
|
||||||
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
|
|
||||||
return !CharacterSet.urlHostAllowed.contains(char)
|
|
||||||
}
|
|
||||||
guard invalidCharacterIndex == nil else { return nil }
|
|
||||||
host = NWEndpoint.Host(hostString)
|
|
||||||
port = endpointPort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Endpoint {
|
|
||||||
public func hasHostAsIPAddress() -> Bool {
|
|
||||||
switch host {
|
|
||||||
case .name:
|
|
||||||
return false
|
|
||||||
case .ipv4:
|
|
||||||
return true
|
|
||||||
case .ipv6:
|
|
||||||
return true
|
|
||||||
@unknown default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func hostname() -> String? {
|
|
||||||
switch host {
|
|
||||||
case .name(let hostname, _):
|
|
||||||
return hostname
|
|
||||||
case .ipv4:
|
|
||||||
return nil
|
|
||||||
case .ipv6:
|
|
||||||
return nil
|
|
||||||
@unknown default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Network
|
|
||||||
|
|
||||||
extension IPv4Address {
|
|
||||||
init?(addrInfo: addrinfo) {
|
|
||||||
guard addrInfo.ai_family == AF_INET else { return nil }
|
|
||||||
|
|
||||||
let addressData = addrInfo.ai_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { ptr -> Data in
|
|
||||||
return Data(bytes: &ptr.pointee.sin_addr, count: MemoryLayout<in_addr>.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.init(addressData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension IPv6Address {
|
|
||||||
init?(addrInfo: addrinfo) {
|
|
||||||
guard addrInfo.ai_family == AF_INET6 else { return nil }
|
|
||||||
|
|
||||||
let addressData = addrInfo.ai_addr.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { ptr -> Data in
|
|
||||||
return Data(bytes: &ptr.pointee.sin6_addr, count: MemoryLayout<in6_addr>.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.init(addressData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Network
|
|
||||||
|
|
||||||
public struct IPAddressRange {
|
|
||||||
public let address: IPAddress
|
|
||||||
public let networkPrefixLength: UInt8
|
|
||||||
|
|
||||||
init(address: IPAddress, networkPrefixLength: UInt8) {
|
|
||||||
self.address = address
|
|
||||||
self.networkPrefixLength = networkPrefixLength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension IPAddressRange: Equatable {
|
|
||||||
public static func == (lhs: IPAddressRange, rhs: IPAddressRange) -> Bool {
|
|
||||||
return lhs.address.rawValue == rhs.address.rawValue && lhs.networkPrefixLength == rhs.networkPrefixLength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension IPAddressRange: Hashable {
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(address.rawValue)
|
|
||||||
hasher.combine(networkPrefixLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension IPAddressRange {
|
|
||||||
public var stringRepresentation: String {
|
|
||||||
return "\(address)/\(networkPrefixLength)"
|
|
||||||
}
|
|
||||||
|
|
||||||
public init?(from string: String) {
|
|
||||||
guard let parsed = IPAddressRange.parseAddressString(string) else { return nil }
|
|
||||||
address = parsed.0
|
|
||||||
networkPrefixLength = parsed.1
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func parseAddressString(_ string: String) -> (IPAddress, UInt8)? {
|
|
||||||
let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex
|
|
||||||
let addressString = String(string[string.startIndex ..< endOfIPAddress])
|
|
||||||
let address: IPAddress
|
|
||||||
if let addr = IPv4Address(addressString) {
|
|
||||||
address = addr
|
|
||||||
} else if let addr = IPv6Address(addressString) {
|
|
||||||
address = addr
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxNetworkPrefixLength: UInt8 = address is IPv4Address ? 32 : 128
|
|
||||||
var networkPrefixLength: UInt8
|
|
||||||
if endOfIPAddress < string.endIndex { // "/" was located
|
|
||||||
let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress)
|
|
||||||
guard indexOfNetworkPrefixLength < string.endIndex else { return nil }
|
|
||||||
let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex]
|
|
||||||
guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil }
|
|
||||||
networkPrefixLength = min(npl, maxNetworkPrefixLength)
|
|
||||||
} else {
|
|
||||||
networkPrefixLength = maxNetworkPrefixLength
|
|
||||||
}
|
|
||||||
|
|
||||||
return (address, networkPrefixLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func subnetMask() -> IPAddress {
|
|
||||||
if address is IPv4Address {
|
|
||||||
let mask = networkPrefixLength > 0 ? ~UInt32(0) << (32 - networkPrefixLength) : UInt32(0)
|
|
||||||
let bytes = Data([
|
|
||||||
UInt8(truncatingIfNeeded: mask >> 24),
|
|
||||||
UInt8(truncatingIfNeeded: mask >> 16),
|
|
||||||
UInt8(truncatingIfNeeded: mask >> 8),
|
|
||||||
UInt8(truncatingIfNeeded: mask >> 0)
|
|
||||||
])
|
|
||||||
return IPv4Address(bytes)!
|
|
||||||
}
|
|
||||||
if address is IPv6Address {
|
|
||||||
var bytes = Data(repeating: 0, count: 16)
|
|
||||||
for i in 0..<Int(networkPrefixLength/8) {
|
|
||||||
bytes[i] = 0xff
|
|
||||||
}
|
|
||||||
let nibble = networkPrefixLength % 32
|
|
||||||
if nibble != 0 {
|
|
||||||
let mask = ~UInt32(0) << (32 - nibble)
|
|
||||||
let i = Int(networkPrefixLength / 32 * 4)
|
|
||||||
bytes[i + 0] = UInt8(truncatingIfNeeded: mask >> 24)
|
|
||||||
bytes[i + 1] = UInt8(truncatingIfNeeded: mask >> 16)
|
|
||||||
bytes[i + 2] = UInt8(truncatingIfNeeded: mask >> 8)
|
|
||||||
bytes[i + 3] = UInt8(truncatingIfNeeded: mask >> 0)
|
|
||||||
}
|
|
||||||
return IPv6Address(bytes)!
|
|
||||||
}
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func maskedAddress() -> IPAddress {
|
|
||||||
let subnet = subnetMask().rawValue
|
|
||||||
var masked = Data(address.rawValue)
|
|
||||||
if subnet.count != masked.count {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
for i in 0..<subnet.count {
|
|
||||||
masked[i] &= subnet[i]
|
|
||||||
}
|
|
||||||
if subnet.count == 4 {
|
|
||||||
return IPv4Address(masked)!
|
|
||||||
}
|
|
||||||
if subnet.count == 16 {
|
|
||||||
return IPv6Address(masked)!
|
|
||||||
}
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Network
|
|
||||||
|
|
||||||
public struct InterfaceConfiguration {
|
|
||||||
public var privateKey: PrivateKey
|
|
||||||
public var addresses = [IPAddressRange]()
|
|
||||||
public var listenPort: UInt16?
|
|
||||||
public var mtu: UInt16?
|
|
||||||
public var dns = [DNSServer]()
|
|
||||||
public var dnsSearch = [String]()
|
|
||||||
|
|
||||||
public init(privateKey: PrivateKey) {
|
|
||||||
self.privateKey = privateKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InterfaceConfiguration: Equatable {
|
|
||||||
public static func == (lhs: InterfaceConfiguration, rhs: InterfaceConfiguration) -> Bool {
|
|
||||||
let lhsAddresses = lhs.addresses.filter { $0.address is IPv4Address } + lhs.addresses.filter { $0.address is IPv6Address }
|
|
||||||
let rhsAddresses = rhs.addresses.filter { $0.address is IPv4Address } + rhs.addresses.filter { $0.address is IPv6Address }
|
|
||||||
|
|
||||||
return lhs.privateKey == rhs.privateKey &&
|
|
||||||
lhsAddresses == rhsAddresses &&
|
|
||||||
lhs.listenPort == rhs.listenPort &&
|
|
||||||
lhs.mtu == rhs.mtu &&
|
|
||||||
lhs.dns == rhs.dns &&
|
|
||||||
lhs.dnsSearch == rhs.dnsSearch
|
|
||||||
}
|
|
||||||
}
|
|
||||||