ある朝、Macからインターネットに出られなくなりました。ただし完全に孤立しているわけではなく、同じLAN内に置いている自宅サーバーにはSSHで入れる。Wi-Fiのアイコンも普通に繋がっている顔をしている。
こういう「半分だけ死んでいる」状態は原因の見当がつきにくいのですが、順番に切り分けたらはっきりした犯人が出てきたので、手順ごと残しておきます。
症状
- ブラウザがどのサイトも開かない
- 同じLAN内のサーバー(Ubuntu)にはSSHで入れる
- そのサーバーからは問題なくインターネットに出られる
- Wi-Fiは「接続済み」表示
サーバー側が外に出られている時点で、ルーターもプロバイダも正常です。つまりMac固有の問題だと分かります。
切り分け1: DNSなのか経路なのか
まずこの2つを潰します。
ping -c2 1.1.1.1 # 通る → DNSの問題
dig +short github.com
ping 1.1.1.1 が通るのに dig が返らないならDNS、という定番の切り分けです。今回は ping の時点で100%パケットロスでした。名前解決ではなく、そもそもIPパケットが目的地に向かっていない。
切り分け2: デフォルトルートを見る
route -n get default
route to: default
destination: default
interface: ipsec0
flags: <UP,DONE,CLONING,STATIC,GLOBAL>
mtu: 1280
ipsec0。VPN用のインターフェースです。心当たりがないのに、外向きの通信が全部ここに吸い込まれていました。
核心はnetstatの「フラグ」
ルーティングテーブル全体を見ると、デフォルトルートが3本ありました。
netstat -rn -f inet | head
Destination Gateway Flags Netif
default link#20 UCSg ipsec0
default 192.168.1.1 UGScIg en0
default link#21 UCSIg utun4
3本もあるのに繋がらないのはなぜか。ここで効いてくるのがフラグの I です。これは RTF_IFSCOPE、つまり「このインターフェース宛と明示された通信にだけ使う」という限定つきの経路であることを示します。
macOSは各ネットワークサービスごとにスコープ付きのデフォルトルートを用意したうえで、プライマリサービスのぶんだけスコープなしのデフォルトルートを1本置きます。行き先を指定しない普通の通信はこのスコープなしの1本を使います。
上の表で I が付いていないのは ipsec0 だけです。en0(Wi-Fi、ゲートウェイ192.168.1.1)もTailscaleのutun4も、スコープ付きしか持っていない。死んだVPNが唯一のグローバルな出口を握り続けていた、というのが症状の正体でした。
LAN内のサーバーに届いていたのは、それが同じサブネット(192.168.1.0/24)の直結経路で、デフォルトルートを一切通らないからです。「LANは生きているのに外に出られない」の説明がこれで付きます。
応急処置
VPNインターフェースを落とします。
sudo ifconfig ipsec0 down
ここでもう一度pingすると、エラーの内容が変わりました。
ping: sendto: No route to host
タイムアウト(投げたが返らない)から「No route to host」(投げる先がない)への変化です。読みが当たっていた裏付けになりますが、まだ繋がりません。ipsec0 が消えた結果、スコープなしのデフォルトルートが不在になったためです。手動で入れてやります。
sudo route -n add default 192.168.1.1
ping -c2 1.1.1.1
これで復旧します。ただし手動で足した経路なので、再起動やWi-Fi再接続で消える一時しのぎです。
SSHで作業しているときの注意
VPNを落とす前に、自分がどの経路でサーバーに繋いでいるか を確認してください。私はTailscale経由でSSHしていたので、うっかりTailscaleを止めると自分の作業セッションごと切れるところでした。サーバー側で接続元を確認できます。
ss -tn state established '( sport = :22 )'
Local Address:Port Peer Address:Port
100.78.160.23:22 100.81.41.96:59131
100.x はTailscaleのアドレス帯なので、この接続はトンネル経由だと分かります。作業前にLANのIPで入り直しておけば、VPNをどう触ってもセッションは切れません。
犯人探し
ipsec0 を作っていたのは何なのか。まずVPN構成の一覧を見ます。
scutil --nc list
* (Disconnected) ... "Pico" [PPP:Modem]
* (Connected) ... "Tailscale" [VPN:io.tailscale.ipn.macsys]
Tailscaleしかいません。しかもTailscaleは utun4 のほうです。つまり ipsec0 はネットワーク環境設定に出てくる正規のVPNサービスではなく、アプリがNetwork Extensionとして直接張ったものだと分かります。そこでシステム拡張を列挙します。
systemextensionsctl list
enabled active bundleID (version) [state]
com.eset.network (9.1.2500.0) [terminated waiting to uninstall on reboot]
* * com.eset.network (9.1.3100.0) [activated enabled]
* * com.eset.firewall (9.1.3100.0) [activated enabled]
com.eset.firewall (9.1.2500.0) [terminated waiting to uninstall on reboot]
* * io.tailscale.ipn.macsys.network-extension [activated enabled]
セキュリティソフトESETのネットワーク拡張が、新旧2バージョン並存 していました。9.1.2500.0が terminated waiting to uninstall on reboot、つまり「終了済みだが、アンインストールは再起動待ち」の宙ぶらりん状態。裏で自動アップデートが走り、新旧の入れ替わりが完了しないままネットワークスタックが変な状態で固まった、という筋書きは症状とよく合います。
結末と、断定できないこと
再起動したら直りました。再起動待ちだった旧拡張がアンインストールされ、スタックがクリーンに組み直されたためだと思われます。
ただ正直に書いておくと、ESETが原因だと確定させたわけではありません。確認できたのは「ipsec0 がグローバルなデフォルトルートを握っていた」ことと「ESETの新旧拡張が入れ替わり待ちだった」ことの2つで、その間の因果関係は証明していません。ipsec0 を張っていたのが本当にESETだったのかも、プロセスまで辿ってはいません。状況証拠は揃っていますが、そこまでです。
同じ症状に当たった人が確認するなら、sudo profiles list で構成プロファイル由来のVPNがないかも見ておくとよいと思います。私はこれを未確認のまま再起動してしまいました。
まとめ
「ネットに繋がらない」を切り分ける順番として、今回の流れはそのまま使えます。
ping 1.1.1.1— 通ればDNS、通らなければ経路の問題route -n get default— 出口がどのインターフェースになっているかnetstat -rn -f inet— フラグのIを見る。Iが付いていないデフォルトルートが実質的な唯一の出口scutil --nc listに出てこないVPNはsystemextensionsctl listを見る
特に3番目、複数のデフォルトルートが並んでいても慌てないことです。本当に効いているのは1本だけで、フラグを読めばどれかがすぐ分かります。VPNやセキュリティソフト、Tailscaleのようなオーバーレイネットワークを併用していると、この「誰が出口を握っているか」の把握が復旧の最短経路になります。