【小ネタ】MacのCLIからCisco AnyConnectでVPN接続する〜AIでキレイにしたやつ〜

タイトル通りの小ネタです。
MacのCLIからCisco AnyConnectでVPNをポチれるツールです。

![スクリーンショット 2025-06-23 14.35.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3990358/97f46309-fb95-4b92-b12f-df0b2f9d3515.png)

以下の記事参考にしました。
先人に感謝です、ありがとうございます。

https://qiita.com/mamohacy/items/bcd3f575a9555d2f18a4

https://qiita.com/mr91i/items/b7770104a69fd87a52fd

そして、コード作成は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`）

## セットアップ

### 実行権限の設定

```bash
# パスは実際にvpn.shを配置した場所に変更すること

chmod +x /path/to/vpn.sh
```

### エイリアス設定

```bash
# ~/.bash_profile または ~/.zshrc に追加
# パスは実際にvpn.shを配置した場所に変更すること
alias vpn='/path/to/vpn.sh'
```

設定後は以下のようにコマンドで実行可能
```bash
vpn              # VPN接続（初回時は自動でパスワード設定）
vpn connect      # VPN接続
vpn disconnect   # VPN切断
vpn status       # 接続状態確認
vpn rotate       # パスワード更新
vpn remove       # パスワード削除
```

### タブ補完設定

```bash
# 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
```