Улучши лог установки VPN

This commit is contained in:
2026-05-19 13:36:11 +03:00
parent 3945b2fb9f
commit 4187cb6544
3 changed files with 205 additions and 33 deletions

View File

@@ -157,6 +157,22 @@ Detected state:
В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`.
## Логи установщика
Установщик печатает пошаговый лог с emoji, цветом в интерактивном терминале и коротким пояснением, зачем нужен каждый шаг. Например, перед сборкой Swift-приложения он отдельно пишет, что `swift build` может занять время и что строки компилятора вида `[2/5] Write swift-version...` являются нормальным выводом.
Отключить цвет:
```sh
NO_COLOR=1 sh install.sh
```
Отключить emoji:
```sh
LEMANA_VPN_NO_EMOJI=1 sh install.sh
```
## Модули
### Core

View File

@@ -31,6 +31,42 @@ CONFIGURE_KEYCHAIN_FORCED=0
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then
C_RESET="$(printf '\033[0m')"
C_BOLD="$(printf '\033[1m')"
C_DIM="$(printf '\033[2m')"
C_RED="$(printf '\033[31m')"
C_GREEN="$(printf '\033[32m')"
C_YELLOW="$(printf '\033[33m')"
C_BLUE="$(printf '\033[34m')"
C_CYAN="$(printf '\033[36m')"
else
C_RESET=""
C_BOLD=""
C_DIM=""
C_RED=""
C_GREEN=""
C_YELLOW=""
C_BLUE=""
C_CYAN=""
fi
if [ "${LEMANA_VPN_NO_EMOJI:-0}" = "1" ]; then
E_STEP=">"
E_INFO="i"
E_OK="+"
E_WARN="!"
E_SKIP="-"
E_ERROR="x"
else
E_STEP="➡️"
E_INFO=""
E_OK="✅"
E_WARN="⚠️"
E_SKIP="⏭️"
E_ERROR="❌"
fi
usage() {
cat <<'USAGE'
Usage:
@@ -155,8 +191,38 @@ log() {
printf '%s\n' "$*"
}
color_line() {
color="$1"
shift
printf '%s%s%s\n' "$color" "$*" "$C_RESET"
}
log_step() {
color_line "$C_BOLD$C_CYAN" "$E_STEP $*"
}
log_info() {
color_line "$C_BLUE" "$E_INFO $*"
}
log_detail() {
color_line "$C_DIM" " $*"
}
log_ok() {
color_line "$C_GREEN" "$E_OK $*"
}
log_warn() {
color_line "$C_YELLOW" "$E_WARN $*"
}
log_skip() {
color_line "$C_DIM" "$E_SKIP $*"
}
die() {
printf 'ERROR: %s\n' "$*" >&2
printf '%s%s ERROR: %s%s\n' "$C_RED" "$E_ERROR" "$*" "$C_RESET" >&2
exit 1
}
@@ -206,7 +272,7 @@ yes_no() {
fi
while :; do
printf '%s %s ' "$prompt" "$suffix" > /dev/tty
printf '%s%s %s%s ' "$C_BOLD" "$prompt" "$suffix" "$C_RESET" > /dev/tty
IFS= read -r answer < /dev/tty || answer=""
case "$answer" in
"") [ "$default_answer" = "y" ]; return $? ;;
@@ -234,6 +300,8 @@ zsh_aliases_installed() {
}
print_detected_state() {
log_step "Проверяю установленное окружение"
log_detail "Так установщик понимает, какие модули уже есть, а по каким нужно спросить."
log "Detected state:"
log " openconnect: $(bool_word command -v openconnect)"
log " pipx: $(bool_word command -v pipx)"
@@ -254,11 +322,12 @@ choose_modules() {
print_detected_state
if ! interactive_enabled; then
log "Interactive prompts: off"
log_skip "Interactive prompts: off"
return 0
fi
log "Interactive prompts: on"
log_info "Interactive prompts: on"
log_detail "Будут вопросы только по отсутствующим опциональным модулям; флаги командной строки имеют приоритет."
if [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
if yes_no "Bitwarden CLI не найден. Поставить модуль Bitwarden?" y; then
@@ -365,53 +434,71 @@ write_file() {
install_homebrew_packages() {
need_cmd brew
log_step "Проверяю Homebrew-зависимости"
log_detail "openconnect нужен как VPN-клиент, pipx — для изолированной установки openconnect-lite."
for pkg in openconnect pipx; do
if brew list "$pkg" >/dev/null 2>&1; then
log "Homebrew package already installed: $pkg"
log_ok "Homebrew package already installed: $pkg"
else
log "Installing Homebrew package: $pkg"
log_info "Installing Homebrew package: $pkg"
run brew install "$pkg"
fi
done
if [ "$USE_BITWARDEN" -eq 1 ]; then
if brew list bitwarden-cli >/dev/null 2>&1; then
log "Homebrew package already installed: bitwarden-cli"
log_ok "Homebrew package already installed: bitwarden-cli"
else
log "Installing Homebrew package: bitwarden-cli"
log_info "Installing Homebrew package: bitwarden-cli"
log_detail "Bitwarden CLI нужен, чтобы брать LDAP-пароль и TOTP seed из item '$BW_ITEM'."
run brew install bitwarden-cli
fi
else
log_skip "Bitwarden module disabled; пропускаю bitwarden-cli."
fi
}
install_openconnect_lite() {
need_cmd pipx
log_step "Проверяю openconnect-lite"
log_detail "openconnect-lite открывает SAML/Keycloak SSO и передаёт полученную сессию в openconnect."
if [ -x "$HOME/.local/bin/openconnect-lite" ] && [ "$FORCE" -eq 0 ]; then
log "openconnect-lite already installed"
log_ok "openconnect-lite already installed"
else
log "Installing openconnect-lite via pipx"
log_info "Installing openconnect-lite via pipx"
run pipx install openconnect-lite
fi
if pipx --help 2>/dev/null | grep -q ' pin '; then
log_detail "Закрепляю pipx package, чтобы случайное обновление не сломало SSO-патчи."
run pipx pin openconnect-lite >/dev/null 2>&1 || true
fi
}
install_cli() {
tmp="$1"
log_step "Обновляю CLI и uninstall helper"
log_detail "CLI управляет подключением, статусом модулей, Bitwarden/Keychain sync и runtime-патчами."
run mkdir -p "$INSTALL_BIN_DIR"
download_file "bin/vpn-lemanapro.sh" "$tmp/vpn-lemanapro.sh"
run install -m 755 "$tmp/vpn-lemanapro.sh" "$INSTALL_BIN_DIR/vpn-lemanapro.sh"
log_ok "CLI installed: $INSTALL_BIN_DIR/vpn-lemanapro.sh"
download_file "uninstall.sh" "$tmp/uninstall.sh"
run install -m 755 "$tmp/uninstall.sh" "$INSTALL_BIN_DIR/uninstall-lemana-vpn.sh"
log_ok "Uninstall helper installed: $INSTALL_BIN_DIR/uninstall-lemana-vpn.sh"
}
install_config() {
tmp="$1"
log_step "Записываю конфигурацию"
log_detail "Здесь сохраняются выбранные модули, LDAP username и профиль openconnect-lite для Keycloak SSO."
run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR"
download_file "templates/openconnect-lite-config.toml" "$tmp/openconnect-lite-config.toml"
@@ -429,20 +516,32 @@ LEMANA_VPN_USE_TOUCHID=\"$USE_TOUCHID\"
LEMANA_VPN_DNS_CLEANUP=\"$DNS_CLEANUP\""
write_file "$tmp/env" "$env_content"
run install -m 600 "$tmp/env" "$CONFIG_DIR/env"
log_ok "Config installed: $CONFIG_DIR/env"
}
install_dns_cleanup() {
tmp="$1"
dns_cleanup_dir="$(dirname "$DNS_CLEANUP")"
log_step "Устанавливаю DNS cleanup wrapper"
log_detail "Wrapper сбрасывает только похожие на корпоративные 10.x DNS после аварийного завершения VPN."
log_detail "macOS может запросить sudo-пароль для записи в $dns_cleanup_dir."
download_file "libexec/lemana-vpn-dns-cleanup" "$tmp/lemana-vpn-dns-cleanup"
run sudo install -d -m 755 -o root -g wheel "$dns_cleanup_dir"
log "Installing DNS cleanup wrapper: $DNS_CLEANUP"
log_info "Installing DNS cleanup wrapper: $DNS_CLEANUP"
run sudo install -m 755 -o root -g wheel "$tmp/lemana-vpn-dns-cleanup" "$DNS_CLEANUP"
log_ok "DNS cleanup wrapper installed"
}
install_sudoers() {
[ "$INSTALL_SUDOERS" -eq 1 ] || return 0
if [ "$INSTALL_SUDOERS" -ne 1 ]; then
log_skip "Sudoers module disabled; openconnect/DNS cleanup могут спрашивать пароль sudo."
return 0
fi
log_step "Настраиваю sudoers"
log_detail "Это убирает повторный sudo-пароль при подключении, но разрешает только openconnect и DNS cleanup wrapper."
openconnect_bin="$(brew --prefix)/bin/openconnect"
[ -x "$openconnect_bin" ] || openconnect_bin="$(command -v openconnect || true)"
@@ -456,17 +555,25 @@ install_sudoers() {
write_file "$tmp/sudoers-openconnect" "$current_user ALL=(ALL) NOPASSWD: $openconnect_bin"
run sudo install -m 440 -o root -g wheel "$tmp/sudoers-openconnect" /etc/sudoers.d/lemana-vpn-openconnect
run sudo visudo -c -f /etc/sudoers.d/lemana-vpn-openconnect
log_ok "sudoers openconnect rule is valid"
write_file "$tmp/sudoers-dns" "$current_user ALL=(ALL) NOPASSWD: $DNS_CLEANUP"
run sudo install -m 440 -o root -g wheel "$tmp/sudoers-dns" /etc/sudoers.d/lemana-vpn-dns
run sudo visudo -c -f /etc/sudoers.d/lemana-vpn-dns
log_ok "sudoers DNS cleanup rule is valid"
}
install_touchid_helper() {
[ "$USE_TOUCHID" -eq 1 ] || return 0
if [ "$USE_TOUCHID" -ne 1 ]; then
log_skip "Touch ID module disabled; мастер-пароль Bitwarden будет вводиться вручную при необходимости."
return 0
fi
log_step "Проверяю Touch ID helper"
log_detail "Helper добавляет локальный Touch ID prompt перед чтением мастер-пароля Bitwarden из Keychain."
if [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ] && [ "$FORCE" -eq 0 ]; then
log "Touch ID helper already installed: $INSTALL_BIN_DIR/keychain-fingerprint"
log_ok "Touch ID helper already installed: $INSTALL_BIN_DIR/keychain-fingerprint"
return 0
fi
@@ -474,26 +581,34 @@ install_touchid_helper() {
need_cmd swiftc
tmp="$1"
log "Building keychain-fingerprint helper"
log_info "Building keychain-fingerprint helper"
run git clone --depth 1 https://github.com/dss99911/keychain-fingerprint.git "$tmp/keychain-fingerprint"
run swiftc -o "$tmp/keychain-fingerprint-bin" "$tmp/keychain-fingerprint/main.swift" -framework LocalAuthentication -framework Security
run install -m 700 "$tmp/keychain-fingerprint-bin" "$INSTALL_BIN_DIR/keychain-fingerprint"
log_ok "Touch ID helper installed"
}
install_menu_bar_app() {
[ "$INSTALL_APP" -eq 1 ] || return 0
if [ "$INSTALL_APP" -ne 1 ]; then
log_skip "Menu Bar app disabled; CLI и алиасы останутся основным интерфейсом."
return 0
fi
need_cmd swift
tmp="$1"
app_src="$tmp/app"
log_step "Собираю Swift Menu Bar app"
log_detail "Приложение живёт в status bar и вызывает $INSTALL_BIN_DIR/vpn-lemanapro.sh для подключения и статуса модулей."
log_detail "Swift build может занять минуту; строки вида '[2/5] Write swift-version...' — нормальный вывод компилятора."
run mkdir -p "$app_src/Sources/LemanaVPN"
download_file "app/Package.swift" "$app_src/Package.swift"
download_file "app/Sources/LemanaVPN/LemanaVPNApp.swift" "$app_src/Sources/LemanaVPN/LemanaVPNApp.swift"
download_file "app/Sources/LemanaVPN/VPNManager.swift" "$app_src/Sources/LemanaVPN/VPNManager.swift"
log "Building LemanaVPN.app"
log_info "Building LemanaVPN.app"
run swift build -c release --package-path "$app_src"
app_bin="$app_src/.build/release/LemanaVPN"
@@ -531,14 +646,23 @@ install_menu_bar_app() {
</dict>
</plist>'
run install -m 644 "$info_plist" "$APP_DIR/Contents/Info.plist"
log_ok "Menu Bar app installed: $APP_DIR"
}
install_launch_agent() {
[ "$INSTALL_AUTOSTART" -eq 1 ] || return 0
[ "$INSTALL_APP" -eq 1 ] || return 0
if [ "$INSTALL_AUTOSTART" -ne 1 ]; then
log_skip "Autostart disabled; LemanaVPN.app можно запускать вручную."
return 0
fi
if [ "$INSTALL_APP" -ne 1 ]; then
log_skip "Autostart skipped because Menu Bar app is disabled."
return 0
fi
tmp="$1"
plist="$tmp/ru.dokops.LemanaVPN.plist"
log_step "Настраиваю автозапуск Menu Bar app"
log_detail "LaunchAgent запускает LemanaVPN.app при логине, чтобы VPN был доступен из status bar."
run mkdir -p "$HOME/Library/LaunchAgents"
write_file "$plist" "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
@@ -564,6 +688,7 @@ install_launch_agent() {
else
printf '+ launchctl load %s\n' "$LAUNCH_AGENT"
fi
log_ok "LaunchAgent installed: $LAUNCH_AGENT"
}
restart_running_menu_bar_app() {
@@ -575,18 +700,28 @@ restart_running_menu_bar_app() {
fi
if pgrep -x LemanaVPN >/dev/null 2>&1; then
log "Restarting running LemanaVPN.app"
log_step "Перезапускаю уже запущенное LemanaVPN.app"
log_detail "Это нужно, чтобы status bar приложение сразу увидело свежий CLI и новый код."
killall LemanaVPN >/dev/null 2>&1 || true
sleep 1
open "$APP_DIR" >/dev/null 2>&1 || true
log_ok "LemanaVPN.app restarted"
else
log_skip "LemanaVPN.app сейчас не запущен; перезапуск не нужен."
fi
}
install_shell_aliases() {
[ "$INSTALL_ALIASES" -eq 1 ] || return 0
if [ "$INSTALL_ALIASES" -ne 1 ]; then
log_skip "Shell aliases disabled; команды можно вызывать напрямую из $INSTALL_BIN_DIR."
return 0
fi
zshrc="$HOME/.zshrc"
tmp="$1"
log_step "Обновляю shell aliases"
log_detail "Алиасы vpn, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc."
[ -f "$zshrc" ] || run touch "$zshrc"
block="$tmp/zshrc-block"
@@ -616,19 +751,29 @@ EOF
} > "$tmp/zshrc.new"
mv "$tmp/zshrc.new" "$zshrc"
log_ok "Shell aliases updated: $zshrc"
}
maybe_login_bitwarden() {
[ "$USE_BITWARDEN" -eq 1 ] || return 0
command -v bw >/dev/null 2>&1 || return 0
if [ "$USE_BITWARDEN" -ne 1 ]; then
log_skip "Bitwarden module disabled; credentials будут браться из macOS Keychain."
return 0
fi
if ! command -v bw >/dev/null 2>&1; then
log_warn "Bitwarden CLI enabled but bw not found."
return 0
fi
log_step "Проверяю Bitwarden CLI session"
log_detail "На первом запуске vpn CLI сможет понять, нужно ли делать bw login/unlock."
status="$(bw status 2>/dev/null || true)"
if printf '%s\n' "$status" | grep -q '"status":"unauthenticated"'; then
log "Bitwarden CLI is not logged in. Run later: bw login"
log_warn "Bitwarden CLI is not logged in. Run later: bw login"
elif printf '%s\n' "$status" | grep -q '"status":"locked"'; then
log "Bitwarden CLI is logged in but locked. First vpn run will ask for master password."
log_info "Bitwarden CLI is logged in but locked. First vpn run will ask for master password."
else
log "Bitwarden CLI is available."
log_ok "Bitwarden CLI is available."
fi
}
@@ -640,8 +785,9 @@ main() {
choose_modules
log "Installing Lemana VPN"
log "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
log_step "Начинаю установку Lemana VPN"
log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются."
log_info "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
install_homebrew_packages
install_openconnect_lite
@@ -657,16 +803,18 @@ main() {
maybe_login_bitwarden
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
log_step "Записываю credentials в macOS Keychain"
log_detail "Будут запрошены LDAP-пароль и постоянный TOTP seed, не текущий 30-секундный код."
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-keychain
fi
log ""
log "Done."
log "Open a new shell or run: exec zsh"
log "Connect: vpn"
log "Status: vpn --status"
log_ok "Done."
log_info "Open a new shell or run: exec zsh"
log_info "Connect: vpn"
log_info "Status: vpn --status"
if [ "$INSTALL_APP" -eq 1 ]; then
log "App: open '$APP_DIR'"
log_info "App: open '$APP_DIR'"
fi
}

View File

@@ -16,11 +16,19 @@ output="$(cd "$ROOT" && sh install.sh --dry-run --non-interactive --minimal)"
printf '%s\n' "$output" | grep -q 'Detected state:'
printf '%s\n' "$output" | grep -q 'Interactive prompts: off'
printf '%s\n' "$output" | grep -q 'Modules: bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
printf '%s\n' "$output" | grep -q 'Проверяю Homebrew-зависимости'
printf '%s\n' "$output" | grep -q 'Swift build может занять минуту'
printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin'
printf '%s\n' "$output" | grep -q 'swift build -c release --package-path'
printf '%s\n' "$output" | grep -q 'launchctl load'
printf '%s\n' "$output" | grep -q 'restart LemanaVPN.app if running'
esc="$(printf '\033')"
if printf '%s\n' "$output" | grep -q "$esc"; then
echo "non-tty dry-run output contains ANSI color codes" >&2
exit 1
fi
status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$status_json" | grep -q '"modules":'
printf '%s\n' "$status_json" | grep -q '"app":'