Добавь установщик 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