ある朝、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がないかも見ておくとよいと思います。私はこれを未確認のまま再起動してしまいました。

まとめ

「ネットに繋がらない」を切り分ける順番として、今回の流れはそのまま使えます。

  1. ping 1.1.1.1 — 通ればDNS、通らなければ経路の問題
  2. route -n get default — 出口がどのインターフェースになっているか
  3. netstat -rn -f inetフラグの I を見るI が付いていないデフォルトルートが実質的な唯一の出口
  4. scutil --nc list に出てこないVPNは systemextensionsctl list を見る

特に3番目、複数のデフォルトルートが並んでいても慌てないことです。本当に効いているのは1本だけで、フラグを読めばどれかがすぐ分かります。VPNやセキュリティソフト、Tailscaleのようなオーバーレイネットワークを併用していると、この「誰が出口を握っているか」の把握が復旧の最短経路になります。