Cisco AnyConnect を使った VPN への接続を自動化する （Mac 版）

# これは何？

macOS でこれの入力を自動化するやつ。 

![スクリーンショット 2020-10-02 12.32.00 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25060/128f028b-34b6-b2f6-134f-2045ab848bbd.png)

Cisco AnyConnect クライアントは VPN 接続で Multi-Factor Authentication (MFA) をサポートしているが， これを毎回接続時に実行するのは死ぬほど面倒くさい。というわけで自動化を試みてみた。

当初は AppleScript を使って GUI 操作の自動化を試みていたが，このアプリケーションのクセが非常に強かったので断念。 CLI からの自動化を試みて，後から GUI 版を起動するという解決策を採る。

# 準備手順

## Cisco AnyConnect のインストール

通常，企業から支給された PC にはインストールされていると思います。未インストールの場合は各自ダウンロードしてきてインストールしてください。

**GUI アプリケーションには CLI コマンドも同梱されているので， これを `vpn` コマンドで呼び出せるようにパスを通すかエイリアスを張るかしておいてください。自分の環境では以下の場所にインストールされていました。**

```
/opt/cisco/anyconnect/bin/vpn
```

## `oathtool` コマンドのインストール

```bash
brew install oath-toolkit
```

MFA の TOTP 発行のために必要です。

## キーチェーンへの登録

macOS の 「キーチェーンアクセス」にあらかじめ以下の秘密情報を登録しておいてください。

- **パスワード**
- **MFA のトークン発行元となるシークレットの BASE 32 エンコード値**

![スクリーンショット 2020-10-02 12.39.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25060/037f1747-b765-b6c4-d9ea-16038748695c.png)

- 「アカウント名」はここでは使用しないので何でもいいです。
- もしシークレットが QR コードで提供されている場合は， `zbarimg` および `php` を使って読み取ってください。

```bash
# インストール
brew install zbar

# パースして出力
zbarimg -q 画像ファイルへのパス | php -r 'parse_str(parse_url(fgets(STDIN), PHP_URL_QUERY), $_); echo $_["secret"];'
```

## スクリプトの整備

以下のコマンドを，普段お使いのシェルが Zsh の場合には `~/.zshrc` に， Bash の場合には `~/.bashrc` にて登録してください。

```bash
vpn() (
  # エラー対応
  set -eu

  # パラメータの設定
  export VPN_HOST='接続先'
  export VPN_GROUP=${VPN_GROUP:=デフォルトのグループ番号} 
  export VPN_USERNAME='ユーザ名'
  export VPN_PASSWORD_KEYNAME='パスワードキーチェーン登録名'
  export VPN_SECOND_USERNAME='第2ユーザ名'
  export VPN_TOTP_SECRET_KEYNAME='シークレットキーチェーン登録名'

  # "vpn" "vpn connect" 以外の呼び出しは元のコマンドに流す
  if [[ "$#" -ne 0 && ( "$#" -ne 1 || "$1" != 'connect' ) ]]; then
    exec command vpn "$@"
  fi

  # GUI 版が起動していたら一度強制終了する
  killall 'Cisco AnyConnect Secure Mobility Client' >/dev/null 2>&1 || :

  # 既に VPN に接続していたら強制切断する
  expect -c '
    set log_user 0
    set timeout 5
    spawn vpn disconnect
    expect ">> state: Disconnected"
    interact
  '

  # キーチェーンからの取得
  VPN_PASSWORD="$(security find-generic-password -s "$VPN_PASSWORD_KEYNAME" -w)"
  VPN_TOKEN="$(oathtool --totp --base32 "$(security find-generic-password -s "$VPN_TOTP_SECRET_KEYNAME" -w)")"
  export VPN_PASSWORD
  export VPN_TOKEN

  # 接続
  expect -c '
    set log_user 0
    set timeout 5

    spawn vpn connect $env(VPN_HOST)

    expect "Group:"
    send "$env(VPN_GROUP)\r"
    expect "Username:"
    send "$env(VPN_USERNAME)\r"
    expect "Password:"
    send "$env(VPN_PASSWORD)\r"
    expect "Second Username:"
    send "$env(VPN_SECOND_USERNAME)\r"
    expect "Second Password:"
    send "$env(VPN_TOKEN)\r"

    interact
  '

  # GUI 版を起動
  open '/Applications/Cisco/Cisco AnyConnect Secure Mobility Client.app'
)
```

- 環境変数を利用して，エスケープ処理などを考慮せずに簡単に下位のプロセスに値を渡せるようにしています。
- シェルを単なるサブシェル `{}` ではなく別プロセス `()` として起動させることで， `export` や `set -eu` の影響が外に出ないようにしています。
- GUI 版起動状態では CLI 版での操作は行えませんが， **CLI 版で接続済みの状態で GUI 版を起動すると状態の引き継ぎが行えるようです**。ここではその性質を利用して最後に GUI アプリケーションの起動を行っています。

やっぱり接続状態が視覚的に見えるほうがいいですよね↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25060/3cd6e884-883d-5c48-ecc4-7a3d172cfde8.png)

# 使い方

- `vpn` または `vpn connect` で接続
  - パスワード入力を求められます。 残念ながら **Touch ID は使えない**ようなので， 利便性を考えると**常に許可**を押しておくのが現状妥当な選択肢かなと思います。
  - グループ番号を変更したいときは `VPN_GROUP=xxx vpn`
- `vpn disconnect` で切断
- `vpn state` で接続状態確認
- 元のコマンドをそのまま実行したいときは `command vpn`