Files
lemana-vpn/README.md

508 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lemana VPN
CLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.
**Модули по умолчанию:** Core: включён; Bitwarden: включён; Touch ID: включён; DNS cleanup: включён; Swift Menu Bar app: включён; автозапуск приложения: включён; runtime-патчи: применяются автоматически перед подключением.
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
- `openconnect` как VPN-клиент;
- `openconnect-lite` для SAML SSO через Keycloak;
- опциональный Bitwarden CLI для LDAP-пароля и TOTP seed;
- опциональный Touch ID helper для мастер-пароля Bitwarden;
- Swift Menu Bar app `LemanaVPN.app`;
- безопасный DNS cleanup через root-owned wrapper;
- алиасы `vpn`, `vpn-auto`, `vpn-manual`, `vpn-debug`, `vpn-fix-dns`.
## Быстрая установка
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
```
Если установка запущена из терминала, скрипт сначала проверит, что уже стоит, и спросит по отсутствующим опциональным модулям.
После установки открой новый shell или выполни:
```sh
exec zsh
vpn
```
## Варианты установки
Полная установка, режим по умолчанию:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
```
Без Touch ID, но с Bitwarden:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --without-touchid
```
Минимальная установка без Bitwarden и Touch ID. В macOS Keychain вручную будут записаны LDAP-пароль и TOTP secret. Не текущий 30-секундный TOTP-код, а постоянный seed из настройки 2FA.
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --minimal --configure-keychain
```
Проверить действия без изменений:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --dry-run
```
Принудительно включить интерактивные вопросы:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --interactive
```
Запустить без вопросов, с выбранными флагами и дефолтами:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --non-interactive
```
Если raw URL отличается, переопредели базовый адрес:
```sh
curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \
| LEMANA_VPN_RAW_BASE_URL=https://example.org/dokril/lemana-vpn/raw/branch/main sh
```
## Что ставится
| Путь | Назначение |
| --- | --- |
| `~/bin/vpn-lemanapro.sh` | Основной CLI для подключения, статуса и sync секретов |
| `~/bin/uninstall-lemana-vpn.sh` | Локальный uninstall helper |
| `~/bin/keychain-fingerprint` | Опциональный Touch ID helper для мастер-пароля Bitwarden |
| `~/Applications/LemanaVPN.app` | Swift Menu Bar app для подключения из status bar |
| `~/Library/LaunchAgents/ru.dokops.LemanaVPN.plist` | Автозапуск Menu Bar app при логине |
| `~/.config/lemana-vpn/env` | Локальная конфигурация модулей |
| `~/.config/lemana-vpn/patch-backups/` | Backup исходника `openconnect-lite` перед runtime-патчами |
| `~/.config/openconnect-lite/config.toml` | Профиль SSO и auto-fill правила Keycloak |
| `/usr/local/sbin/lemana-vpn-dns-cleanup` | Root-owned wrapper для сброса только корпоративных DNS |
| `/etc/sudoers.d/lemana-vpn-openconnect` | `NOPASSWD` только для `openconnect` |
| `/etc/sudoers.d/lemana-vpn-dns` | `NOPASSWD` только для DNS cleanup wrapper |
| `~/.zshrc` | Идемпотентный блок алиасов `vpn`, `vpn-auto`, `vpn-manual`, `vpn-debug`, `vpn-fix-dns` |
## Статус модулей
`vpn` и `vpn --status` первой строкой показывают, какие модули включены в конфиге и реально установлены на машине:
```sh
vpn --status
Modules: ✅ core=ok, ✅ bitwarden=on, ✅ touchid=on, ✅ dns=on, ✅ app=on, ✅ autostart=on, ✅ patches=active, ✅ keychain=password:yes/totp_seed:yes
VPN disconnected
```
Emoji в human-выводе помогают быстро отличать норму, отключённый опциональный модуль и проблему:
- `✅` — модуль установлен или состояние готово;
- `⏭️` — модуль осознанно отключён;
- `⚠️` — модуль включён, но чего-то не хватает.
Значения:
| Поле | Значение |
| --- | --- |
| `core=ok` | Есть `openconnect`, `openconnect-lite` и config |
| `bitwarden=on` | Модуль включён и `bw` установлен |
| `bitwarden=off` | Модуль отключён через `--without-bitwarden` или `LEMANA_VPN_USE_BITWARDEN=0` |
| `bitwarden=missing` | Модуль включён, но `bw` не найден |
| `touchid=on/off/missing` | Состояние Touch ID helper |
| `dns=on/missing` | Наличие DNS cleanup wrapper |
| `app=on/missing` | Установлен ли `~/Applications/LemanaVPN.app` |
| `autostart=on/off` | Есть ли LaunchAgent для запуска приложения при логине |
| `patches=active/pending` | Применены ли runtime-патчи `openconnect-lite` |
| `keychain=password:yes/totp_seed:yes` | Есть ли LDAP-пароль и TOTP seed в Keychain |
JSON-режим тоже отдаёт модульный статус:
```sh
vpn --status --json
```
## Интерактивная установка
Перед установкой `install.sh` печатает текущее состояние:
```text
Detected state:
openconnect: yes
pipx: yes
openconnect-lite: yes
Bitwarden CLI: no
Touch ID helper: no
DNS cleanup: no
sudoers: no/no
shell aliases: no
Swift: yes
Menu Bar app: no
LaunchAgent: no
Keychain password: no
Keychain TOTP seed: no
```
Если доступен терминал (`/dev/tty`), скрипт спросит только по тому, чего не хватает:
- поставить ли Bitwarden CLI, если `bw` не найден;
- собрать ли Touch ID helper, если его нет и Bitwarden включён;
- собрать ли Swift Menu Bar app, если `~/Applications/LemanaVPN.app` не найден;
- включить ли автозапуск Menu Bar app при логине;
- настроить ли sudoers для `openconnect` и DNS cleanup;
- добавить ли алиасы в `~/.zshrc`;
- записать ли LDAP-пароль и TOTP seed в Keychain, если Bitwarden отключён.
Флаги имеют приоритет над вопросами. Например, `--without-bitwarden` не будет спрашивать про Bitwarden, а `--no-shell` не будет предлагать алиасы.
В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`.
## Логи установщика и удаления
Установщик и uninstall script печатают пошаговый лог с emoji, цветом в интерактивном терминале и коротким пояснением, зачем нужен каждый шаг. Например, перед сборкой Swift-приложения установщик отдельно пишет, что `swift build` может занять время и что строки компилятора вида `[2/5] Write swift-version...` являются нормальным выводом. При удалении отдельно показывается откат runtime-патчей `openconnect-lite`, удаление sudoers/DNS wrapper, приложения, aliases и config.
Отключить цвет:
```sh
NO_COLOR=1 sh install.sh
NO_COLOR=1 sh uninstall.sh
```
Отключить emoji:
```sh
LEMANA_VPN_NO_EMOJI=1 sh install.sh
LEMANA_VPN_NO_EMOJI=1 sh uninstall.sh
```
## Модули
### Core
Всегда устанавливается:
- `openconnect` через Homebrew;
- `pipx` через Homebrew;
- `openconnect-lite` через `pipx`;
- CLI `vpn-lemanapro.sh`;
- `openconnect-lite` config;
- DNS cleanup wrapper.
### Bitwarden
Включён по умолчанию. CLI при каждом запуске `vpn` пытается получить LDAP-пароль и TOTP seed из записи Bitwarden `LM LDAP`, затем записывает их в macOS Keychain для `openconnect-lite`.
TOTP seed — это постоянный секрет 2FA. Сам одноразовый TOTP-код меняется каждые 30 секунд и генерируется `openconnect-lite` в момент входа.
Если vault заблокирован и Touch ID helper не смог его открыть, CLI спросит `Bitwarden master password`. Это пароль от хранилища Bitwarden, а не корпоративный LDAP-пароль. Он нужен только чтобы достать LDAP password/TOTP seed из item `LM LDAP` и переложить их в macOS Keychain.
Отключить:
```sh
sh install.sh --without-bitwarden
```
В этом режиме credentials нужно положить в Keychain вручную:
```sh
vpn-lemanapro.sh --configure-keychain
```
Если credentials уже лежат в Keychain, подключение без Bitwarden не будет спрашивать пароль заново. CLI явно напишет, что Bitwarden отключён и используются сохранённые LDAP password/TOTP seed из macOS Keychain.
### Если Bitwarden нет
Bitwarden не обязателен. Без него установка работает как обычный `openconnect-lite` profile с секретами в macOS Keychain.
Установка:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
| sh -s -- --without-bitwarden --without-touchid --configure-keychain
```
Что понадобится:
- LDAP username;
- LDAP password: корпоративный LDAP/domain пароль, не мастер-пароль Bitwarden;
- TOTP secret из корпоративной 2FA настройки.
Важно: вводить нужно не текущие 6 цифр из authenticator-приложения, а постоянный secret. Обычно он есть в QR-коде как `secret=BASE32...` или может быть показан при ручной настройке TOTP.
Если запуск идёт из `LemanaVPN.app`, приложение не может безопасно показать интерактивный terminal prompt для ввода LDAP/TOTP. Если Keychain пустой, приложение покажет ошибку. В этом случае один раз выполни в Terminal:
```sh
vpn --configure-keychain
```
Если secret есть только в QR-коде:
1. Открой QR-код в приложении/на портале, где настраивалась 2FA.
2. Найди режим ручной настройки, где показывается secret.
3. Если доступен только QR, его нужно расшифровать любым локальным QR-сканером и взять параметр `secret`.
4. Вставь secret в prompt `TOTP secret (BASE32...)`.
Если TOTP secret получить нельзя, автоматический headless-вход невозможен: `openconnect-lite` не сможет сам генерировать свежий TOTP-код на каждом входе.
### Touch ID
Включён по умолчанию. Установщик собирает `keychain-fingerprint` из `https://github.com/dss99911/keychain-fingerprint.git` и кладёт бинарник в `~/bin/keychain-fingerprint`.
Важно: этот helper показывает системный Touch ID prompt перед чтением мастер-пароля Bitwarden, но это не аппаратный Keychain ACL. Это удобный локальный гейт поверх записи Keychain.
Отключить:
```sh
sh install.sh --without-touchid
```
### Swift Menu Bar app
Включён по умолчанию. Установщик собирает Swift-приложение из исходников в репозитории и кладёт bundle в:
```sh
~/Applications/LemanaVPN.app
```
Приложение живёт в macOS status bar, запускает `~/bin/vpn-lemanapro.sh --json`, показывает состояние VPN, IP, оставшееся время сессии, health-check тоннеля и строку состояния модулей.
Строка состояния модулей в меню приложения использует те же маркеры, что CLI: `✅` для готового модуля, `⏭️` для отключённого опционального модуля и `⚠️` для проблемы. Иконка строки тоже меняется: `checkmark.circle` для полностью готового набора и `exclamationmark.triangle` для неполной установки.
Если в меню видно `modules unavailable: update CLI`, значит запущенное приложение обращается к старому `~/bin/vpn-lemanapro.sh`, который ещё не умеет отдавать модульный статус. Повтори установку через `curl`; установщик обновит CLI и перезапустит уже запущенное `LemanaVPN.app`.
Для сборки нужен Swift 5.9+ из Xcode Command Line Tools:
```sh
xcode-select --install
```
Отключить установку приложения:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --without-app
```
Оставить приложение, но отключить автозапуск:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --without-autostart
```
Ручной запуск:
```sh
open ~/Applications/LemanaVPN.app
```
## Использование
```sh
vpn # подключиться
vpn-auto # автоматический режим: скрытый браузер, auto-fill и submit
vpn-manual # ручной режим: видимый браузер, auto-fill без submit
vpn --manual # то же самое без alias
vpn --status # статус без нового подключения
vpn --status --json # статус в JSON
vpn-debug # видимый браузер и debug-логи
vpn --manual --debug # ручной режим с debug-логами
vpn-fix-dns # сбросить корпоративные DNS после аварийного завершения
open ~/Applications/LemanaVPN.app # открыть Swift-приложение в menu bar
```
Режимы подключения:
- `auto` — режим по умолчанию. Браузер скрытый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain, Keycloak форма заполняется и отправляется автоматически.
- `manual` — браузер видимый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain и подставляются в поля, но кнопки входа не нажимаются. Нажимаешь сам после проверки формы.
- `--manual-sso` оставлен как совместимый alias для `--manual`.
Первый запуск с Bitwarden:
1. CLI проверит `bw`.
2. Если vault locked, попросит мастер-пароль.
3. Если установлен Touch ID helper, предложит сохранить мастер-пароль за Touch ID prompt.
4. Достанет `LM LDAP`, запишет LDAP-пароль и TOTP seed в Keychain.
5. Запустит `openconnect-lite` и пройдёт Keycloak SSO.
## Настройка
Файл `~/.config/lemana-vpn/env`:
```sh
LEMANA_VPN_USERNAME="60103293"
LEMANA_VPN_BW_ITEM="LM LDAP"
LEMANA_VPN_USE_BITWARDEN="1"
LEMANA_VPN_USE_TOUCHID="1"
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
```
Для другого логина:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
| sh -s -- --username 12345678
```
## Bitwarden item
Нужна запись:
- название: `LM LDAP`;
- username: корпоративный LDAP логин;
- password: LDAP пароль;
- TOTP: `otpauth://...secret=BASE32...` или raw BASE32 secret.
Это не 6-значный одноразовый код. В Bitwarden должен лежать постоянный TOTP secret, из которого коды генерируются автоматически.
## Почему DNS wrapper, а не wildcard sudoers
Старый вариант давал `NOPASSWD` на `networksetup -setdnsservers *`. Это слишком широкое право: любой локальный процесс пользователя мог поменять DNS на произвольный сервер.
Новый вариант разрешает sudo только на `/usr/local/sbin/lemana-vpn-dns-cleanup`. Wrapper сбрасывает DNS только если текущий DNS начинается с `10.`, то есть похож на корпоративный VPN DNS.
## Runtime-патчи openconnect-lite
`openconnect-lite` работает, но для текущей macOS + Keycloak SSO цепочки ему нужны runtime-патчи. CLI применяет их перед подключением в файле:
```sh
~/.local/pipx/venvs/openconnect-lite/lib/python*/site-packages/openconnect_lite/browser/webengine_process.py
```
Патчи:
| Патч | Что меняет | Зачем |
| --- | --- | --- |
| `minimal -> offscreen` | Меняет Qt platform mode для скрытого браузера | `minimal` падает с Qt WebEngine на macOS |
| `input/change events` | Оставляет старое прямое `value = ...`, но после него отправляет DOM events | Keycloak не реагирует на прямую запись value без событий |
| `legacy auto-fill` | Сохраняет старую рабочую схему `ApplicationWorld`, прямой `value = ...` и простой `click()` | Это ровно тот режим, на котором hidden SSO раньше стабильно проходил Keycloak |
| `URL guard` | Проверяет `location.href` через `new RegExp(...)` перед auto-fill | Qt игнорирует `@include`, без guard auto-fill может кликнуть Cisco ACS и сломать SAML |
| `auth redirect` | Читает 302 с `vpn.lemanapro.ru` без автоматического follow-redirect | Python `requests` может падать на TLS reset при открытии `/` на `sslvpna/b`, хотя для SAML нужен только конечный host |
| `manual submit gate` | Позволяет отключить только auto-click через `LEMANA_VPN_AUTOFILL_CLICK=0` | Ручной режим видит заполненную форму, но сам решает, когда нажать вход |
| `manual SSO disable` | Позволяет полностью отключить auto-fill через `LEMANA_VPN_AUTOFILL_DISABLE=1` | Нужен для низкоуровневой диагностики без подстановки полей |
Перед первым изменением CLI сохраняет оригинальный файл:
```sh
~/.config/lemana-vpn/patch-backups/webengine_process.py.before-lemana-vpn
```
Откат патчей выполняет uninstall script. Если backup отсутствует, автоматического rollback нет: значит файл был уже патчен старой ручной установкой или `openconnect-lite` переустановили после backup.
## Диагностика
Проверить установку:
```sh
command -v vpn-lemanapro.sh
openconnect --version
~/.local/bin/openconnect-lite --help
sudo -n /usr/local/sbin/lemana-vpn-dns-cleanup
vpn --status
```
Обычный `vpn` теперь пишет путь к подробному логу `openconnect-lite`:
```sh
~/Library/Logs/LemanaVPN-openconnect-lite.log
```
Если после `Connecting to VPN (lemanapro)...` SSO завис или не видно, что происходит, смотри этот файл:
```sh
tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log
```
В обычном режиме CLI также печатает heartbeat `Still waiting for SSO/openconnect-lite...` до успешного подключения, чтобы было понятно, что процесс живой. В `vpn-debug` дополнительно показываются raw-логи и видимый браузер.
Если в логе повторяется один и тот же URL вида `employee.auth.lemanapro.ru/realms/employee/login-actions/authenticate`, значит hidden-браузер застрял на Keycloak до перехода в Cisco ACS. Сначала обнови и примени runtime-патчи без подключения:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
vpn-lemanapro.sh --patch-only
```
Если лог падает раньше браузера с `SSLEOFError` / `UNEXPECTED_EOF_WHILE_READING` на `sslvpna.lemanapro.ru` или `sslvpnb.lemanapro.ru`, это ломается этап определения конечного Cisco headend. Актуальный runtime-патч `auth redirect` не открывает `/` на `sslvpna/b`, а только берёт `Location` из 302 ответа `vpn.lemanapro.ru` и продолжает штатный SAML init через POST.
Если SSO ломается после обновления `openconnect-lite`, запусти:
```sh
vpn-debug
```
Если нужно самому посмотреть форму Keycloak, но оставить подстановку LDAP/TOTP:
```sh
vpn --manual
```
В этом режиме браузер видимый, `openconnect-lite` заполняет поля из Keychain/Bitwarden, но не нажимает submit. Для полной диагностики без подстановки можно отдельно выставить `LEMANA_VPN_AUTOFILL_DISABLE=1`.
Если установка падает на строке `install: /usr/local/sbin/...: No such file or directory`, значит на машине не было `/usr/local/sbin`. Актуальный `install.sh` создаёт эту директорию сам; достаточно повторить установку свежей командой `curl`.
CLI перед подключением патчит `openconnect-lite`:
- `minimal` -> `offscreen`, чтобы Qt WebEngine не падал на macOS;
- добавляет `input` и `change` events для Keycloak auto-fill, сохраняя старое прямое присваивание `value = ...`;
- оставляет auto-fill в старом `ApplicationWorld` и не добавляет stateful click guards/native setters;
- добавляет URL guard, чтобы auto-fill не кликал submit на Cisco ACS;
- добавляет auth redirect patch, чтобы Python не падал на TLS reset при follow-redirect к `sslvpna/b`;
- добавляет manual submit gate для видимой ручной диагностики с auto-fill, но без auto-submit.
## Удаление
Рекомендуемый способ:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/uninstall.sh | sh
```
Или локально:
```sh
uninstall-lemana-vpn.sh
```
Что делает uninstall:
- восстанавливает `openconnect-lite` из backup, если backup есть;
- удаляет `vpn-lemanapro.sh` и `uninstall-lemana-vpn.sh`;
- удаляет sudoers rules и DNS cleanup wrapper;
- удаляет блок `lemana-vpn` из `~/.zshrc`;
- удаляет `~/.config/openconnect-lite/config.toml`;
- останавливает уже запущенный процесс `LemanaVPN`, удаляет `~/Applications/LemanaVPN.app` и LaunchAgent автозапуска;
- удаляет `~/.config/lemana-vpn`, если не передан `--keep-config`.
Опциональные режимы:
```sh
uninstall-lemana-vpn.sh --dry-run
uninstall-lemana-vpn.sh --keep-config
uninstall-lemana-vpn.sh --keep-app
uninstall-lemana-vpn.sh --remove-keychain
uninstall-lemana-vpn.sh --remove-touchid-helper
uninstall-lemana-vpn.sh --remove-openconnect-lite
```
Ручной вариант, если нужен полный контроль:
```sh
rm -f ~/bin/vpn-lemanapro.sh ~/bin/uninstall-lemana-vpn.sh
rm -rf ~/.config/lemana-vpn
rm -f ~/.config/openconnect-lite/config.toml
sudo rm -f /usr/local/sbin/lemana-vpn-dns-cleanup
sudo rm -f /etc/sudoers.d/lemana-vpn-openconnect /etc/sudoers.d/lemana-vpn-dns
```
Из `~/.zshrc` удалить блок:
```sh
# >>> lemana-vpn
...
# <<< lemana-vpn
```