macOS Catalina の NFS 問題を解決する

こんにちは、masm11 です。

弊社では、macOS 上の Vagrant 環境内から、macOS のディレクトリを NFS で mount して、 開発しています。 仮想マシンとしては VirtualBox を使用しています。 この度、私に貸与された mac の macOS を Catalina に上げた ところ、NFS mount ができない、という症状が発生しました。

今回はこの症状を解決するまでをご紹介します。

なお、macOS は Catalina 10.15.4、Vagrant は 2.2.7 です。

症状

vagrant up すると、途中まではうまく処理が進むのですが、 NFS mount するところで、以下のように失敗します。

==> default: Exporting NFS shared folders...
==> default: Preparing to edit /etc/exports. Administrator privileges will be required...
==> default: Mounting NFS shared folders...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

mount -o vers=3,udp 192.168.33.1:/System/Volumes/Data/Users/masm/proj/ra/share /vagrant

Stdout from the command:



Stderr from the command:

mount.nfs: requested NFS version or transport protocol is not supported

そして、少し経ってから vagrant ssh でログインし、手動で

sudo mount -o vers=3,udp 192.168.33.1:/System/Volumes/Data/Users/masm/proj/ra/share /vagrant

を実行すると、なんと成功します。

おそらく、どこかで何かがタイムアウトしているのでしょう。

情報を漁ってみる

世の中で既にたくさん使われているはずの Catalina です。そして Vagrant もです。 情報がないはずがありません。

ぐぐってみると、たくさんの情報が見つかりました。一番多かったのがこれです。

  • /etc/exports に書く macOS 側 path は /System/Volumes/Data/... になっていること。

    しかし、最新の Vagrant なら既にそうなっています。上に書いたエラーメッセージでもそうなっていますね。

また、Vagrant の issue をいくつか見たところ、以下の情報が得られました。

  • 既知のディレクトリ (~/Documents みたいな) を export しないこと。

    既知のディレクトリを export する場合は /sbin/nfsd にフルディスクアクセス権限を与える必要があるそうです。

    私の場合は独自のディレクトリですので、該当しません。そもそも権限が問題なら、時間が経ったら 成功する、などということはないはずです。

  • macOS 側 /etc/hostslocalhost をちゃんと書いておくこと。

    普通は書いてあると思います。私も書いてありました。

    ただし、私の場合、showmount -e localhost が何も返さず黙ったまま、という症状もあり、 それは ::1localhost にしてあったのが原因でした。::1 を削除したところ、 この症状はおさまりました。

  • Vagrantfile 内 synced_folder は1つだけにすること。

    複数書くと、/etc/exports の1行に複数個のディレクトリが並ぶ場合があって、その場合に問題が起きていたようです。

    このコメントに例付きで書いてあります。

    最新の Vagrant では大丈夫かもしれませんが、追い切れていません。

    私の場合、synced_folder は1つだけでした。

以上が、情報を漁った結果です。

さらに症状は続く…

しかし、これらの情報では解決に至りませんでした。

仕方ありません、独自の調査を試みてみます。

NFS をやめる

代わりに VirtualBox の sync folder を使うという手があります。

しかし sync folder は遅いのです。NFS が解決しなければ sync folder を 使うという手も考えましたが、遅くて開発に支障が出そうだったので、解決しない 場合は解決しないまま使うことにしました。問題なのは mount する時だけ ですから。

rpcinfo してみる

NFS で問題が起きたら、とりあえず rpcinfo だと思っています。

rpcinfo とは何でしょうか? 使う前に少し説明してみます。

普通、サービスにはポート番号が割り当てられていますね。smtp であれば 25番、 http であれば 80番です。しかし、ポート番号は 16bit であり、1~65535 しかなく、 しかもその中はざっくりと範囲に用途が決まっています。well-known ポートであったり、 エフェメラルポートであったり、NAT に使うものであったり、よくわからないサービスが 独自に割り当てて使ってしまうものであったりします。つまり、65535個って結構たくさん あるように見えて、割り当てられる番号はその極一部なのです。有限なのです。 使うかどうかも判らないようなサービスにポート番号を固定で割り当てたくはないのです。

すると、当然、「じゃぁ動的に割り当てよっか?」という発想が生まれます。つまり、 もっと広い空間の「プログラム番号」「プログラム名」を用意し、使われていないポート番号 から動的に割り当て、「プログラム番号」→「ポート番号」の表を管理しておくのです。 こういった機構を portmap などと呼んでいます。

rpcinfo -p 192.168.33.1 を実行すると、192.168.33.1 のマシンの中のその表を管理 しているプロセスから、その一覧を取得することができます。

試してみましょう。

yuukinombp:~ % rpcinfo -p 192.168.33.1 | grep nfs
    100003    2   udp   2049  nfs
    100003    3   udp   2049  nfs
    100003    2   tcp   2049  nfs
    100003    3   tcp   2049  nfs

100003 はプログラム番号、2 や 3 はプログラムのバージョン、 udp や tcp は使用するプロトコル、 2049 はポート番号、nfs はプログラム名です。つまり、nfs の vers=2 の UDP 版は ポート 2049 を使用する、と読みます。

192.168.33.1 の nfs に UDP でアクセスしたいクライアントは、 一旦 192.168.33.1 の portmap にリクエストを投げ、 nfs の vers=2 の UDP が 2049 であることがわかったら、 改めて 2049/udp に nfs のリクエストを投げる、 ということになります。

ただし、この仕組みは広くいろんなサービスに使われているわけではありません。 結局、NFS や NIS くらいでしか使われなかったように記憶しています。

さて、話を戻します。

NFS mount できる時とできない時で差がありました。できない時には mountd がありません。例をお見せします。

yuukinombp:~ % rpcinfo -p 192.168.33.1 | grep -E 'nfs|mountd'
    100003    2   udp   2049  nfs
    100003    3   udp   2049  nfs
    100003    2   tcp   2049  nfs
    100003    3   tcp   2049  nfs
yuukinombp:~ % rpcinfo -p 192.168.33.1 | grep -E 'nfs|mountd'
    100003    2   udp   2049  nfs
    100003    3   udp   2049  nfs
    100003    2   tcp   2049  nfs
    100003    3   tcp   2049  nfs
    100005    1   udp    632  mountd
    100005    3   udp    632  mountd
    100005    1   tcp    991  mountd
    100005    3   tcp    991  mountd
yuukinombp:~ %

上側が mount できない時、下側が mount できる時です。

たまたまかもしれません。もう少しタイミングをしっかり確認してみましょう。

macOS 側で以下を実行します。

while :; do rpcinfo -p 192.168.33.1 | grep -E 'nfs|mountd'; sleep 1; done

別の端末で、vagrant ssh して以下を実行します。

while ! sudo mount -o vers=3,udp 192.168.33.1:/System/Volumes/Data/Users/masm/proj/ra/share /vagrant; do sleep 1; done

ようするに、rpcinfo を1秒おきに実行しながら、mount も1秒おきにやってみる、ということです。

少し時間が経つと、両方同時に変化がありました。 rpcinfo は mountd を出力するようになりましたし、mount も完了しました。 タイミング的には1秒未満の差です。関連してそう、と思って良いでしょう。

私もどのプログラムがどういった役割をしているのか、それほど把握しているわけでは ないのですが、mountd がいなければ mount はできないのでしょう。

しかし、何故そんなことになるのかはさっぱり原因がつかめません。

log を見る

困って更にぐぐっていたある日、syslog を見る方法を見つけました。

その名も log コマンド。

log stream --style=syslog

とすれば、syslog っぽく整形されたログがどんどんリアルタイムで出力されていきます。

これです! これが欲しかったんです! これさえあれば…

nfs に関するメッセージを探してみたところ、ありました!

2020-04-18 01:37:34.795213+0900  localhost nfsd[7277]: couldn't register NFS/TCP service.

これですね。

couldn't register は、portmap の表に登録できない、という意味なのでしょう、おそらく。 そして、NFS/TCP と書いてありますね。ならば、TCP を無効にしてみましょう。

/etc/nfs.conf に以下のように書きます。

nfs.server.tcp = 0

これで vagrant up しなおします。

==> default: Exporting NFS shared folders...
==> default: Preparing to edit /etc/exports. Administrator privileges will be required...
Password:
==> default: Mounting NFS shared folders...
==> default: Mounting shared folders...
    default: /tmp/vagrant-chef/783542ad0dccc700ff72bcae0833b125/cookbooks => /Users/masm/proj/ra/cookbooks
==> default: Machine already provisioned. Run `vagrant provision` or use the `--provision`
==> default: flag to force provisioning. Provisioners marked to run always will still run.

いけました! めでたく mount できました。

根本原因 (何故 TCP 版が登録できないのか) はわかりませんが、これでひとまず解決です。

考慮点

これでは TCP 版 NFS が使えません。が、Vagrant が UDP を指定しちゃっているので、 TCP を無効にしても影響はないでしょう。

ただ、今後、docker を使うなどで TCP での NFS を使いたくなった場合には、改めて 困るかもしれません。

まとめ

NFS トラブルの解決策が見つからなかったため、自力で解決してしまいました。 ただ、ぐぐって情報を集めても、同様の症状はほとんど見当たりませんし、 たまにあっても解決していませんでした。少数派なのでしょうね。

それにしても、最近、どの OS も syslog をバイナリ化してしまうので、困ります。 テキストファイルなら、テキストファイルを扱えるコマンドは無数にありますから、 好みの方法で使えます。しかしバイナリ化されていると、専用コマンドの使い方を 覚えて、一旦テキスト化してやる必要があります。 テキストでいいじゃん… とは思うのですが、バイナリにもそれなりのメリットが あるのでしょうね。

後日談

と、ここまで書ききった後で、後日談が生まれました。

社内で「ESET をバージョンアップしたら NFS mount できなくなった!」 「NFS mount できない時は ESET ファイアウォールを一旦無効にしてます!」 という声があり、どうやら ESET が影響しているということが判りました。

パーソナルファイアウォールのログを確認したところ、ターゲットのポートが 111 なのは予想通りでしたが、ソースの IP アドレスが :: となっていました。 IPv6 です。 しかし :: をルールの IP/IPv6アドレスに入力してもルールが作成できません。 代わりに ::0 と入力すると作成できました。

まとめると、以下のルールを作成することで、NFS mount できるようになりました。

  • 名前: RPC/NFS 等、適当なもの
  • すべてのアプリケーション: ON
  • アクション: 許可
  • 方向: 両方
  • プロトコル: TCP
  • ポート: ローカル
  • ローカルポート: 111
  • 宛先: IPアドレス
  • IP/IPv6 アドレス: ::0

私の ESET は ESET Cyber Security Pro 6.8.300.0 です。

nfs.conf に手を加えるか、パーソナルファイアウォールに設定するか、 どちらかお好みの方法で良いと思います。

ではまた!