/etc/ppp/ip-up に route コマンドを書き、社内トンネルに再接続しても、そのファイルは決して実行されません。スクリプトに問題はありません。その接続は PPP をまったく通らないからです。内蔵の Cisco IPSec と IKEv2 クライアントは pppd を起動しないため、フック用ディレクトリは読まれません。一方、PPTP と IPSec 上の L2TP はいまもそこを通ります。どちらのケースかを判断し、PPP でないほうを launchd の監視ジョブから起動してください。

フックを編集する前にインターフェースを確認する

二つのコマンドで、どの仕組みが使えるかが決まります。設定済みのサービスを一覧し、次にトンネルが上がっている間に存在するインターフェースを一覧します。証拠はインターフェース名であり、システム設定のラベルではありません。

scutil --nc list
ifconfig -l | tr ' ' '\n' | grep -E '^(utun|ppp)[0-9]+$'

ppp0 を生成するサービスは PPP フックを実行し、あなたのファイルを走らせます。Cisco IPSec や IKEv2 のサービスは代わりに utun デバイスを生成します。PPP 層を持たないトンネルインターフェースなので、/etc/ppp の中の何もそこには届きません。両方の種類が現れたら、どちらのサービスに接続したかを確認してください。クライアント名を読んで間違ったフックに午後を浪費する人がいます。

PPP の経路では、ファイルの所有者が root で実行可能でなければなりません。pppd は何かを実行する前に両方を確認するからです。install は所有者とモードを一度に設定します。

sudo install -o root -g wheel -m 0755 /dev/null /etc/ppp/ip-up
sudo ls -l /etc/ppp/ip-up

Finder やグラフィカルエディタで作ったファイルは、自分のアカウント所有になったり実行ビットがなかったりして、存在しないフックと同じくらい静かに失敗します。いま隣に ip-down を置いてください。所有者の列に root と wheel、モードの列に rwxr-xr-x があるのが望ましい形です。

トンネルが現れたら launchd にスクリプトを起動させる

PPP に触れないクライアントでは、きっかけを別の場所から取る必要があり、システム構成ストアが正直な情報源です。macOS はトンネルの遷移のたびにこれを書き換えるので、そのディレクトリを監視する launchd ジョブは、トンネルが現れたり消えたりすると呼ばれます。

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
  <key>WatchPaths</key>
  <array><string>/Library/Preferences/SystemConfiguration</string></array>
  <key>Label</key><string>net.example.tunnel-watch</string>
  <key>ProgramArguments</key>
  <array><string>/bin/sh</string><string>/usr/local/sbin/tunnel-hook</string></array>
  <key>RunAtLoad</key><true/>
  <key>ThrottleInterval</key><integer>20</integer>
</dict>
</plist>

ユーザー単位のエージェントではなく launch デーモンとして導入してください。経路の修復はマシンに属するからです。bootstrap は現在のリリースで古い load -w 形式を置き換えます。RunAtLoad は最初の実行を観測可能にし、ThrottleInterval は Wi-Fi 接続の変化が続いてもスクリプトが繰り返し起動するのを防ぎます。

sudo install -o root -g wheel -m 0644 tunnel-watch.plist \
  /Library/LaunchDaemons/net.example.tunnel-watch.plist
sudo launchctl bootstrap system \
  /Library/LaunchDaemons/net.example.tunnel-watch.plist

罠は、監視対象のディレクトリが VPN と無関係な出来事でも変わることです。Wi-Fi 接続、DHCP の更新、ケーブルの挿入などです。下のガードは騒がしいきっかけを使えるフックに変え、ログの一行が妙な時刻の経路を説明します。

所有権はスクリプトだけでなく plist にも及びます。自分のアカウントが書き込めるディレクトリのファイルを実行するデーモンは、そのアカウントに root を渡すことになります。

二度目の実行で何も変わらないようスクリプトを守る

スクリプトはまずトンネルの有無を判断し、なければ静かに終了します。その後のすべては繰り返しても安全でなければなりません。きっかけは一回の接続よりはるかに頻繁に発火するからです。次の接続で変わるインデックスを固定せず、実行時にインターフェースを解決してください。

#!/bin/sh
set -u
TUN=$(ifconfig -l | tr ' ' '\n' | grep -m1 '^utun[0-9]' || true)
[ -n "$TUN" ] || exit 0
netstat -rn -f inet | grep -q '^10\.42' && exit 0
route -n add -net 10.42.0.0/16 -interface "$TUN"

route add は経路がすでにあると非ゼロの状態で終了するため、非ゼロをすべて失敗とみなすスクリプトは、正しいことをするたびに壊れたフックを報告します。上の netstat のテストはその場合を明示的な no-op に変えます。

インターフェースの存在は見た目より弱い証拠です。他の VPN クライアントや一部のコンテナ環境も utun デバイスを作るからです。scutil --nc status "Office VPN" でサービス自体に問い合わせ、ルーティングに触る前に接続済みの状態を必須にしてください。一週間後にはその記録が唯一の説明になるので、答えはログの一行に残してください。

DNS を壊さずにゲートウェイを取り戻す

トンネルが既定経路を奪うと、普通の閲覧は社内を通り抜け、ローカルのインターネット経路が動かなくなります。まず物理インターフェースのルータを取得し、既定経路をそこへ戻してください。DHCP は、すでにトンネル自身の既定を持つ表を解析するより確実です。

PHYS=en0
GW=$(ipconfig getoption "$PHYS" router)
[ -n "$GW" ] && sudo route -n change default "$GW"

リゾルバは別のツールを持つ別の層で、経路を直しても直りません。scutil --dns で何が有効かを見て、それを生んでいるものを直してください。サービスの順序と物理サービスの DNS エントリです。キャッシュの消去はネットワーク切り替え後に固まった名前解決を診断するもので、予定して行うものではありません。

Wi-Fi の変化を乗り越えたのに DNS が止まるトンネルは三つ目の不具合です。sudo killall -HUP mDNSResponder でリゾルバに設定を読み直させます。これは一度だけ、手で使ってください。

ジョブが自ら発火することを証明する

監視経路を信じる前に kickstart -k で一度だけ強制実行し、ジョブが記録した終了状態を読んでください。きれいな終了はスクリプトが動く証拠で、きっかけが動く証拠ではありません。次にジョブを読み込んだまま切断して再接続し、頼んでいない実行を観察してください。

sudo launchctl kickstart -k system/net.example.tunnel-watch
sudo launchctl print system/net.example.tunnel-watch | grep -E 'last exit code|state ='

試すたびに前後で scutil --nc status を確認してください。トンネルが落ちている間に走ったスクリプトは、接続経路について何も証明しません。

撤去側の分岐も接続側と同じ扱いが必要です。経路を手で削除し、ジョブを走らせ、戻ることを確認してください。経路を追加して決して削除しないフックは、一週間後に不調になるマシンを残し、二つの出来事を結ぶものは何もありません。両方の振る舞いが保てたら、sudo launchctl bootout system/net.example.tunnel-watch でジョブをきれいに退職させます。読み込んだままにした監視デーモンは、一度きりの修理が数か月後に説明のつかない経路になる道です。

関連記事