From f2d4f8e04b319ed5da20144b7a2c784f74a8f7d5 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Tue, 26 May 2026 14:18:38 +0300 Subject: [PATCH] Add fully manual VPN mode --- .agents/skills/lemana-vpn-operations/SKILL.md | 4 +- .../skills/lemana-vpn-sso-autofill/SKILL.md | 2 + .agents/skills/lemana-vpn-testing/SKILL.md | 6 +++ AGENTS.md | 4 +- README.md | 18 +++++-- app/Sources/LemanaVPN/LemanaVPNApp.swift | 4 ++ app/Sources/LemanaVPN/VPNManager.swift | 4 +- bin/vpn-lemanapro.sh | 14 ++++-- install.sh | 3 +- tests/smoke.sh | 48 +++++++++++++++++++ 10 files changed, 94 insertions(+), 13 deletions(-) diff --git a/.agents/skills/lemana-vpn-operations/SKILL.md b/.agents/skills/lemana-vpn-operations/SKILL.md index 01477e9..06989fe 100644 --- a/.agents/skills/lemana-vpn-operations/SKILL.md +++ b/.agents/skills/lemana-vpn-operations/SKILL.md @@ -12,7 +12,7 @@ This repo is a macOS VPN packaging layer around `openconnect`, `openconnect-lite ## System Map - `install.sh` installs/updates the whole package and restarts `LemanaVPN.app` only if it is already running. -- `bin/vpn-lemanapro.sh` is the runtime source for `vpn`, `vpn-auto`, `vpn-manual`, `vpn-debug`, `vpn-fix-dns`. +- `bin/vpn-lemanapro.sh` is the runtime source for `vpn`, `vpn-auto`, `vpn-manual`, `vpn-manual-full`, `vpn-debug`, `vpn-fix-dns`. - `app/Sources/LemanaVPN/VPNManager.swift` shells out to `~/bin/vpn-lemanapro.sh --json`; app state must stay compatible with CLI JSON events. - `templates/openconnect-lite-config.toml` holds Keycloak selectors and the VPN profile. - `uninstall.sh` must stop the running menu-bar app when removing the app, not only delete the bundle. @@ -26,7 +26,7 @@ vpn --status vpn --status --json ``` -Do not start another `vpn`, `vpn-auto`, `vpn-debug`, `vpn --manual`, or app connect if status says connected/connecting/reconnecting or if the previous connect attempt is still active. Inspect logs instead: +Do not start another `vpn`, `vpn-auto`, `vpn-debug`, `vpn --manual`, `vpn --manual-full`, or app connect if status says connected/connecting/reconnecting or if the previous connect attempt is still active. Inspect logs instead: ```sh tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log diff --git a/.agents/skills/lemana-vpn-sso-autofill/SKILL.md b/.agents/skills/lemana-vpn-sso-autofill/SKILL.md index fc3041a..d860f10 100644 --- a/.agents/skills/lemana-vpn-sso-autofill/SKILL.md +++ b/.agents/skills/lemana-vpn-sso-autofill/SKILL.md @@ -48,6 +48,7 @@ vpn-lemanapro.sh --patch-only - `vpn` / `vpn-auto`: hidden browser, autofill, auto-submit. - `vpn --manual` / `vpn-manual`: visible browser, autofill, no submit. +- `vpn --manual-full` / `vpn-manual-full`: visible browser, no autofill, no submit. - `vpn-debug`: visible browser and raw logs. When diagnosing SSO, use manual mode first. Do not repeatedly start automatic mode if a connection attempt is already in progress. @@ -66,6 +67,7 @@ If live behavior must be checked: ```sh vpn --status vpn --manual +vpn --manual-full tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log ``` diff --git a/.agents/skills/lemana-vpn-testing/SKILL.md b/.agents/skills/lemana-vpn-testing/SKILL.md index 3c6b3a0..bad0714 100644 --- a/.agents/skills/lemana-vpn-testing/SKILL.md +++ b/.agents/skills/lemana-vpn-testing/SKILL.md @@ -61,6 +61,12 @@ When SSO/autofill changed, prefer: vpn --manual ``` +Use the fully manual path when validating a no-autofill diagnosis path: + +```sh +vpn --manual-full +``` + Only use automatic mode after manual mode proves the form is filled correctly: ```sh diff --git a/AGENTS.md b/AGENTS.md index 4492a19..4f2fc11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,14 +30,14 @@ vpn --status --json Use `~/bin/vpn-lemanapro.sh` if aliases are not loaded. For repo-local code checks, `bin/vpn-lemanapro.sh --status` validates the source script, but it may not be the installed version used by the menu-bar app. -Do not run `vpn`, `vpn-auto`, `vpn-debug`, `vpn --manual`, or the Swift app connect action repeatedly when a connection is already connected, connecting, reconnecting, or when a live connect attempt is still running. Inspect status and logs instead: +Do not run `vpn`, `vpn-auto`, `vpn-debug`, `vpn --manual`, `vpn --manual-full`, or the Swift app connect action repeatedly when a connection is already connected, connecting, reconnecting, or when a live connect attempt is still running. Inspect status and logs instead: ```sh tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log tail -f ~/Library/Logs/LemanaVPN.log ``` -Use `vpn --manual` before `vpn`/`vpn-auto` when debugging SSO/autofill, because manual mode shows the browser, fills fields, and does not press submit. Use `vpn-debug` only when raw logs and a visible browser are needed. +Use `vpn --manual` before `vpn`/`vpn-auto` when debugging SSO/autofill, because manual mode shows the browser, fills fields, and does not press submit. Use `vpn --manual-full` when the form itself must be filled entirely by hand with no auto-fill. Use `vpn-debug` only when raw logs and a visible browser are needed. `vpn-lemanapro.sh --patch-only` is safe for applying runtime patches without starting a VPN session. diff --git a/README.md b/README.md index 82ad78c..af308bf 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ CLI-установка корпоративного VPN `vpn.lemanapro.ru` дл - опциональный 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`. +- алиасы `vpn`, `vpn-auto`, `vpn-manual`, `vpn-manual-full`, `vpn-debug`, `vpn-fix-dns`. ## Быстрая установка @@ -106,7 +106,7 @@ curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \ | `/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` | +| `~/.zshrc` | Идемпотентный блок алиасов `vpn`, `vpn-auto`, `vpn-manual`, `vpn-manual-full`, `vpn-debug`, `vpn-fix-dns` | ## Статус модулей @@ -355,7 +355,9 @@ open ~/Applications/LemanaVPN.app vpn # подключиться vpn-auto # автоматический режим: скрытый браузер, auto-fill и submit vpn-manual # ручной режим: видимый браузер, auto-fill без submit +vpn-manual-full # полностью ручной режим: видимый браузер без auto-fill и submit vpn --manual # то же самое без alias +vpn --manual-full # то же самое без alias vpn --status # статус без нового подключения vpn --status --json # статус в JSON vpn-debug # видимый браузер и debug-логи @@ -368,7 +370,9 @@ open ~/Applications/LemanaVPN.app # открыть Swift-приложение - `auto` — режим по умолчанию. Браузер скрытый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain, Keycloak форма заполняется и отправляется автоматически. - `manual` — браузер видимый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain и подставляются в поля, но кнопки входа не нажимаются. Нажимаешь сам после проверки формы. +- `manual-full` — браузер видимый, auto-fill отключён полностью: поля Keycloak заполняешь и отправляешь сам. - `--manual-sso` оставлен как совместимый alias для `--manual`. +- `--manual-no-autofill` оставлен как совместимый alias для `--manual-full`. Первый запуск с Bitwarden: @@ -443,7 +447,7 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \ | `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` | Нужен для низкоуровневой диагностики без подстановки полей | +| `manual SSO disable` | Позволяет полностью отключить auto-fill через `vpn --manual-full` или `LEMANA_VPN_AUTOFILL_DISABLE=1` | Нужен для низкоуровневой диагностики без подстановки полей | Перед первым изменением CLI сохраняет оригинальный файл: @@ -500,7 +504,13 @@ vpn-debug vpn --manual ``` -В этом режиме браузер видимый, `openconnect-lite` заполняет поля из Keychain/Bitwarden, но не нажимает submit. Для полной диагностики без подстановки можно отдельно выставить `LEMANA_VPN_AUTOFILL_DISABLE=1`. +В этом режиме браузер видимый, `openconnect-lite` заполняет поля из Keychain/Bitwarden, но не нажимает submit. + +Если нужно проверить SSO полностью вручную, без подстановки LDAP-пароля и TOTP: + +```sh +vpn --manual-full +``` Если установка падает на строке `install: /usr/local/sbin/...: No such file or directory`, значит на машине не было `/usr/local/sbin`. Актуальный `install.sh` создаёт эту директорию сам; достаточно повторить установку свежей командой `curl`. diff --git a/app/Sources/LemanaVPN/LemanaVPNApp.swift b/app/Sources/LemanaVPN/LemanaVPNApp.swift index fa740f7..0e894a6 100644 --- a/app/Sources/LemanaVPN/LemanaVPNApp.swift +++ b/app/Sources/LemanaVPN/LemanaVPNApp.swift @@ -100,6 +100,10 @@ struct VPNMenuView: View { vpnManager.connect(mode: .manual) } .keyboardShortcut("m") + Button("Подключить полностью вручную") { + vpnManager.connect(mode: .manualFull) + } + .keyboardShortcut("f") } } diff --git a/app/Sources/LemanaVPN/VPNManager.swift b/app/Sources/LemanaVPN/VPNManager.swift index e9bda2a..32f85a9 100644 --- a/app/Sources/LemanaVPN/VPNManager.swift +++ b/app/Sources/LemanaVPN/VPNManager.swift @@ -122,11 +122,13 @@ enum VPNState: Equatable { enum VPNLaunchMode: String { case auto case manual + case manualFull var cliArgument: String { switch self { case .auto: return "--auto" case .manual: return "--manual" + case .manualFull: return "--manual-full" } } } @@ -459,7 +461,7 @@ class VPNManager: ObservableObject { state = .disconnected userInitiatedDisconnect = false autoReconnectAttempts = 0 - } else if currentLaunchMode == .manual { + } else if currentLaunchMode != .auto { log("Manual connection ended; auto-reconnect is disabled for manual mode") state = .disconnected autoReconnectAttempts = 0 diff --git a/bin/vpn-lemanapro.sh b/bin/vpn-lemanapro.sh index ec7d35e..85cc636 100755 --- a/bin/vpn-lemanapro.sh +++ b/bin/vpn-lemanapro.sh @@ -85,14 +85,17 @@ for arg in "$@"; do --patch-only) PATCH_ONLY_MODE=true ;; --auto|auto) CONNECT_MODE="auto" ;; --manual|manual|--manual-sso) CONNECT_MODE="manual" ;; + --manual-full|manual-full|--manual-no-autofill) CONNECT_MODE="manual-full" ;; --help|-h) cat <<'HELP' -Usage: vpn-lemanapro.sh [--auto|--manual] [--debug] [--json] [--status] [--configure-keychain] [--patch-only] +Usage: vpn-lemanapro.sh [--auto|--manual|--manual-full] [--debug] [--json] [--status] [--configure-keychain] [--patch-only] --status Show current VPN status without connecting --status --json Show current VPN status as JSON --auto Hidden browser, auto-fill and auto-submit (default) --manual Visible browser, auto-fill fields, do not press submit + --manual-full Visible browser, no auto-fill, do not press submit + --manual-no-autofill Compatibility alias for --manual-full --manual-sso Compatibility alias for --manual --debug Passthrough debug logs; also shows browser in auto mode --json Emit JSON Lines events for UI wrappers @@ -105,9 +108,9 @@ HELP done case "$CONNECT_MODE" in - auto|manual) ;; + auto|manual|manual-full) ;; *) - printf 'Unknown VPN mode: %s. Use --auto or --manual.\n' "$CONNECT_MODE" >&2 + printf 'Unknown VPN mode: %s. Use --auto, --manual, or --manual-full.\n' "$CONNECT_MODE" >&2 exit 2 ;; esac @@ -953,6 +956,11 @@ if [[ "$CONNECT_MODE" == "manual" ]]; then autofill_disable="0" autofill_click="0" _emit '{"event":"manual_sso","autofill":true,"submit":false}' "Manual mode: browser is visible, fields are auto-filled, submit is not pressed." +elif [[ "$CONNECT_MODE" == "manual-full" ]]; then + display_mode="shown" + autofill_disable="1" + autofill_click="0" + _emit '{"event":"manual_sso","autofill":false,"submit":false}' "Full manual mode: browser is visible, auto-fill is disabled, submit is not pressed." fi if $DEBUG; then display_mode="shown" diff --git a/install.sh b/install.sh index 01ead22..e49caf9 100755 --- a/install.sh +++ b/install.sh @@ -839,7 +839,7 @@ install_shell_aliases() { zshrc="$HOME/.zshrc" tmp="$1" log_step "Обновляю shell aliases" - log_detail "Алиасы vpn, vpn-auto, vpn-manual, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc." + log_detail "Алиасы vpn, vpn-auto, vpn-manual, vpn-manual-full, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc." [ -f "$zshrc" ] || run touch "$zshrc" @@ -854,6 +854,7 @@ install_shell_aliases() { vpn() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" "\$@"; } vpn-auto() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --auto "\$@"; } vpn-manual() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --manual "\$@"; } +vpn-manual-full() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --manual-full "\$@"; } vpn-debug() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --debug "\$@"; } vpn-fix-dns() { sudo "$DNS_CLEANUP"; } # <<< lemana-vpn diff --git a/tests/smoke.sh b/tests/smoke.sh index 444c6d6..fea9db5 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -66,13 +66,16 @@ grep -q '"event":"waiting"' "$ROOT/bin/vpn-lemanapro.sh" grep -q -- '--patch-only' "$ROOT/bin/vpn-lemanapro.sh" grep -q -- '--auto' "$ROOT/bin/vpn-lemanapro.sh" grep -q -- '--manual' "$ROOT/bin/vpn-lemanapro.sh" +grep -q -- '--manual-full' "$ROOT/bin/vpn-lemanapro.sh" grep -q -- '--manual-sso' "$ROOT/bin/vpn-lemanapro.sh" grep -q 'LEMANA_VPN_AUTOFILL_DISABLE' "$ROOT/bin/vpn-lemanapro.sh" grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$ROOT/bin/vpn-lemanapro.sh" grep -q 'vpn-auto' "$ROOT/install.sh" grep -q 'vpn-manual' "$ROOT/install.sh" +grep -q 'vpn-manual-full' "$ROOT/install.sh" grep -q 'connect(mode: .auto)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift" grep -q 'connect(mode: .manual)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift" +grep -q 'connect(mode: .manualFull)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift" grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift" grep -q 'struct Credentials: Decodable' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift" grep -q 'credential_source' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift" @@ -235,6 +238,51 @@ if printf '%s\n' "$manual_output" | grep -q 'Cleaning up VPN DNS'; then exit 1 fi +fake_path_bin="$TMP_DIR/fake-path-bin" +mkdir -p "$fake_path_bin" +cat > "$fake_path_bin/security" <<'SH' +#!/bin/sh +if [ "${1:-}" = "find-generic-password" ]; then + exit 0 +fi +exit 1 +SH +chmod +x "$fake_path_bin/security" + +fake_oc_bin="$TMP_DIR/fake-openconnect-lite" +manual_full_capture="$TMP_DIR/manual-full-capture" +cat > "$fake_oc_bin" <<'SH' +#!/bin/sh +{ + printf 'args=%s\n' "$*" + printf 'autofill_disable=%s\n' "${LEMANA_VPN_AUTOFILL_DISABLE:-}" + printf 'autofill_click=%s\n' "${LEMANA_VPN_AUTOFILL_CLICK:-}" +} > "$LEMANA_VPN_CAPTURE_LAUNCH" +exit 0 +SH +chmod +x "$fake_oc_bin" + +manual_full_output="$( + HOME="$HOME" \ + PATH="$fake_path_bin:$PATH" \ + LEMANA_VPN_USERNAME="lemana-manual-full-$$" \ + LEMANA_VPN_CREDENTIAL_SOURCE=keychain \ + LEMANA_VPN_OC_BIN="$fake_oc_bin" \ + LEMANA_VPN_OC_PYTHON=python3 \ + LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \ + LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \ + LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/manual-full-patch-backups" \ + LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \ + LEMANA_VPN_CONNECT_LOG="$TMP_DIR/manual-full.log" \ + LEMANA_VPN_CAPTURE_LAUNCH="$manual_full_capture" \ + bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json +)" + +printf '%s\n' "$manual_full_output" | grep -q '"event":"manual_sso","autofill":false,"submit":false' +grep -q -- '--browser-display-mode shown' "$manual_full_capture" +grep -q '^autofill_disable=1$' "$manual_full_capture" +grep -q '^autofill_click=0$' "$manual_full_capture" + fake_oc_python="$TMP_DIR/fake-oc-python" captured_totp="$TMP_DIR/captured-totp" cat > "$fake_oc_python" <<'SH'