Добавь установщик Lemana VPN
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
|
||||||
207
README.md
Normal file
207
README.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Lemana VPN
|
||||||
|
|
||||||
|
CLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.
|
||||||
|
|
||||||
|
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
|
||||||
|
|
||||||
|
- `openconnect` как VPN-клиент;
|
||||||
|
- `openconnect-lite` для SAML SSO через Keycloak;
|
||||||
|
- опциональный Bitwarden CLI для LDAP-пароля и TOTP;
|
||||||
|
- опциональный Touch ID helper для мастер-пароля Bitwarden;
|
||||||
|
- безопасный DNS cleanup через root-owned wrapper;
|
||||||
|
- алиасы `vpn`, `vpn-debug`, `vpn-fix-dns`.
|
||||||
|
|
||||||
|
## Быстрая установка
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL http://192.168.50.109/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
После установки открой новый shell или выполни:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
exec zsh
|
||||||
|
vpn
|
||||||
|
```
|
||||||
|
|
||||||
|
## Варианты установки
|
||||||
|
|
||||||
|
Полная установка, режим по умолчанию:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL http://192.168.50.109/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Без Touch ID, но с Bitwarden:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL http://192.168.50.109/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --without-touchid
|
||||||
|
```
|
||||||
|
|
||||||
|
Минимальная установка без Bitwarden и Touch ID. Пароль LDAP и TOTP будут один раз записаны в macOS Keychain вручную:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL http://192.168.50.109/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --minimal --configure-keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить действия без изменений:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL http://192.168.50.109/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Если 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/keychain-fingerprint` | Опциональный Touch ID helper для мастер-пароля Bitwarden |
|
||||||
|
| `~/.config/lemana-vpn/env` | Локальная конфигурация модулей |
|
||||||
|
| `~/.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-debug`, `vpn-fix-dns` |
|
||||||
|
|
||||||
|
## Модули
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|
Всегда устанавливается:
|
||||||
|
|
||||||
|
- `openconnect` через Homebrew;
|
||||||
|
- `pipx` через Homebrew;
|
||||||
|
- `openconnect-lite` через `pipx`;
|
||||||
|
- CLI `vpn-lemanapro.sh`;
|
||||||
|
- `openconnect-lite` config;
|
||||||
|
- DNS cleanup wrapper.
|
||||||
|
|
||||||
|
### Bitwarden
|
||||||
|
|
||||||
|
Включён по умолчанию. CLI при каждом запуске `vpn` пытается получить LDAP-пароль и TOTP из записи Bitwarden `LM LDAP`, затем записывает их в macOS Keychain для `openconnect-lite`.
|
||||||
|
|
||||||
|
Отключить:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh install.sh --without-bitwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
В этом режиме credentials нужно положить в Keychain вручную:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn-lemanapro.sh --configure-keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn # подключиться
|
||||||
|
vpn --status # статус без нового подключения
|
||||||
|
vpn --status --json # статус в JSON
|
||||||
|
vpn-debug # видимый браузер и debug-логи
|
||||||
|
vpn-fix-dns # сбросить корпоративные DNS после аварийного завершения
|
||||||
|
```
|
||||||
|
|
||||||
|
Первый запуск с Bitwarden:
|
||||||
|
|
||||||
|
1. CLI проверит `bw`.
|
||||||
|
2. Если vault locked, попросит мастер-пароль.
|
||||||
|
3. Если установлен Touch ID helper, предложит сохранить мастер-пароль за Touch ID prompt.
|
||||||
|
4. Достанет `LM LDAP`, запишет LDAP-пароль и TOTP в 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 http://192.168.50.109/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.
|
||||||
|
|
||||||
|
## Почему DNS wrapper, а не wildcard sudoers
|
||||||
|
|
||||||
|
Старый вариант давал `NOPASSWD` на `networksetup -setdnsservers *`. Это слишком широкое право: любой локальный процесс пользователя мог поменять DNS на произвольный сервер.
|
||||||
|
|
||||||
|
Новый вариант разрешает sudo только на `/usr/local/sbin/lemana-vpn-dns-cleanup`. Wrapper сбрасывает DNS только если текущий DNS начинается с `10.`, то есть похож на корпоративный VPN DNS.
|
||||||
|
|
||||||
|
## Диагностика
|
||||||
|
|
||||||
|
Проверить установку:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
command -v vpn-lemanapro.sh
|
||||||
|
openconnect --version
|
||||||
|
~/.local/bin/openconnect-lite --help
|
||||||
|
sudo -n /usr/local/sbin/lemana-vpn-dns-cleanup
|
||||||
|
vpn --status
|
||||||
|
```
|
||||||
|
|
||||||
|
Если SSO ломается после обновления `openconnect-lite`, запусти:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn-debug
|
||||||
|
```
|
||||||
|
|
||||||
|
CLI перед подключением патчит `openconnect-lite`:
|
||||||
|
|
||||||
|
- `minimal` -> `offscreen`, чтобы Qt WebEngine не падал на macOS;
|
||||||
|
- добавляет `input` и `change` events для Keycloak auto-fill;
|
||||||
|
- добавляет URL guard, чтобы auto-fill не кликал submit на Cisco ACS.
|
||||||
|
|
||||||
|
## Удаление
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rm -f ~/bin/vpn-lemanapro.sh ~/bin/keychain-fingerprint
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
459
bin/vpn-lemanapro.sh
Executable file
459
bin/vpn-lemanapro.sh
Executable file
@@ -0,0 +1,459 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/env"
|
||||||
|
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
OC_VENV="${LEMANA_VPN_OC_VENV:-$HOME/.local/pipx/venvs/openconnect-lite}"
|
||||||
|
OC_PYTHON="${LEMANA_VPN_OC_PYTHON:-$OC_VENV/bin/python}"
|
||||||
|
OC_BIN="${LEMANA_VPN_OC_BIN:-$HOME/.local/bin/openconnect-lite}"
|
||||||
|
BW_ITEM_NAME="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
||||||
|
KC_USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
||||||
|
KC_FP="${LEMANA_VPN_KEYCHAIN_FINGERPRINT:-$HOME/bin/keychain-fingerprint}"
|
||||||
|
USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN:-1}"
|
||||||
|
USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-1}"
|
||||||
|
CACHE_BW_SESSION="${LEMANA_VPN_CACHE_BW_SESSION:-0}"
|
||||||
|
DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP:-/usr/local/sbin/lemana-vpn-dns-cleanup}"
|
||||||
|
BW_KC_SERVICE="${LEMANA_VPN_BW_KC_SERVICE:-vpn-lemanapro}"
|
||||||
|
BW_KC_ACCOUNT_SESSION="${LEMANA_VPN_BW_KC_ACCOUNT_SESSION:-bw-session}"
|
||||||
|
BW_KC_ACCOUNT_MASTER="${LEMANA_VPN_BW_KC_ACCOUNT_MASTER:-bw-master}"
|
||||||
|
STATUS_DIR="${LEMANA_VPN_STATUS_DIR:-$HOME/.local/state/vpn-lemanapro}"
|
||||||
|
STATUS_FILE="$STATUS_DIR/status.json"
|
||||||
|
|
||||||
|
DEBUG=false
|
||||||
|
JSON_MODE=false
|
||||||
|
STATUS_MODE=false
|
||||||
|
CONFIGURE_KEYCHAIN_MODE=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--debug) DEBUG=true ;;
|
||||||
|
--json) JSON_MODE=true ;;
|
||||||
|
--status) STATUS_MODE=true ;;
|
||||||
|
--configure-keychain) CONFIGURE_KEYCHAIN_MODE=true ;;
|
||||||
|
--help|-h)
|
||||||
|
cat <<'HELP'
|
||||||
|
Usage: vpn-lemanapro.sh [--debug] [--json] [--status] [--configure-keychain]
|
||||||
|
|
||||||
|
--status Show current VPN status without connecting
|
||||||
|
--status --json Show current VPN status as JSON
|
||||||
|
--debug Run visible browser and passthrough debug logs
|
||||||
|
--json Emit JSON Lines events for UI wrappers
|
||||||
|
--configure-keychain Prompt for LDAP password/TOTP and save them to Keychain
|
||||||
|
HELP
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
_emit() {
|
||||||
|
local json="$1" human="$2"
|
||||||
|
if $JSON_MODE; then
|
||||||
|
printf '%s\n' "$json"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$human"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_write_status() {
|
||||||
|
mkdir -p "$STATUS_DIR"
|
||||||
|
printf '%s\n' "$1" > "$STATUS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
_clear_status() {
|
||||||
|
_write_status "{\"pid\":$$,\"state\":\"disconnected\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
_json_get() {
|
||||||
|
local key="$1"
|
||||||
|
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
_check_status() {
|
||||||
|
if [[ ! -f "$STATUS_FILE" ]]; then
|
||||||
|
if $JSON_MODE; then
|
||||||
|
printf '%s\n' '{"state":"disconnected","reason":"no status file"}'
|
||||||
|
else
|
||||||
|
printf '%s\n' "VPN disconnected (нет status-файла)"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local status_json pid state ip expires dns_target
|
||||||
|
status_json="$(cat "$STATUS_FILE")"
|
||||||
|
pid="$(printf '%s' "$status_json" | _json_get pid)"
|
||||||
|
state="$(printf '%s' "$status_json" | _json_get state)"
|
||||||
|
ip="$(printf '%s' "$status_json" | _json_get ip)"
|
||||||
|
expires="$(printf '%s' "$status_json" | _json_get expires)"
|
||||||
|
dns_target="$(printf '%s' "$status_json" | _json_get dns_target)"
|
||||||
|
|
||||||
|
local process_alive=false
|
||||||
|
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
||||||
|
process_alive=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$state" != "connected" ]] || ! $process_alive; then
|
||||||
|
if $JSON_MODE; then
|
||||||
|
printf '{"state":"disconnected","reason":"%s"}\n' "$(! $process_alive && printf 'process dead (pid=%s)' "$pid" || printf '%s' "$state")"
|
||||||
|
else
|
||||||
|
printf '%s\n' "VPN disconnected"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local healthy=false
|
||||||
|
if [[ -n "$dns_target" ]] && ping -c 1 -W 2000 "$dns_target" >/dev/null 2>&1; then
|
||||||
|
healthy=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
local remaining_sec=0 hours=0 mins=0
|
||||||
|
if [[ -n "$expires" ]]; then
|
||||||
|
local exp_ts now_ts
|
||||||
|
exp_ts="$(date -jf "%Y-%m-%dT%H:%M:%SZ" "$expires" "+%s" 2>/dev/null || true)"
|
||||||
|
now_ts="$(date "+%s")"
|
||||||
|
if [[ -n "$exp_ts" ]]; then
|
||||||
|
remaining_sec=$((exp_ts - now_ts))
|
||||||
|
hours=$((remaining_sec / 3600))
|
||||||
|
mins=$(((remaining_sec % 3600) / 60))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $JSON_MODE; then
|
||||||
|
printf '{"state":"connected","ip":"%s","healthy":%s,"remaining_sec":%s,"expires":"%s","pid":%s,"dns_target":"%s"}\n' \
|
||||||
|
"$ip" "$healthy" "$remaining_sec" "$expires" "$pid" "$dns_target"
|
||||||
|
else
|
||||||
|
printf 'VPN connected - %s (session: %sh %sm, tunnel: %s)\n' \
|
||||||
|
"$ip" "$hours" "$mins" "$($healthy && printf healthy || printf unhealthy)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_find_webengine_process() {
|
||||||
|
if [[ -n "${LEMANA_VPN_WEBENGINE_PROCESS:-}" ]]; then
|
||||||
|
printf '%s\n' "$LEMANA_VPN_WEBENGINE_PROCESS"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
find "$OC_VENV/lib" -path '*/site-packages/openconnect_lite/browser/webengine_process.py' -print -quit 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
_patch_oc() {
|
||||||
|
local wep
|
||||||
|
wep="$(_find_webengine_process)"
|
||||||
|
if [[ -z "$wep" || ! -f "$wep" ]]; then
|
||||||
|
printf 'webengine_process.py not found. Run: pipx install openconnect-lite\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$OC_PYTHON" - "$wep" <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
src = path.read_text()
|
||||||
|
original = src
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
src = src.replace('argv += ["-platform", "minimal"]', 'argv += ["-platform", "offscreen"]')
|
||||||
|
if src != original:
|
||||||
|
messages.append("minimal -> offscreen")
|
||||||
|
original = src
|
||||||
|
|
||||||
|
old_fill = 'elem.dispatchEvent(new Event("focus")); elem.value = {value}; elem.dispatchEvent(new Event("blur"));'
|
||||||
|
new_fill = 'elem.dispatchEvent(new Event("focus")); elem.value = {value}; elem.dispatchEvent(new Event("input", {{bubbles: true}})); elem.dispatchEvent(new Event("change", {{bubbles: true}})); elem.dispatchEvent(new Event("blur"));'
|
||||||
|
if old_fill in src:
|
||||||
|
src = src.replace(old_fill, new_fill)
|
||||||
|
messages.append("input/change events")
|
||||||
|
|
||||||
|
if "new RegExp" not in src:
|
||||||
|
old_block = ''' script.setSourceCode(
|
||||||
|
f"""
|
||||||
|
// ==UserScript==
|
||||||
|
// @include {url_pattern}
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
function autoFill() {{
|
||||||
|
{get_selectors(rules, credentials)}
|
||||||
|
setTimeout(autoFill, 1000);
|
||||||
|
}}
|
||||||
|
autoFill();
|
||||||
|
"""
|
||||||
|
)'''
|
||||||
|
new_block = ''' regex_str = "^" + url_pattern.replace(".", "\\\\.").replace("*", ".*") + "$"
|
||||||
|
js_regex_str = json.dumps(regex_str)
|
||||||
|
script.setSourceCode(
|
||||||
|
f"""
|
||||||
|
// ==UserScript==
|
||||||
|
// @include {url_pattern}
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
var _afUrlPattern = new RegExp({js_regex_str});
|
||||||
|
var _afRun = 0;
|
||||||
|
function autoFill() {{
|
||||||
|
if (!_afUrlPattern.test(location.href.split('?')[0]) && !_afUrlPattern.test(location.href)) {{
|
||||||
|
_afRun++;
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
_afRun++;
|
||||||
|
|
||||||
|
{get_selectors(rules, credentials)}
|
||||||
|
setTimeout(autoFill, 1000);
|
||||||
|
}}
|
||||||
|
autoFill();
|
||||||
|
"""
|
||||||
|
)'''
|
||||||
|
if old_block not in src:
|
||||||
|
print("Cannot apply URL guard patch: unsupported openconnect-lite source", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
src = src.replace(old_block, new_block)
|
||||||
|
messages.append("URL guard")
|
||||||
|
|
||||||
|
if src != path.read_text():
|
||||||
|
path.write_text(src)
|
||||||
|
for message in messages:
|
||||||
|
print(f"Patch applied: {message}")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
_store_keychain() {
|
||||||
|
local password="$1" totp_secret="$2"
|
||||||
|
KC_USERNAME="$KC_USERNAME" _VPN_PASS="$password" _VPN_TOTP_SECRET="$totp_secret" "$OC_PYTHON" - <<'PY'
|
||||||
|
import keyring
|
||||||
|
import os
|
||||||
|
|
||||||
|
username = os.environ["KC_USERNAME"]
|
||||||
|
password = os.environ.get("_VPN_PASS", "")
|
||||||
|
totp_secret = os.environ.get("_VPN_TOTP_SECRET", "")
|
||||||
|
|
||||||
|
if password:
|
||||||
|
keyring.set_password("openconnect-lite", username, password)
|
||||||
|
if totp_secret:
|
||||||
|
keyring.set_password("openconnect-lite", "totp/" + username, totp_secret)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
_configure_keychain() {
|
||||||
|
local password totp_secret
|
||||||
|
read -rsp "LDAP password: " password
|
||||||
|
printf '\n'
|
||||||
|
read -rsp "TOTP secret (BASE32, optional if already stored): " totp_secret
|
||||||
|
printf '\n'
|
||||||
|
if [[ -z "$password" ]]; then
|
||||||
|
printf 'Empty password, nothing was saved.\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_store_keychain "$password" "$totp_secret"
|
||||||
|
printf 'Credentials saved to macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
_bw_cache_session() {
|
||||||
|
[[ "$CACHE_BW_SESSION" == "1" ]] || return 0
|
||||||
|
local session="$1"
|
||||||
|
security delete-generic-password -s "$BW_KC_SERVICE" -a "$BW_KC_ACCOUNT_SESSION" >/dev/null 2>&1 || true
|
||||||
|
security add-generic-password -s "$BW_KC_SERVICE" -a "$BW_KC_ACCOUNT_SESSION" -w "$session" -U >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
_bw_unlock() {
|
||||||
|
[[ "$USE_BITWARDEN" == "1" ]] || return 1
|
||||||
|
|
||||||
|
if ! command -v bw >/dev/null 2>&1; then
|
||||||
|
printf 'Bitwarden CLI is not installed. Using existing Keychain credentials.\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CACHE_BW_SESSION" == "1" ]]; then
|
||||||
|
local cached_session
|
||||||
|
cached_session="$(security find-generic-password -s "$BW_KC_SERVICE" -a "$BW_KC_ACCOUNT_SESSION" -w 2>/dev/null || true)"
|
||||||
|
if [[ -n "$cached_session" ]] && bw status --session "$cached_session" 2>/dev/null | grep -q '"status":"unlocked"'; then
|
||||||
|
BW_SESSION="$cached_session"
|
||||||
|
_emit '{"event":"bw_cached"}' "Bitwarden: cached session"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$USE_TOUCHID" == "1" && -x "$KC_FP" ]]; then
|
||||||
|
local master_pw
|
||||||
|
master_pw="$("$KC_FP" get "$BW_KC_SERVICE" "$BW_KC_ACCOUNT_MASTER" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$master_pw" ]]; then
|
||||||
|
_emit '{"event":"bw_touchid"}' "Bitwarden: unlocking via Touch ID..."
|
||||||
|
BW_SESSION="$(BW_PASSWORD="$master_pw" bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null || true)"
|
||||||
|
unset master_pw
|
||||||
|
if [[ -n "${BW_SESSION:-}" ]]; then
|
||||||
|
_bw_cache_session "$BW_SESSION"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'Touch ID password was rejected. Falling back to manual unlock.\n' >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local manual_pw
|
||||||
|
_emit '{"event":"bw_manual"}' "Unlocking Bitwarden vault..."
|
||||||
|
read -rsp "Bitwarden master password: " manual_pw
|
||||||
|
printf '\n'
|
||||||
|
if [[ -z "$manual_pw" ]]; then
|
||||||
|
printf 'Empty Bitwarden password. Using existing Keychain credentials.\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BW_SESSION="$(BW_PASSWORD="$manual_pw" bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null || true)"
|
||||||
|
if [[ -z "${BW_SESSION:-}" ]]; then
|
||||||
|
unset manual_pw
|
||||||
|
printf 'Failed to unlock Bitwarden. Using existing Keychain credentials.\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_bw_cache_session "$BW_SESSION"
|
||||||
|
|
||||||
|
if [[ "$USE_TOUCHID" == "1" && -x "$KC_FP" ]]; then
|
||||||
|
local save_choice
|
||||||
|
read -rp "Save Bitwarden master password behind Touch ID? [Y/n] " save_choice
|
||||||
|
if [[ "${save_choice:-y}" =~ ^[Yy]?$ ]]; then
|
||||||
|
printf '%s' "$manual_pw" | "$KC_FP" set "$BW_KC_SERVICE" "$BW_KC_ACCOUNT_MASTER" >/dev/null 2>&1 \
|
||||||
|
&& printf 'Saved. Next unlock can use Touch ID.\n' \
|
||||||
|
|| printf 'Could not save Bitwarden master password.\n' >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset manual_pw
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_sync_bitwarden() {
|
||||||
|
if ! _bw_unlock; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local bw_password bw_item bw_totp_secret
|
||||||
|
bw_password="$(bw get password "$BW_ITEM_NAME" --session "$BW_SESSION" 2>/dev/null || true)"
|
||||||
|
bw_item="$(bw get item "$BW_ITEM_NAME" --session "$BW_SESSION" 2>/dev/null || true)"
|
||||||
|
bw_totp_secret="$(printf '%s' "$bw_item" | python3 -c '
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
totp = data.get("login", {}).get("totp", "") or ""
|
||||||
|
if totp.startswith("otpauth://"):
|
||||||
|
match = re.search(r"secret=([A-Z2-7]+)", totp, re.IGNORECASE)
|
||||||
|
print(match.group(1) if match else "")
|
||||||
|
else:
|
||||||
|
print(totp.strip())
|
||||||
|
except Exception:
|
||||||
|
print("")
|
||||||
|
')"
|
||||||
|
|
||||||
|
if [[ -n "$bw_password" ]]; then
|
||||||
|
_store_keychain "$bw_password" "$bw_totp_secret"
|
||||||
|
_emit '{"event":"bw_synced"}' "Credentials synced from Bitwarden to Keychain"
|
||||||
|
else
|
||||||
|
printf 'Could not fetch Bitwarden item "%s". Using existing Keychain credentials.\n' "$BW_ITEM_NAME" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_dns_cleanup() {
|
||||||
|
_emit '{"event":"dns_cleanup"}' "Cleaning up VPN DNS..."
|
||||||
|
if [[ -x "$DNS_CLEANUP" ]]; then
|
||||||
|
sudo "$DNS_CLEANUP" || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for svc in "Wi-Fi" "USB 10/100/1000 LAN" "Ethernet"; do
|
||||||
|
local dns
|
||||||
|
dns="$(networksetup -getdnsservers "$svc" 2>/dev/null || true)"
|
||||||
|
if printf '%s\n' "$dns" | grep -q '^10\.'; then
|
||||||
|
sudo networksetup -setdnsservers "$svc" empty >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
sudo dscacheutil -flushcache >/dev/null 2>&1 || true
|
||||||
|
sudo killall -HUP mDNSResponder >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
_filter_output() {
|
||||||
|
local vpn_ip=""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if $DEBUG; then
|
||||||
|
printf '%s\n' "$line"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$line" =~ Configured\ as\ ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then
|
||||||
|
vpn_ip="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$line" =~ Session\ authentication\ will\ expire\ at\ (.+) ]]; then
|
||||||
|
local expiry_str="${BASH_REMATCH[1]}"
|
||||||
|
local expiry_ts expiry_local expiry_iso now_ts remaining hours mins
|
||||||
|
expiry_ts="$(date -jf "%a %b %d %H:%M:%S %Y" "$expiry_str" "+%s" 2>/dev/null || true)"
|
||||||
|
expiry_iso=""
|
||||||
|
if [[ -n "$expiry_ts" ]]; then
|
||||||
|
expiry_local="$(date -jf "%a %b %d %H:%M:%S %Y" "$expiry_str" "+%H:%M" 2>/dev/null || true)"
|
||||||
|
expiry_iso="$(date -r "$expiry_ts" -u "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || true)"
|
||||||
|
now_ts="$(date "+%s")"
|
||||||
|
remaining=$((expiry_ts - now_ts))
|
||||||
|
hours=$((remaining / 3600))
|
||||||
|
mins=$(((remaining % 3600) / 60))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$vpn_ip" ]]; then
|
||||||
|
_emit "{\"event\":\"connected\",\"ip\":\"$vpn_ip\",\"expires\":\"$expiry_iso\"}" "VPN connected - $vpn_ip"
|
||||||
|
else
|
||||||
|
_emit '{"event":"connected"}' "VPN connected"
|
||||||
|
fi
|
||||||
|
if [[ -n "${expiry_ts:-}" ]]; then
|
||||||
|
_emit "{\"event\":\"session_info\",\"ip\":\"${vpn_ip:-}\",\"expires\":\"$expiry_iso\",\"remaining_sec\":$remaining}" "Session until $expiry_local (${hours}h ${mins}m)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local dns_target
|
||||||
|
dns_target="$(scutil --dns 2>/dev/null | awk '/Supplemental/{found=1} found && /nameserver\[0\]/{split($0,a," : "); if(a[2] ~ /^10\./) {print a[2]; exit}}' || true)"
|
||||||
|
_write_status "{\"pid\":$$,\"state\":\"connected\",\"ip\":\"${vpn_ip:-}\",\"expires\":\"${expiry_iso:-}\",\"started_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"dns_target\":\"${dns_target:-}\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
||||||
|
|
||||||
|
$DEBUG || $JSON_MODE || printf '%s\n' "Ctrl+C to disconnect"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if $STATUS_MODE; then
|
||||||
|
_check_status
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $CONFIGURE_KEYCHAIN_MODE; then
|
||||||
|
_configure_keychain
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap '_dns_cleanup; _clear_status' EXIT
|
||||||
|
|
||||||
|
_patch_oc
|
||||||
|
_sync_bitwarden
|
||||||
|
|
||||||
|
_emit '{"event":"connecting"}' "Connecting to VPN (lemanapro)..."
|
||||||
|
_write_status "{\"pid\":$$,\"state\":\"connecting\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
||||||
|
|
||||||
|
display_mode="hidden"
|
||||||
|
log_level=""
|
||||||
|
if $DEBUG; then
|
||||||
|
display_mode="shown"
|
||||||
|
log_level="--log-level debug"
|
||||||
|
fi
|
||||||
|
|
||||||
|
reconnect_count=0
|
||||||
|
while true; do
|
||||||
|
QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu" \
|
||||||
|
"$OC_BIN" --browser-display-mode "$display_mode" $log_level 2>&1 \
|
||||||
|
| _filter_output
|
||||||
|
exit_code=${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
if [[ $exit_code -eq 0 ]]; then
|
||||||
|
_emit '{"event":"disconnected","reason":"user"}' "VPN disconnected"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
reconnect_count=$((reconnect_count + 1))
|
||||||
|
_emit "{\"event\":\"reconnecting\",\"attempt\":$reconnect_count,\"delay\":5}" "VPN exited with $exit_code. Reconnecting in 5s..."
|
||||||
|
sleep 5
|
||||||
|
_emit '{"event":"connecting"}' "Reconnecting..."
|
||||||
|
done
|
||||||
|
|
||||||
346
install.sh
Executable file
346
install.sh
Executable file
@@ -0,0 +1,346 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
APP_NAME="lemana-vpn"
|
||||||
|
DEFAULT_RAW_BASE_URL="http://192.168.50.109/dokril/lemana-vpn/raw/branch/main"
|
||||||
|
|
||||||
|
RAW_BASE_URL="${LEMANA_VPN_RAW_BASE_URL:-$DEFAULT_RAW_BASE_URL}"
|
||||||
|
INSTALL_BIN_DIR="${LEMANA_VPN_BIN_DIR:-$HOME/bin}"
|
||||||
|
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
|
||||||
|
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
||||||
|
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
||||||
|
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
||||||
|
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
||||||
|
USE_BITWARDEN=1
|
||||||
|
USE_TOUCHID=1
|
||||||
|
INSTALL_SUDOERS=1
|
||||||
|
INSTALL_ALIASES=1
|
||||||
|
CONFIGURE_KEYCHAIN=0
|
||||||
|
DRY_RUN=0
|
||||||
|
FORCE=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage:
|
||||||
|
sh install.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--with-bitwarden Install/use Bitwarden CLI module (default)
|
||||||
|
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
|
||||||
|
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
|
||||||
|
--without-touchid Do not install/use Touch ID helper
|
||||||
|
--configure-keychain Prompt for LDAP password/TOTP after install
|
||||||
|
--username VALUE Corporate LDAP username (default: 60103293)
|
||||||
|
--bw-item VALUE Bitwarden item name (default: LM LDAP)
|
||||||
|
--raw-base-url URL Raw file base URL for curl installs
|
||||||
|
--no-sudoers Do not install sudoers rules
|
||||||
|
--no-shell Do not update ~/.zshrc aliases
|
||||||
|
--minimal Same as --without-bitwarden --without-touchid
|
||||||
|
--dry-run Print actions without changing files
|
||||||
|
--force Reinstall files even when present
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
sh install.sh
|
||||||
|
sh install.sh --minimal --configure-keychain
|
||||||
|
sh install.sh --without-touchid
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--with-bitwarden) USE_BITWARDEN=1 ;;
|
||||||
|
--without-bitwarden) USE_BITWARDEN=0 ;;
|
||||||
|
--with-touchid) USE_TOUCHID=1 ;;
|
||||||
|
--without-touchid) USE_TOUCHID=0 ;;
|
||||||
|
--configure-keychain) CONFIGURE_KEYCHAIN=1 ;;
|
||||||
|
--username)
|
||||||
|
shift
|
||||||
|
[ "$#" -gt 0 ] || { echo "--username requires a value" >&2; exit 1; }
|
||||||
|
USERNAME="$1"
|
||||||
|
;;
|
||||||
|
--bw-item)
|
||||||
|
shift
|
||||||
|
[ "$#" -gt 0 ] || { echo "--bw-item requires a value" >&2; exit 1; }
|
||||||
|
BW_ITEM="$1"
|
||||||
|
;;
|
||||||
|
--raw-base-url)
|
||||||
|
shift
|
||||||
|
[ "$#" -gt 0 ] || { echo "--raw-base-url requires a value" >&2; exit 1; }
|
||||||
|
RAW_BASE_URL="${1%/}"
|
||||||
|
;;
|
||||||
|
--no-sudoers) INSTALL_SUDOERS=0 ;;
|
||||||
|
--no-shell) INSTALL_ALIASES=0 ;;
|
||||||
|
--minimal)
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
;;
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
--force) FORCE=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '%s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf 'ERROR: %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
printf '+'
|
||||||
|
for arg in "$@"; do
|
||||||
|
printf ' %s' "$arg"
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || die "Command not found: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
script_dir() {
|
||||||
|
case "$0" in
|
||||||
|
*/*) cd "$(dirname "$0")" 2>/dev/null && pwd ;;
|
||||||
|
*) pwd ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
download_file() {
|
||||||
|
src="$1"
|
||||||
|
dst="$2"
|
||||||
|
local_dir="$(script_dir)"
|
||||||
|
|
||||||
|
if [ -f "$local_dir/$src" ]; then
|
||||||
|
run cp "$local_dir/$src" "$dst"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$PWD/$src" ]; then
|
||||||
|
run cp "$PWD/$src" "$dst"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
need_cmd curl
|
||||||
|
run curl -fsSL "$RAW_BASE_URL/$src" -o "$dst"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_file() {
|
||||||
|
dst="$1"
|
||||||
|
content="$2"
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
printf '+ write %s\n' "$dst"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$content" > "$dst"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_homebrew_packages() {
|
||||||
|
need_cmd brew
|
||||||
|
|
||||||
|
for pkg in openconnect pipx; do
|
||||||
|
if brew list "$pkg" >/dev/null 2>&1; then
|
||||||
|
log "Homebrew package already installed: $pkg"
|
||||||
|
else
|
||||||
|
log "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"
|
||||||
|
else
|
||||||
|
log "Installing Homebrew package: bitwarden-cli"
|
||||||
|
run brew install bitwarden-cli
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_openconnect_lite() {
|
||||||
|
need_cmd pipx
|
||||||
|
|
||||||
|
if [ -x "$HOME/.local/bin/openconnect-lite" ] && [ "$FORCE" -eq 0 ]; then
|
||||||
|
log "openconnect-lite already installed"
|
||||||
|
else
|
||||||
|
log "Installing openconnect-lite via pipx"
|
||||||
|
run pipx install openconnect-lite
|
||||||
|
fi
|
||||||
|
|
||||||
|
if pipx --help 2>/dev/null | grep -q ' pin '; then
|
||||||
|
run pipx pin openconnect-lite >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_cli() {
|
||||||
|
tmp="$1"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_config() {
|
||||||
|
tmp="$1"
|
||||||
|
run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR"
|
||||||
|
|
||||||
|
download_file "templates/openconnect-lite-config.toml" "$tmp/openconnect-lite-config.toml"
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
printf '+ render %s/config.toml\n' "$OC_CONFIG_DIR"
|
||||||
|
else
|
||||||
|
sed "s/{{USERNAME}}/$USERNAME/g" "$tmp/openconnect-lite-config.toml" > "$OC_CONFIG_DIR/config.toml"
|
||||||
|
chmod 600 "$OC_CONFIG_DIR/config.toml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
|
||||||
|
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
|
||||||
|
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dns_cleanup() {
|
||||||
|
tmp="$1"
|
||||||
|
|
||||||
|
download_file "libexec/lemana-vpn-dns-cleanup" "$tmp/lemana-vpn-dns-cleanup"
|
||||||
|
log "Installing DNS cleanup wrapper: $DNS_CLEANUP"
|
||||||
|
run sudo install -m 755 -o root -g wheel "$tmp/lemana-vpn-dns-cleanup" "$DNS_CLEANUP"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_sudoers() {
|
||||||
|
[ "$INSTALL_SUDOERS" -eq 1 ] || return 0
|
||||||
|
|
||||||
|
openconnect_bin="$(brew --prefix)/bin/openconnect"
|
||||||
|
[ -x "$openconnect_bin" ] || openconnect_bin="$(command -v openconnect || true)"
|
||||||
|
[ -n "$openconnect_bin" ] || die "openconnect binary not found"
|
||||||
|
|
||||||
|
tmp="$1"
|
||||||
|
current_user="$(id -un)"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
install_touchid_helper() {
|
||||||
|
[ "$USE_TOUCHID" -eq 1 ] || return 0
|
||||||
|
|
||||||
|
if [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ] && [ "$FORCE" -eq 0 ]; then
|
||||||
|
log "Touch ID helper already installed: $INSTALL_BIN_DIR/keychain-fingerprint"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
need_cmd git
|
||||||
|
need_cmd swiftc
|
||||||
|
|
||||||
|
tmp="$1"
|
||||||
|
log "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"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_shell_aliases() {
|
||||||
|
[ "$INSTALL_ALIASES" -eq 1 ] || return 0
|
||||||
|
|
||||||
|
zshrc="$HOME/.zshrc"
|
||||||
|
tmp="$1"
|
||||||
|
[ -f "$zshrc" ] || run touch "$zshrc"
|
||||||
|
|
||||||
|
block="$tmp/zshrc-block"
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
printf '+ update %s aliases\n' "$zshrc"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$block" <<EOF
|
||||||
|
# >>> lemana-vpn
|
||||||
|
vpn() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" "\$@"; }
|
||||||
|
vpn-debug() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --debug "\$@"; }
|
||||||
|
vpn-fix-dns() { sudo "$DNS_CLEANUP"; }
|
||||||
|
# <<< lemana-vpn
|
||||||
|
EOF
|
||||||
|
|
||||||
|
awk '
|
||||||
|
/^# >>> lemana-vpn$/ { skip=1; next }
|
||||||
|
/^# <<< lemana-vpn$/ { skip=0; next }
|
||||||
|
skip != 1 { print }
|
||||||
|
' "$zshrc" > "$tmp/zshrc"
|
||||||
|
|
||||||
|
{
|
||||||
|
cat "$tmp/zshrc"
|
||||||
|
printf '\n'
|
||||||
|
cat "$block"
|
||||||
|
} > "$tmp/zshrc.new"
|
||||||
|
|
||||||
|
mv "$tmp/zshrc.new" "$zshrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_login_bitwarden() {
|
||||||
|
[ "$USE_BITWARDEN" -eq 1 ] || return 0
|
||||||
|
command -v bw >/dev/null 2>&1 || return 0
|
||||||
|
|
||||||
|
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"
|
||||||
|
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."
|
||||||
|
else
|
||||||
|
log "Bitwarden CLI is available."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
[ "$(uname -s)" = "Darwin" ] || die "This installer supports macOS only"
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||||
|
|
||||||
|
log "Installing Lemana VPN"
|
||||||
|
log "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES"
|
||||||
|
|
||||||
|
install_homebrew_packages
|
||||||
|
install_openconnect_lite
|
||||||
|
install_cli "$tmp"
|
||||||
|
install_config "$tmp"
|
||||||
|
install_dns_cleanup "$tmp"
|
||||||
|
install_sudoers "$tmp"
|
||||||
|
install_touchid_helper "$tmp"
|
||||||
|
install_shell_aliases "$tmp"
|
||||||
|
maybe_login_bitwarden
|
||||||
|
|
||||||
|
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
||||||
23
libexec/lemana-vpn-dns-cleanup
Executable file
23
libexec/lemana-vpn-dns-cleanup
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SERVICES="${LEMANA_VPN_DNS_SERVICES:-Wi-Fi
|
||||||
|
USB 10/100/1000 LAN
|
||||||
|
Ethernet}"
|
||||||
|
|
||||||
|
printf '%s\n' "$SERVICES" | while IFS= read -r service; do
|
||||||
|
[ -n "$service" ] || continue
|
||||||
|
|
||||||
|
dns="$(/usr/sbin/networksetup -getdnsservers "$service" 2>/dev/null || true)"
|
||||||
|
printf '%s\n' "$dns" | grep -q '^10\.' || continue
|
||||||
|
|
||||||
|
/usr/sbin/networksetup -setdnsservers "$service" empty >/dev/null 2>&1 || {
|
||||||
|
printf 'Failed to reset DNS for %s\n' "$service" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
printf 'DNS reset for %s\n' "$service"
|
||||||
|
done
|
||||||
|
|
||||||
|
/usr/bin/dscacheutil -flushcache >/dev/null 2>&1 || true
|
||||||
|
/usr/bin/killall -HUP mDNSResponder >/dev/null 2>&1 || true
|
||||||
|
|
||||||
32
templates/openconnect-lite-config.toml
Normal file
32
templates/openconnect-lite-config.toml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
on_disconnect = "sudo /usr/local/sbin/lemana-vpn-dns-cleanup >/dev/null 2>&1 || true"
|
||||||
|
|
||||||
|
[default_profile]
|
||||||
|
address = "vpn.lemanapro.ru"
|
||||||
|
user_group = ""
|
||||||
|
name = "Default-K"
|
||||||
|
|
||||||
|
[credentials]
|
||||||
|
username = "{{USERNAME}}"
|
||||||
|
|
||||||
|
[auto_fill_rules]
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://employee.auth.lemanapro.ru/*"]]
|
||||||
|
selector = "span#input-error"
|
||||||
|
action = "stop"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://employee.auth.lemanapro.ru/*"]]
|
||||||
|
selector = "input#username"
|
||||||
|
fill = "username"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://employee.auth.lemanapro.ru/*"]]
|
||||||
|
selector = "input#password"
|
||||||
|
fill = "password"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://employee.auth.lemanapro.ru/*"]]
|
||||||
|
selector = "input#otp, input[name=otp], form input:not(#username):not(#password):not([type=hidden]):not([type=submit]):not([type=button])"
|
||||||
|
fill = "totp"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://employee.auth.lemanapro.ru/*"]]
|
||||||
|
selector = "input[type=submit], button[type=submit], #kc-login"
|
||||||
|
action = "click"
|
||||||
|
|
||||||
Reference in New Issue
Block a user