feat(cli): файл настроек tglock.toml (#35)

* fix(bundle): спрятать CLI за фичей — beta.2 и beta.3 упаковывали не тот бинарь

Мой предыдущий фикс (mainBinaryName в #31) фиксом не был. Он изменил только
имя выходного файла: бандлер по-прежнему брал headless CLI и просто
переименовывал его в tglock. Проверено по содержимому опубликованных
артефактов, а не по имени:

  версия  файл в MacOS/  размер  признаки CLI       признаки GUI
  beta.1  tglock         17.8 МБ  нет               ipc.localhost, wry×3206
  beta.2  tglock-cli      4.3 МБ  tglock-cli×6      нет
  beta.3  tglock          4.3 МБ  tglock-cli×6      нет

То есть beta.3 тоже не запускается, и моя же проверка CFBundleExecutable это
пропустила, потому что сверяла имя.

Настоящая причина найдена воспроизведением локально. Ломается только при явном
--target: без него бандлер выбирает GUI, с ним — CLI. Локальная сборка, на
которой я объявил фикс подтверждённым, шла без --target, а обе сборки в CI — с
ним. Обобщение было неправомерным.

Исправление убирает саму возможность выбора: CLI спрятан за фичей cli, которой
нет в default. При сборке приложения второго бинаря просто не существует.

Проверено против воспроизведённой поломки:
  до : tglock.exe 1.7 МБ, признаки CLI, GUI нет
  после: tglock.exe 9.0 МБ, признаки GUI, CLI нет, tglock-cli.exe не собран

Проверки переписаны на содержимое:
- release.yml распаковывает .app и .deb и ищет ipc.localhost (есть только в
  GUI) и allow-direct (есть только в CLI). Поймала бы и beta.2, и beta.3;
- новая задача CI bundle собирает бандл с явным --target aarch64-apple-darwin
  и проверяет его так же. Ловит до публикации, а не после;
- заодно исправлен шаблон grep для .deb: dpkg-deb -c выводит путь без ./, из-за
  чего проверка ложно падала на исправной сборке.

Команда сборки CLI теперь требует --features cli; обновлены README, ci.yml и
release.yml.

* fix(ci): одна проверка бандла на Python вместо трёх копий grep

Проверка содержимого бандла, добавленная в #33, работала на Linux и давала
ложное «в бандле не GUI» на macOS. Причина в BSD grep: в UTF-8-локали он молча
не находит строки в бинарных данных там, где GNU grep находит. Из-за этого
задача macOS в релизе beta.4 упала уже после загрузки артефактов, а вместе с
ней снова пропустилась задача с CLI-бинарями.

Сами артефакты beta.4 при этом корректны — проверено скачиванием: в бандле
tglock на 20.3 МБ, universal/fat, признаки GUI на месте, признаков CLI нет.
Не хватает только tglock-cli-*.

Проверка вынесена в scripts/verify_bundle_binary.py и вызывается из всех трёх
мест: задачи CI bundle, проверки macOS и проверки Linux в релизе. Один скрипт
вместо трёх копий шелл-кода исключает и платформенные различия grep, и
расхождение копий между собой.

Скрипт проверен на реальных исторических артефактах:
  beta.4 (исправный)  → код 0
  beta.3 (сломанный)  → код 1, найдены allow-direct и secret-file
  beta.2 (сломанный)  → код 1, то же

* feat(cli): файл настроек tglock.toml

Запрошено в #32: держать все параметры и секрет в одном месте, чтобы не
собирать батник с ключами при каждом запуске.

Файл ищется рядом с бинарём под именем tglock.toml — как и просили в ишью, —
либо указывается через --config. Приоритеты: значения по умолчанию → файл →
флаги. Флаги-переключатели могут только включать: отсутствие --quiet не
отменяет quiet = true из файла, иначе файлом нельзя было бы ничего включить.

Секрет можно вписать прямо в конфиг, в том числе в форме с префиксом dd — то
есть скопировав из напечатанной ссылки tg://proxy. Отдельный secret_file
остаётся, inline-секрет важнее.

deny_unknown_fields намеренно: опечатка вроде porrt = 1443 останавливает старт
с перечислением допустимых полей. Сервис, который из-за опечатки слушает 1080
вместо 1443, хуже сервиса, который не запустился. Явный --config к
несуществующему файлу тоже ошибка, а не тихий откат к настройкам по умолчанию.

Если секрет не закреплён ни одним из способов, CLI печатает предупреждение:
после перезапуска ссылка станет другой и настроенные клиенты отвалятся.

Найдено при живом прогоне и исправлено здесь же: демон падал, если stdout
закрывался. println! при ошибке записи паникует, а канал закрывается штатно —
`| head`, закрытый терминал, перезапуск сборщика логов. Воспроизводилось
одной командой:

  tglock-cli --port 18101 | head -2
  thread 'tokio-rt-worker' panicked at stdio.rs: failed printing to stdout

Печать переведена на say(), который возвращает признак успеха; наблюдатель
статуса при закрытом stdout просто прекращает печатать, туннель продолжает
работать.

Тестов в CLI стало 26 вместо 12: приоритеты, конфликт lan и bind из разных
источников, обе формы записи секрета, отказ на опечатке и на битых значениях,
комментарии в файле. Плюс parse_secret в ядре с проверкой того, что секрет,
сам начинающийся с dd, не теряет первый байт.

В CI добавлены три шага: чтение настроек из файла, перекрытие флагом и отказ
на опечатке; отдельно — проверка, что закрытый stdout не роняет демон.

Документация: секция про файл настроек в README, полный пример с пояснениями в
tglock.example.toml, юнит systemd переведён на --config.

* fix: вернуть плоский путь бинаря, модуль настроек — в библиотеку

Проверка бандла в CI упала на этом PR:

  failed to rename app binary .../release/cli: No such file or directory

Причина моя. Я перенёс CLI в каталожную форму src/bin/cli/main.rs, и
перечисление бинарей в Tauri вывело имя приложения из имени каталога —
получилось "cli", которого не существует. Явное name = "tglock-cli" в
[[bin]] при этом игнорируется. До переноса, при плоском src/bin/cli.rs,
всё собиралось.

Путь бинаря вернулся к src/bin/cli.rs, а модуль настроек переехал в
библиотеку как cli_settings под фичей cli. Ему там и место: он работает с
ListenConfig, mtproto и proxy, а clap не использует вовсе.

Проверено воспроизведением того же условия локально — сборка бандла с явным
--target: приложение собирается, скрипт проверки подтверждает GUI-бинарь
(9.0 МБ, ipc.localhost и wry на месте, признаков CLI нет).

Тестов: 66 в библиотеке с фичей cli, 12 в бинаре, clippy чист в обоих
вариантах сборки. Живой прогон с конфигом и отказ на опечатке сохранились.

---------

Co-authored-by: by-sonic <171230345+by-sonic@users.noreply.github.com>
This commit is contained in:
Никита Sonic
2026-07-30 20:19:31 +03:00
committed by GitHub
parent 4ae755c6ae
commit 9429975403
9 changed files with 643 additions and 66 deletions
+29 -2
View File
@@ -173,6 +173,30 @@ tglock-cli --worker my-name.workers.dev # резервный маршрут,
tglock-cli --help # все флаги
```
#### Файл настроек
Чтобы не собирать батник с ключами при каждом запуске ([#32](https://github.com/by-sonic/tglock/issues/32)), положите рядом с бинарём `tglock.toml` — он подхватится сам. Или укажите путь: `tglock-cli --config /etc/tglock.toml`.
```toml
port = 1080
lan = true
# Секрет — вторая половина ссылки tg://proxy. Для сервиса его нужно закрепить,
# иначе после перезапуска все настроенные клиенты отвалятся. Запустите один
# раз, скопируйте из напечатанной ссылки и вставьте сюда.
secret = "dd00112233445566778899aabbccddeeff"
worker = ["my-name.workers.dev"]
```
Полный пример со всеми полями и пояснениями — [`tglock.example.toml`](tglock.example.toml).
Приоритеты: значения по умолчанию → файл → флаги. Флаг всегда сильнее файла. Флаги-переключатели (`--lan`, `--allow-direct`, `--quiet`) могут только включать: их отсутствие означает «взять из файла», а не «выключить».
Опечатка в файле — ошибка при старте с перечислением допустимых полей, а не молча проигнорированная строка. Сервис, который из-за `porrt = 1443` слушает 1080, хуже сервиса, который не запустился.
Файл с секретом внутри держите с правами `600`: это доступ к вашему прокси.
При запуске печатается готовая `tg://proxy`-ссылка — её можно открыть на любом устройстве в сети, чтобы Telegram настроился сам. Дальше в лог идёт по строке на каждое изменение состояния: сколько соединений, какой дата-центр, какой маршрут живой, сколько сбоев.
Прав администратора не нужно: TGLock не правит ни системный DNS, ни файл `hosts` — нужные адреса Telegram зашиты в маршрутах, а TLS SNI остаётся настоящим.
@@ -189,7 +213,7 @@ Wants=network-online.target
[Service]
Type=exec
ExecStart=/usr/local/bin/tglock-cli --lan --secret-file /var/lib/tglock/secret
ExecStart=/usr/local/bin/tglock-cli --config /etc/tglock.toml
Restart=on-failure
RestartSec=5s
StateDirectory=tglock
@@ -206,11 +230,14 @@ WantedBy=multi-user.target
```bash
sudo install -m755 tglock-cli-x86_64-unknown-linux-gnu /usr/local/bin/tglock-cli
sudo install -m600 tglock.example.toml /etc/tglock.toml # и отредактировать
sudo systemctl enable --now tglock
journalctl -u tglock -f
```
`--secret-file` здесь обязателен, и это не украшение: секрет — половина `tg://proxy`-ссылки. Без файла он генерируется заново при каждом старте, и после первого же `systemctl restart` все настроенные клиенты перестанут подключаться. `StateDirectory=tglock` создаёт `/var/lib/tglock` с нужными правами, а сам файл пишется с режимом `600`.
**Закреплённый секрет здесь обязателен**, и это не украшение: секрет — половина `tg://proxy`-ссылки. Без него он генерируется заново при каждом старте, и после первого же `systemctl restart` все настроенные клиенты перестанут подключаться. Задайте `secret` в `/etc/tglock.toml` (файл с правами `600`) либо `secret_file = "/var/lib/tglock/secret"` — тогда `StateDirectory=tglock` создаст каталог с нужными правами, а файл запишется с режимом `600`.
При старте без закреплённого секрета CLI печатает предупреждение — молча уйти в такое состояние он не даст.
Остановка по `systemctl stop` приходит как `SIGTERM` — CLI закрывает туннели и выходит с нулевым кодом, а не умирает по `SIGKILL`.