YPtun — клиент, который сам поднимает себе сервер
- суббота, 31 октября 2026 г. в 00:00:13
Обычно свой сервер выглядит так: купил VPS, зашёл по SSH, открыл README, скопировал curl | bash, потом полчаса выясняешь, почему не открывается порт, а в конце переписываешь ключи из терминала в клиент. Мы решили, что раз клиент и так знает, какие параметры нужны серверу, пусть он его сам и ставит. Вводишь IP и пароль от VPS, жмёшь кнопку, через минуту поля подключения уже заполнены.
Ниже о том, как это устроено, и о том, сколько раз оно у нас ломалось.
Это вторая статья про YPtun. Первая, обзорная, здесь: YPtun: один клиент вместо десятка.
Часть движков в YPtun работает только со своим сервером, подписку от провайдера к ним не подключишь. Сейчас их семь: VK‑TURN (WDTT Plus) и freeturn гонят WireGuard через TURN‑релеи, MasterDNS туннелирует трафик через DNS‑запросы, olcRTC ходит поверх сервисов видеосвязи, OpenFlux это исследовательский туннель со сменными транспортами, snolc модульный движок на Rust с Noise, и ещё CSQTT.
У каждого свой сервер, своя инструкция и свои сюрпризы. Человек, который разобрался с VLESS, совершенно не обязан знать, как собирается сервер MasterDNS и почему после установки он молчит. Поэтому и появилась кнопка.

Добавляете локацию нужного типа, например MasterDNS. На экране сервера первой стоит карточка «Установить на VPS». Вводите IP, логин и пароль. Можно вместо пароля приватный SSH‑ключ, в том числе с парольной фразой. Жмёте кнопку и смотрите журнал установки вживую. Когда всё закончится, клиент сам впишет в локацию ключи, порты и домен, останется только подключиться.
Так ставится сервер для любого из семи движков, о которых шла речь выше: VK‑TURN, freeturn, MasterDNS, olcRTC, OpenFlux, snolc и CSQTT. Никакой разницы в том, как это выглядит, нет: та же карточка, те же поля, тот же журнал. Внутри у каждого свой скрипт, который знает, как поднять именно этот сервер, и свои собранные бинарники под x86_64 и ARM64, так что подойдёт почти любой VPS. Можно взять один сервер и поставить на него сразу несколько движков, если они не спорят за один порт. А если спорят, установщик скажет, кто этот порт занял, а VK‑TURN в таком случае сам переедет на соседний свободный.
Работает на Android и на ПК (Windows и Linux). На ПК ключ можно просто выбрать файлом.
На iOS приложение тоже есть, пока в бете, но автоустановки там нет. Причина скучная: установщик написан на JVM‑коде с JSch, а на iPhone JVM нет, там Kotlin/Native, и этот код туда просто не переносится. Так что если у вас iPhone, поставьте сервер с Android или с компьютера, а потом нажмите «Поделиться» у локации и откройте ссылку на телефоне. Все поля подтянутся сами.
В комментариях к первой статье нас спросили, где вообще в приложении этот установщик. Вопрос по делу. Раньше автоустановка была выключена по умолчанию, и включать её надо было тумблером где‑то глубоко в настройках. Нам казалось, что залить бинарник на сервер под root достаточно серьёзное действие, чтобы человек включил его осознанно. На деле получилось, что самую полезную функцию просто никто не видел.
В новой версии мы переделали дизайн, и установщик переехал на видное место. Карточка «Установить на VPS» теперь первая на экране сервера у всех движков, которым нужен свой сервер. Ниже идут «Как подключаться» (тип, маршрутизация, ядро) и «Дополнительная защита» с каскадом и WARP, а пинг показывается прямо под именем.
Заодно разобрали настройки. Раньше это был один длинный список, теперь три группы: «Подключение», «Приложение» и «Ещё». Ничего при этом не удалили, просто разложили по местам. Экран добавления стал проще: поле для ссылки с кнопкой «Вставить» и подсказкой, что именно распознано, рядом QR и файл. Если подписки нет, там же бесплатные серверы и создание вручную, где типы подписаны нормальными словами. У подписки вместо всплывающего окна теперь свой экран с трафиком, сроком и ссылкой.
Ну и внешне: скругления помягче, графитовые цвета, никаких серых рамок, круглая кнопка подключения. На Android, iOS и ПК всё выглядит одинаково.

Если коротко, это SSH‑клиент (JSch, форк от mwiede) и один shell‑скрипт.

Сначала клиент спрашивает у сервера uname -m, чтобы понять, x86_64 там или aarch64. Под обе архитектуры собранные серверы лежат прямо в приложении. Нужный бинарник в gzip заливается в /tmp, после чего мы сравниваем sha256sum на сервере с хешем у себя. Если sha256sum на сервере нет, сравниваем хотя бы размер.
Потом одной командой запускается установочный скрипт. Он останавливает старую установку, если она была, и проверяет, свободен ли порт. Кладёт бинарник в /usr/local/bin, пишет конфиг и генерирует ключ шифрования, причём только если ключа ещё нет, чтобы после переустановки не отвалились уже настроенные клиенты. Дальше systemd‑юнит с Restart=always, открытие порта в файрволе (ufw, firewalld или голый iptables, что найдётся), перезапуск службы и проверка, что сокет правда слушает.
В конце скрипт печатает ключ строкой вида MASTERDNS_KEY=..., приложение его вытаскивает и подставляет в локацию.
На бумаге всё просто. Дальше про то, где мы спотыкались.
Первая версия была сделана как в учебнике: одно SSH‑соединение, на нём SFTP для файла и exec для команд. У всех работало. А у одного из нас на VPS соединение обрывалось ровно в момент, когда открывался второй канал. uname -m проходил, дальше всё падало.
Что именно там так себя ведёт, мы до конца не поняли. Зато нашли способ, который работал всегда: одно новое соединение на одну маленькую команду. Файл в таком режиме едет через base64 кусками по 100 КБ, и каждый кусок это отдельный printf '...' | base64 -d >> файл в своём соединении.
Почему именно 100 КБ. Команда уходит на сервер одним аргументом sh -c, а Linux ограничивает длину одного аргумента 128 КБ (MAX_ARG_STRLEN). Отрежешь больше, и команда просто не запустится.
Какое‑то время только так и заливали, для всех. Потом посчитали: бинарник на 20 МБ это примерно 270 SSH‑логинов и несколько минут ожидания. Многовато ради одного капризного сервера. Сейчас сначала пробуем обычный SFTP одним файлом, а кусками льём, только если он не сработал. Хеш в обоих случаях сверяем. Раньше недолитый файл доходил до gunzip битым, и по ошибке было вообще непонятно, что случилось.
Это был самый неприятный баг. Установщик пишет, что всё готово, а сервер не отвечает.
Оказалось, на сервере остался старый dnstt и держал тот же UDP‑порт. Новый MasterDNS при старте падал, systemd его перезапускал, он снова падал, и так по кругу. А мы проверяли systemctl is-active, который между перезапусками честно отвечает active.
Вторая половина проблемы: systemctl enable --now не перезапускает службу, которая уже работает. Так что если она была запущена раньше, новый конфиг просто не применялся. Тоже молча.
Теперь установщик сначала останавливает свою старую службу и проверяет порт через ss. Если порт занят, он пишет, каким процессом и какой службой, и предлагает «Освободить порт». Вместо enable --now делается обычный restart. А успехом считается только сокет, который реально слушает: ss -Hulpn "sport = :ПОРТ" | grep masterdns. Если его нет, в журнал установки выводятся последние строки journalctl.
С VK‑TURN была похожая история. Старый WDTT сидел на порту 56 000 под службой с нестандартным именем, а freeturn по умолчанию слушает тот же 56000. Теперь установщик находит старый WDTT по процессу на порту, как бы его служба ни называлась, и удаляет. А если порт занят чем‑то другим, ставит сервер на следующий свободный порт и сам меняет его в настройках локации.
По паролю всё входило, а по ключу ed25519, который сейчас у всех самый обычный, Android выдавал Auth fail. В журнале JSch нашлось ssh-ed25519 is not available и curve25519-sha256 is not available.
Дело в том, что JSch выбирает реализацию Ed25519, curve25519 и chacha20 по версии Java. Android сообщает версию ниже 15, и тогда JSch идёт за ними в Bouncy Castle, которого в приложении не было. В итоге ключ серверу даже не предлагался.
Добавили Bouncy Castle и явно переключили JSch на его реализации на всех платформах. На ПК это тоже пригодилось, потому что в урезанной JRE портативной версии этих алгоритмов могло не быть.
Классика: в терминале пароль подходит, а в приложении нет. Чаще всего причин две.
Первая: лишний пробел или перевод строки. Пароль вставляют из менеджера паролей, а в поле одни точки, ничего не видно. Теперь пробелы по краям обрезаются, а если от этого изменилась длина, в журнале будет об этом строчка.
Вторая: сервер принимает пароль только через keyboard‑interactive. На многих VPS обычный метод password выключен, и пароль спрашивает PAM, иногда в несколько заходов. Мы пробуем оба метода и отвечаем паролем на каждый заход, но не больше четырёх раз, чтобы с неверным паролем всё равно получить внятную ошибку.
Если войти так и не получилось, в ошибке написано, что проверить по порядку: PermitRootLogin, PasswordAuthentication, KbdInteractiveAuthentication. А выше в журнале видно, какие способы входа сервер вообще предложил.
Серверные бинарники лежат в ресурсах приложения сжатыми, например masterdns-server-linux-amd64.gz. Только при сборке APK Android распаковывает .gz‑ресурсы и хранит их уже без расширения, так что во время работы файла с .gz просто нет.
Теперь загрузчик ищет файл сначала без расширения, потом с .gz. Если нашёлся несжатый, приложение сожмёт его само: так заливка остаётся быстрой, а gunzip на сервере работает как раньше.
Сначала установщик работал только на Android. На ПК вместо него стояли заглушки с текстом «доступно только в Android‑приложении», хотя по сути это обычный JVM‑код, JSch плюс shell‑скрипт.
Мы вынесли его в отдельный набор исходников jvmSharedMain, общий для Android и десктопа. Копия кода теперь одна, отличается только то, откуда брать бинарники: на Android из assets, на ПК из ресурсов.
Установщик заходит на сервер под root, ставит службы и открывает порты, так что запускать его лучше на чистом VPS, который вы держите под это. Ключ хоста при первом подключении принимается автоматически: у свежего VPS его неоткуда знать заранее, но имейте это в виду. Всё, что подставляется в скрипт (домен, пароль сервера, адреса резолверов), экранируется или фильтруется, чтобы через эти поля нельзя было выбраться из кавычек. Код открыт, установщики лежат в sharedUI/src/jvmSharedMain, скрипты можно прочитать до того, как что‑то запускать.
Следующая статья будет про MasterDNS, туннель на тот случай, когда из сети наружу выпускают только DNS. Ставится он той же кнопкой.
GitHub: github.com/yanisplugg/yptun
Telegram: t.me/yptun