タイトル通りの小ネタです。
MacのCLIからCisco AnyConnectでVPNをポチれるツールです。
以下の記事参考にしました。
先人に感謝です、ありがとうございます。
そして、コード作成はClaude+Bedrockです。
サクッとオプションやエラーハンドリングを実装出来ています。
いや、ちょっとヤバい時代ですね。
README
アーキテクチャ
-
vpn.sh: メインの自動化スクリプト
- コマンドライン引数による機能選択
-
security
コマンドを使用したmacOSキーチェーンとの統合 - Cisco AnyConnectプロセスのライフサイクル管理
- vpn_completion.sh: タブ補完用
設定項目
スクリプト内の設定変数:
-
VPN_USER
: VPNユーザー名 -
VPN_SERVER
: VPNサーバーアドレス
必要に応じて変更可能な変数:
-
cisco_vpn
: Cisco AnyConnectのインストールパス(標準:/opt/cisco/anyconnect/bin/vpn
) -
KEYCHAIN_ACCOUNT
: キーチェーンアカウント名(標準:account_name
) -
KEYCHAIN_SERVICE
: キーチェーンサービス名(標準:cisco_vpn_password
)
セットアップ
実行権限の設定
# パスは実際にvpn.shを配置した場所に変更すること
chmod +x /path/to/vpn.sh
エイリアス設定
# ~/.bash_profile または ~/.zshrc に追加
# パスは実際にvpn.shを配置した場所に変更すること
alias vpn='/path/to/vpn.sh'
設定後は以下のようにコマンドで実行可能
vpn # VPN接続(初回時は自動でパスワード設定)
vpn connect # VPN接続
vpn disconnect # VPN切断
vpn status # 接続状態確認
vpn rotate # パスワード更新
vpn remove # パスワード削除
タブ補完設定
# bash_profileまたはzshrcに追加
# パスは実際にvpn_completion.shを配置した場所に変更すること
source /path/to/vpn_completion.sh
シェルスクリプト
vpn.sh
#!/bin/bash
set -eu
# 設定
VPN_USER='hoge'
VPN_SERVER='hoge.com/hoge'
cisco_vpn='/opt/cisco/anyconnect/bin/vpn'
KEYCHAIN_ACCOUNT='account_name'
KEYCHAIN_SERVICE='cisco_vpn_password'
# ====================
# ユーティリティ関数
# ====================
# Cisco AnyConnectのインストール状態をチェックする関数
check_cisco_installed() {
if [ ! -e $cisco_vpn ]; then
echo "Cisco AnyConnectがインストールされていません" >&2
exit 1
fi
}
# VPN接続成功時の処理関数
connection_success() {
echo "VPNに接続しました。"
exit 0
}
# ====================
# キーチェーン関数
# ====================
# キーチェーンからパスワードを取得する関数
get_password() {
security find-generic-password -a "$KEYCHAIN_ACCOUNT" -s "$KEYCHAIN_SERVICE" -w 2>/dev/null
}
# キーチェーンパスワード設定関数(共通)
set_keychain_password() {
local prompt_msg="$1"
local success_msg="$2"
echo "$prompt_msg"
echo -n "VPNパスワードを入力してください: "
read -s password
echo
# -Uオプションで存在する場合は更新、ない場合は追加
if security add-generic-password -U -a "$KEYCHAIN_ACCOUNT" -s "$KEYCHAIN_SERVICE" -w "$password" 2>/dev/null; then
echo "$success_msg"
else
echo "エラー: パスワードの保存/更新に失敗しました。"
exit 1
fi
}
# キーチェーンパスワード初期設定関数(内部用)
setup_keychain() {
set_keychain_password "キーチェーンにVPNパスワードを設定します。" "パスワードがキーチェーンに保存されました。"
}
# キーチェーンパスワードローテート関数
rotate_keychain() {
set_keychain_password "キーチェーンのVPNパスワードをローテートします。" "パスワードが更新されました。"
}
# パスワードが利用可能か確認する関数(必要に応じて対話的セットアップ)
ensure_password() {
if PASS=$(get_password 2>/dev/null) && [ -n "$PASS" ]; then
return 0
fi
echo "キーチェーンにパスワードが設定されていません。"
echo "VPNに接続するにはパスワードの設定が必要です。"
echo ""
echo -n "パスワードを設定しますか? (y/n): "
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
setup_keychain
if PASS=$(get_password 2>/dev/null) && [ -n "$PASS" ]; then
return 0
else
echo "エラー: パスワードの設定に失敗しました。"
exit 1
fi
else
echo ""
echo "パスワードが設定されていないため、接続を中止します。"
echo "次回接続時にパスワード設定が再度促されます。"
exit 1
fi
}
# キーチェーンパスワードを削除する関数
remove_keychain() {
echo "キーチェーンからVPNパスワードを削除します。"
echo "削除すると次回VPN接続時に再度パスワードの入力が必要になります。"
echo "本当に削除しますか? (y/n)"
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
if security delete-generic-password -a "$KEYCHAIN_ACCOUNT" -s "$KEYCHAIN_SERVICE" 2>/dev/null; then
echo "パスワードがキーチェーンから削除されました。"
else
echo "エラー: パスワードの削除に失敗しました。パスワードが存在しない可能性があります。"
exit 1
fi
else
echo "削除をキャンセルしました。"
fi
}
# ====================
# VPNヘルパー関数
# ====================
# VPNが接続されているかチェックする関数(内部用)
is_vpn_connected() {
[ -e $cisco_vpn ] || return 1
$cisco_vpn state 2>/dev/null | grep -q "Connected"
}
# VPN接続を試行する関数
# ユーザー名とパスワードをCisco VPNクライアントに送信して接続を開始
attempt_vpn_connection() {
printf "${VPN_USER}\n${PASS}" | $cisco_vpn -s connect "$VPN_SERVER" 2>&1
}
# 接続成功をチェックする関数
is_connection_successful() {
echo "$1" | grep -q "state: Connected"
}
# 認証失敗をチェックする関数
has_auth_failure() {
echo "$1" | grep -qi "authentication failed\|login failed\|connection failed\|access denied"
}
# ====================
# メインVPN関数
# ====================
# VPN状態をチェックする関数(statusコマンド用)
check_vpn_status() {
check_cisco_installed
if is_vpn_connected; then
echo "VPN接続中"
else
echo "VPN未接続"
fi
}
# VPNを切断する関数
disconnect_vpn() {
check_cisco_installed
echo "VPNから切断中..."
if $cisco_vpn disconnect > /dev/null 2>&1; then
echo "VPNから切断しました。"
else
echo "VPN切断中にエラーが発生しました。"
exit 1
fi
}
# VPNを接続する関数
connect_vpn() {
# パスワードが利用可能か確認(必要に応じて対話的セットアップ)
ensure_password
check_cisco_installed
# 干渉する可能性のあるCisco GUIプロセスをクリーンアップ
if pgrep -f "Cisco AnyConnect" > /dev/null; then
echo "既存のCisco AnyConnect GUIプロセスを終了します..."
if pkill -f "Cisco AnyConnect"; then
echo "GUIプロセスを終了しました。"
sleep 2 # プロセスクリーンアップを待つ
else
echo "警告: GUIプロセス終了に失敗しました。手動で終了してください。"
fi
fi
# 既に接続済みの場合は再接続不要
if is_vpn_connected; then
echo "VPNは既に接続されています。"
exit 0
fi
echo "VPNに接続中..."
# VPN接続を実行(ここで実際の接続試行が行われる)
output=$(attempt_vpn_connection)
exit_code=$?
# "Connected"状態を確認して接続成功をチェック
if is_connection_successful "$output"; then
connection_success
fi
# 再試行が必要な重大エラーをチェック
if [ $exit_code -ne 0 ] || has_auth_failure "$output"; then
echo "VPN接続中にエラーが発生しました。プロセスを終了して再試行します。"
echo "エラー詳細: $output"
pkill vpn 2>/dev/null
pkill cisco 2>/dev/null
# プロセス終了を待つ
sleep 3
output=$(attempt_vpn_connection)
if is_connection_successful "$output"; then
connection_success
elif has_auth_failure "$output"; then
echo "再試行後もエラーが発生しました。接続に失敗しました。"
echo "エラー詳細: $output"
exit 1
fi
fi
# 警告は表示するがエラーとして扱わない
if echo "$output" | grep -qi "warning\|cert.*expired"; then
echo "警告が発生しましたが、接続を継続します。"
echo "警告内容:"
echo "$output" | grep -i "warning\|cert.*expired"
fi
# 接続が実際に成功したかチェック
sleep 2
if is_vpn_connected; then
connection_success
else
echo "接続処理は完了しましたが、VPN状態の確認に失敗しました。"
echo "手動で接続状態を確認してください: $0 status"
exit 1
fi
}
# ====================
# UI関数
# ====================
# 使用方法を表示する関数
show_usage() {
echo "使用方法: $0 [オプション]"
echo "オプション:"
echo " connect VPNに接続 (デフォルト、初回時は自動でパスワード設定)"
echo " disconnect VPNから切断"
echo " rotate キーチェーンのパスワードを更新"
echo " remove キーチェーンからパスワードを削除"
echo " status VPN接続状態を確認"
echo " -h, --help このヘルプを表示"
}
# ====================
# メインスクリプト
# ====================
# コマンドライン引数を解析
if [ $# -eq 0 ]; then
# デフォルトアクションは接続
connect_vpn
else
case "$1" in
connect)
connect_vpn
;;
disconnect)
disconnect_vpn
;;
rotate)
rotate_keychain
;;
remove)
remove_keychain
;;
status)
check_vpn_status
;;
-h|--help)
show_usage
;;
*)
echo "不明なオプション: $1"
show_usage
exit 1
;;
esac
fi
タブ補完
vpn_completion.sh
#!/bin/bash
# vpn.sh用のBash補完設定
_vpn_completion() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# 利用可能なオプション
opts="connect disconnect rotate remove status"
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
# vpn.sh用の補完を登録
complete -F _vpn_completion vpn.sh
complete -F _vpn_completion ./vpn.sh