診療所のクライアントVPN端末がブチブチして仕方なかったので対処した話


Linc'well Advent Calendar 2日目の記事です。

弊社が展開するクリニックフォアではサードパーティ製のクライアントアプリのサーバ接続にVPNが必須になっていたのですが、これがまた頻繁に切れるってことで問題になっていました。こういう営業所はまだまだ世の中に存在するんだなぁってことで、同じようなケースに当たった時にとりあえず応急処置にでもやったほうがいいことを紹介したいと思います。
(インフラや情シス担当が居ればログ追求も期待できると思いますが、多くの営業所がそのような体力なく運営されていると思いますので)

何が起きていたのか

Macから接続しているクライアントVPNがとにかくブチブチと切れる。仕方がなく手動での再接続の手間が発生するが、ユーザーは頻繁にこの作業を強いられてイライラするし、VPNのセッションが切れる度にクライアントアプリが落ちてしまうという状況のため、生産性の低下に直結していました。

VPNサーバーとの間でなにも通信が行われない場合に切断されている可能性がありますが、OS XのL2TPクライアントにKeep-Aliveの機能はついてないようです。パケットの送信がうまくいっていない可能性とかもあるのでしょうか。調べることがいっぴあ〜

時間がなかったので、本質的ではないけれど、1.とにかくできるだけの通信が停滞しないこと、2.切れてしまったらVPNとアプリが手間なく再起動されること、をゴールに暫定対応します。

やったこと

1. MTUのチューニング

どうやらクライアントVPNの通信エラーにはMTUのチューニングが効くようです。
MTU(Maximum Transmission Unit)っていうのは一度にネットワークへ送信できる最大パケットサイズの事で、大きく設定すれば一度に沢山のデータを送信できるけれどその分エラー率も上がり再送信コストが大きいらしい。。。

でもそれだけだとスループットが落ちるって話なんじゃ?と思ったんですが、どうもパケットのヘッダー内に分割を禁止するフラグ(DFビット)が立っている場合があると、VPNルーターが分割できないパケットを破棄し、送信元に対してエラーを伝えるICMPパケットを送るそうなんですね。また、そもそもこのMTU、OS毎にPMTU検出(Path MTU discovery)という機能が実装されていて、ユーザーが意識しなくてもMTUサイズを自動的に最適化するようになっているのですが、このレポーティングにICMPを使っているらしい。

ということはこのICMPパケットが途中にあるファイアウォールなんかの事情で遮断されると通信が途切れて、そのままVPN接続全体が止まるということもありうると。。。

これだけの調査ではDFビットが立つ条件とかはよくわからないのですが、とにかくネットワークの仕様に最適なMTUをチューニングすることでパケット分割の機会を最小限に抑えることはやった方がいいのと、ICMP ECHOが通らない(わざとやらない限り無いとは思うが)のはまずいのでその設定を見返していきます。

まずはifcofigで端末のMTUの初期設定値を確認します。ppp0がPPTPのVPNを指しています。この場合はMTU値は1280bytesにセットされてました。(ちなみに何も弄っていなければ通常1500にセットされているはずなんですが・・・おかしい・・・)

ifconfigの中身
ppp0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1280
    inet 10.255.1.2 --> 10.255.0.1 netmask 0xff000000

試しに1280bytesに指定してping打ってみると見事にRequest timeout。

ping
PING 10.255.1.2 (10.255.0.1): 1280 data bytes
ping: sendto: Message too long
ping: sendto: Message too long
Request timeout for icmp_seq 0
ping: sendto: Message too long
Request timeout for icmp_seq 1
ping: sendto: Message too long
Request timeout for icmp_seq 2
ping: sendto: Message too long
Request timeout for icmp_seq 3
ping: sendto: Message too long
Request timeout for icmp_seq 4
ping: sendto: Message too long
Request timeout for icmp_seq 5
ping: sendto: Message too long
Request timeout for icmp_seq 6
ping: sendto: Message too long
Request timeout for icmp_seq 7
ping: sendto: Message too long
Request timeout for icmp_seq 8

じゃあMTU書き換えるしか無いよねってことで、どこを弄ったらいいかというと /etc/ppp/ip-up というパスのファイルを作成して、以下の内容を記述します。

#!/bin/sh
/sbin/ifconfig ppp0 mtu 1500

ちなみに最適なMTU値は回線の種類によって違うそうで、nuro光の場合だと一般的には1500が最適なんだとか。実際うちはnuro光だったので上記も1500にして設定しました。

ファイルに実行権限を付与しておきます。

$ sudo chmod a+x /etc/ppp/ip-up

しかしpingを打ってみると、1500に到達せずMessage too longを起こしてしまいました。
よくわからなかったので(オイ)、10bytesずつ下げながらpingを打っていきます。

$ ping -D -v -s 1470 -c 1 www.example.com
PING www.example.com (93.184.216.34): 1470 data bytes
1478 bytes from 93.184.216.34: icmp_seq=0 ttl=52 time=108.876 ms
--- www.example.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 108.876/108.876/108.876/0.000 ms
$ ping -D -v -s 1480 -c 1 www.example.com
PING www.example.com (93.184.216.34): 1480 data bytes
ping: sendto: Message too long

するとMTU 1470byteで0.0% packet lossとなり、安定しました。
ipフラグメンテーションする時にヘッダーとかもろもろ加味して純粋にパケロス無しでデータとして送れる境界がこれくらいということでしょうか? その値を/etc/ppp/ip-upのMTU値としてセットすれば、回線に対して最適な設定になるということですね。

ppp0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1470
    inet 10.255.1.2 --> 10.255.0.1 netmask 0xff000000

/etc/ppp/ip-upを書き換えました。catで確認。

$ cat /etc/ppp/ip-up
#!/bin/sh

/sbin/ifconfig ppp0 mtu 1470

2. ステルスモードにしない

一応、ICMPが通らないなんてことがないようネットワーク設定についても確認しておきます。MacをICMP(Ping応答)するように設定するには

  • システム環境設定 > セキュリティとプライバシー > ファイアウォール
  • [ファイアウォールオプション]をクリック。
     (変更するには鍵をクリックしてパスワードを入れてもらう必要があります)

  • ファイアウォールオプションで「ステルスモードを有効にする」のチェックを外す。

これでICMPに応答するようになるはずです。

3. 切断時自動復旧スクリプトの常駐

上記のような対策をしてもVPN接続が切断されてしまうならば再起動するしかありませんが、Macの場合、ネットワークを監視して接続を復旧するAppleScriptを仕込んでおくことが可能です。業務上、クライアントVPNが必須ということであれば、そもそもPC起動時にスクリプトを常駐させて、自動でVPN接続されるようにしちゃうとかも便利です。

下記のサンプルは、VPNコネクションの接続是非を検知して、もし繋がっていなかったらVPNを再起動する処理を5秒ごとに行なっています。

VPN_Connect_v1
on idle
    tell application "System Events"
        tell current location of network preferences
            set myConnection to the service "hogeVPNname"
            if myConnection is not null then
                if current configuration of myConnection is not connected then
                    connect myConnection
                end if
            end if
        end tell
        return 5
    end tell
end idle

"hogeVPNname"のところは、自分の端末のVPN接続名に書き換えます。そして適当な場所に保存して、常時起動するようにする。

環境設定>ユーザーとアクセシビリティ>ログイン項目に追加して、起動時に常駐するようにします。

おわりに

今回は営業所への暫定対応ということで、すぐに効果を発揮しそうな対応をご紹介しました。実際には、上記に加えてVPNが切れる度にクライアントアプリが落ちてしまうという状況だったため、弊社の場合はapplescriptにVPN接続アプリの自動起動も書き足して運用しました。短時間で仕込んだ暫定的な対応ではありますが、すぐに打てるアクションでうまくいけば現場スタッフからの反応もよいのでおすすめです。

なお、弊社は中期的にTLS1.2での接続に切り替えていくことにしたので今回は詳しい調査をしなかったのですが、上記以外の根本的な解決についてネットでは言及されていましたので最後に紹介します。VPNでの接続を現役でやっていて切断現象に悩んでおられる事業者さんは、下記のような事象を疑ってもいいのかもしれません。

ルータは、VPNのパケットをアドレス変換(NAT)しないで通す機能があり、これをPPTPパススルーと言います。家庭用のルータなどでは、この機能は1台までしか使えず、同時に複数の接続があると正しく対処できません。そのため、同時接続して通信すると混線が発生して頻繁な切断を引き起こします。この問題は、PPTPマルチパススルーに対応したルータを使う事で解決します。

L2TP/IPSecトンネルの崩壊と戦った話

先日、L2TP/IPSec VPNを設定する機会があり、その際に「トンネル内でサイズが大きなパケットを1つでも送出した瞬間にトンネル全体の通信が止まってVPN接続が切れる」というトンネル崩壊現象に悩まされました。