2011年11月22日

AndroidからL2TP/IPsec CRT VPNに接続する

はてなブックマークに登録

Android端末には、標準でVPN接続機能が搭載されています。

その中でもL2TP/IPsec方式では、x509証明書による認証がサポートされていて、端末ごとに個別の証明書を発行するなどの方法でセキュリティの高いVPN接続を行うことができます。

KLabでは、社内用の認証局を運用していて、社員が自分のユーザ名の入ったクライアント証明書を持てるようになっています。
管理用サイトへのブラウザでのアクセスのほか、社内の無線アクセスポイントもWPA2 Enterprise EAP-TLS認証に対応させて、クライアント証明書による強力な認証を活用しています。
もちろん、万が一の秘密鍵流出の際には、認証局側でCRLにより証明書を失効させることができるよう整備しています。

本記事では、クライアント証明書を使ったAndroid端末からのL2TP/IPsec VPN接続を試してみたいと思います。

Android 2.2系までの端末では・・・

残念ながら、Android 2.2系までの端末では、組み込まれている racoon(ipsec-tools)に問題があり、x509証明書による認証が実質的に使えない状態です。理由は以下のとおりです。

IPsecでは、IKEの認証フェーズ1にて、お互いのIDを交換します。
このIDには、「ID_IPV4_ADDR」・「ID_FQDN」などいくつかの種類があり、x509証明書を認証に使う場合には、証明書の識別名を使う「ID_DER_ASN1_DN」というIDタイプを使うことができます。

本記事のように、接続元IPアドレスを固定せずx509証明書を使う環境では、ID_DER_ASN1_DNタイプのIDを使うのがベストですが、Android 2.2系のracoonでは、「L2TP/IPsec CRT VPN」を選んでいても常にID_IPV4_ADDRタイプのIDがサーバに通知されてしまいます。

x509証明書による認証で、ID_IPV4_ADDR等をIDに使うことは可能ですが、この場合は使用する証明書のsubjectAltName拡張等のフィールドに「IP:192.168.0.1」等の値を含めておく必要があります。
そしてモバイル端末では、キャリアから動的に割り当てられるIPアドレスを使いますから、事前にIPアドレス等を記載した証明書を用意することも難しいです。
このため、Android 2.2までのOSを搭載した端末では、証明書によるIPsecは実質使えないのです。

この問題は、バグレポートされていて、Android 2.3では改善されています。 そのため、以降はAndroid 2.3系のOSを搭載した端末を前提とします。

認証に使う証明書とCA

認証に使うクライアント証明書・サーバ証明書、発行するCAには、Android端末の実装上、いくつかの制約があるため注意が必要です。

  • サーバ証明書のコモンネームには、サーバのFQDNを記載する

一般のWebサーバ用の証明書と同様に、コモンネームに記載されたFQDNと、端末の設定画面に入力した接続先サーバ名は一致する必要があります。

  • サーバ証明書は、シングルルートCAから発行する

サーバ証明書やクライアント証明書の発行元は、いわゆるオレオレ認証局を使うことができますが、自己署名しているルートCAから直接発行する必要があります。

Android端末のIPsecの処理では、端末ビルトインの証明書ストアは使われず、設定画面で指定したクライアント証明書とCA証明書だけが認証に使われます。ですから、サーバ証明書の検証に必要なルート証明書は、CA証明書としてインポートしてあげれば良いことになります。
しかし、Android端末のCertInstallerでは、複数の証明書をまとめてインポートできない仕様らしく、PEMフォーマットの証明書を複数連結したファイルをインポートしようとしても、「CA証明書1件」と表示されて、中間証明書を含めてインポートすることができません。
IPsecの認証プロトコル中でやり取りできる証明書も各方向1枚だけのため、インポートしたルート証明書から直接サーバ証明書を発行するしか手がありません。
(IKEのプロコトル仕様では、複数の証明書ペイロードを送ることができます。 実際にstrongSwanに改造を加えて実験してみましたが、なんとracoon側が1つ目の証明書ペイロード以外を無視する実装になっていたため失敗しました・・・)

  • クライアント証明書については、自由度は高い

クライアント証明書の検証は、サーバ側の仕事ですので、サーバに必要な中間証明書をインストールすればどのような証明書でも使用することができます。

openswanやstrongswanでは、クライアントのID_DER_ASN1_DNタイプのIDにワイルドカードを指定できるため、証明書の識別名についても自由度があります。
「rightid = "C=JP, ST=Tokyo, O=KLab Inc., CN=*」と設定すれば、各ユーザに個別に発行している「C=JP, ST=Tokyo, O=KLab Inc., CN=USERNAME」のような識別名の証明書をすべて使えるようになります。

端末への証明書・秘密鍵のインポート方法

インポートするクライアント証明書と秘密鍵は 「.p12」拡張子のPKCS#12形式、CA証明書は「.crt」拡張子のPEM形式もしくはDER形式にして、SDカードのルートかdownloadディレクトリに置きます。
Android端末のCertInstallerは「.p12」または「.crt」の拡張子の付いたファイルのみインポートしようとしますので、必ず所定の拡張子にする必要があります。

インポートは、設定の「現在地情報とセキュリティ」メニューの「SDカードからインストール」から行えます。

証明書のインポート画面

それぞれ読み込みできると証明書名を聞かれますが、これは後ほどVPN設定を行う際、証明書を選ぶメニューでの名前になりますので、わかりやすい名前を設定しておきます。

Android端末では、インポート済みの証明書の一覧表示や削除のインタフェースが充実していないので、証明書名には「シリアル番号+コモンネーム」等、証明書の個体を識別しやすい名前にするとよいでしょう。

サーバサイドの準備

Android端末からのVPN接続を受け入れるサーバ側の構築を行います。

使用するソフトウェアは、以下のとおりです。

  • openswan-2.6.36
  • xl2tpd-1.3.0 (後述のチューニングのためにパッチ適用あり)
  • ppp-2.4.5

それぞれ、ビルドオプションに特別なものを指定する必要はありません。

カーネルのESPサポートは、linux-3.0.4の標準のものを使用しています。
また、LinuxでのIPsec実装は、openswanのほかにstrongswanもありますが、strongswanではVPN切断後の再接続が上手くいかないという問題が発生したため、openswanを使用します。

Android端末からのL2TP/IPsec接続では、VPN切断時にサーバ側のSAをうまく削除してくれないという問題があるようで、サーバ側にlifetime時間までSAが残ってしまいます。strongswanを使用するとSAが残っている間は再接続(2本目のSA)が出来ず通信不能になってしまいました。

openswanの設定ファイル(ipsec.conf)は以下のとおりです。

config setup
    interfaces = %defaultroute
    syslog = auth.error
    plutodebug = "control"
    uniqueids = no
    nat_traversal = yes

conn Android-L2TP-IPsec
    auto = add
    type = transport
    keyexchange = ike
    auth = esp
    authby = rsasig
    pfs = no
    keyingtries = 1
    ikelifetime = 8h
    keylife = 8h
    rekeymargin = 10m
    left = %defaultroute
    leftid = "C=JP, ST=Tokyo, O=KLab inc., (略)"
    leftrsasigkey = %cert
    leftcert = Android_IPsec_Server.pem
    leftupdown = "/bin/true"
    leftprotoport = 17/1701
    right = %any
    rightca = "C=JP, ST=Tokyo, O=KLab Inc., (略)"
    rightid = "C=JP, ST=Tokyo, O=KLab Inc., (略)"
    rightrsasigkey = %cert
    rightprotoport = 17/%any

読み込む証明書の指定(leftcert)、各ID等は環境に合わせて調整する必要があります。

サーバ証明書を/etc/ipsec.d/certsに、秘密鍵を/etc/ipsec.d/private、クライアント証明書の検証に必要な中間CA証明書を/etc/ipsec.d/cacertsにそれぞれ設置します。
ファイル名は、OpenSSL流のハッシュ形式にしなくても自動的に読み込んでくれます。
また、秘密鍵の復号化に必要なパスワードは/etc/ipsec.secretsに以下のように記述します。
(leftcertに指定しているAndroid_IPsec_Server.pemに対応する秘密鍵が、/etc/ipsec.d/private以下にAndroid_IPsec_Server.keyの名前で保存されている前提です。)

: RSA Android_IPsec_Server.key "hogefuga"

xl2tpd側の設定ファイル(xl2tpd.conf)とpppオプション(ppp-options)は以下のとおりです。
トンネルに割り当てるIPアドレス(local ipやip range)もお好みで選んでください。

[global]
listen-addr                 = 0.0.0.0
port                        = 1701
debug network               = no
debug state                 = no
debug tunnel                = no

[lns default]
pppoptfile                  = /etc/xl2tpd/ppp-options
hostname                    = gw1
exclusive                   = No
local ip                    = 10.100.254.1
ip range                    = 10.100.254.128 - 10.100.254.254
length bit                  = yes
require chap                = yes
refuse pap                  = yes
require authentication      = yes

pppoptfileでは、認証可否の設定の他、Android端末に通知するDNSサーバを設定します。

auth
refuse-pap
require-chap
ms-dns 10.100.254.1

上記設定では、CHAP認証を行う設定になっています。許可するユーザ名・パスワードは/etc/ppp/chap-secretsに記述します。

USERNAME * "PASSWORD" *

PAP/CHAPともにrefuseとすると、L2TP上では認証しない設定にすることができますが、Android端末ではユーザ名・パスワードの入力が必須になっているため、ダミーのパスワードを入力する必要があります。

Android側からのVPN接続を行うと、端末からのすべての通信がトンネルを経由してサーバへルーティングされます。
サーバ側では、トンネルごとにPPPインタフェースが作られますので、適宜インターネットや社内のシステムに対してルーティング・アクセス許可してあげれば通信可能になります。
名前解決に使われるDNSサーバはpppoptfileのms-dnsで指定したサーバが使用されます。

Android端末サイドのVPN設定

Android端末に、VPN接続のプロファイルを作ります。

設定の「無線とネットワーク」から「VPN設定」とたどります。
「VPNの追加」をタップして「L2TP/IPsec CRP VPNを追加」を選びます。

各項目を以下のように設定して保存すると、プロファイルが作られます。

  • VPN名: 任意の名前を設定します
  • VPNサーバの設定: 接続するサーバのFQDNを指定します
  • L2TPセキュリティ保護を設定: チェックを外したままにします
  • 証明書を設定する:インポートしたクライアント証明書を選びます
  • CA証明書を設定する:インポートしたCA証明書を選びます
  • DNS検索ドメイン: 社内のドメイン名等を任意で設定します
VPNプロファイル画面

保存後、VPN欄に現れたプロファイルをタップして、サーバのchap-secretsに記述したユーザ名・パスワードを入力し接続します。

画面上部に「VPN [プロファイル名] が接続されました」と出ればVPN接続は完了です。
切断する場合には、プロファイルを再度タップすれば切断されます。

VPN接続中に通信できなくなる問題

筆者の環境では、長時間接続していると、突然通信不能になる症状が発生しました。

調査してみたところ、キャリアあるいは電波状況によっては、不定期的にパケロスする状況が発生するようで、L2TPサーバ側で定期的に送っているHelloメッセージがロスし最大再送回数に到達してしまったためにトンネルが閉じられているという状態でした。

Android端末側から、L2TPトンネルの切断を検知できるようなメッセージを送出できると良いのですが、端末標準の設定インタフェースでは実現できないため、サーバサイドで出来る限りの調整を行なってみます。

今回使用したL2TPサーバ(xl2tpd)では、HelloメッセージによるKeepaliveの間隔(60秒)、コントロールメッセージの再送間隔(1秒)・最大再送回数(5回)がソースコード中にハードコードされています。

この再送条件では、電波状況等により通信品質が変化するモバイル環境には厳しすぎますので、設定ファイルからパラメータを変更できるようにするパッチを書いてみました。

xl2tpd-1.3.0-add-ctrl-retrans-opt.patch
(2012/02/17:コメントにて、1.3.1では上記パッチが当たらないという連絡をいただきましたのでdiffを取り直しました: xl2tpd-1.3.1-add-ctrl-retrans-opt.patch )

このパッチを適用すると、設定ファイルのglobalセクションに以下の3つのパラメータを設定できるようになります。

  • hello delay (Helloメッセージの送信間隔)
  • ctrl retrans max (最大再送回数)
  • ctrl retrans delay (再送回数)

各パラメータをそれぞれ、Hello送信間隔を5分(hello delay = 300)、コントロールパケットの再送間隔を10秒(ctrl retrans delay = 10)で最大再送回数を18回(ctrl retrans max = 18)とすれば、最大3分程度通信不能な時間が発生してもトンネルを閉じられずにすみます。
長すぎる値を設定すると、本当に端末がオフラインになってしまっていても、長時間サーバ側にトンネルが残り続けてしまうため注意が必要です。

このパッチによるパラメータの変更は、あくまで切断される条件の緩和ですので、確実にVPNが切れなくなるわけではありませんが、多少の効果はあると思います。


#dSn
klab_gijutsu2 at 17:49│Comments(9)TrackBack(0)Android | ipsec

トラックバックURL

この記事へのコメント

1. Posted by もも   2012年01月19日 21:11
はじめまして。
自宅サーバーでAndroidからL2TP/IPsec CRT VPNを実現させたく、早速トライしてみましたが、上手くいかないのです。もしよろしければご教示いただけないでしょうか。
つまづいているのはCA証明書をAndroidにインストールする部分です。

OS:CentOS release 5.6 (Final)
実行コマンド
/etc/pkt/tls/misc/CA -newca


CA certificate filename (or enter to create)
エンター入力


Country Name (2 letter code) [GB]:JP
State or Province Name (full name) [Berkshire]:Osaka
Locality Name (eg, city) [Newbury]:Osaka
Organization Name (eg, company) [My Company Ltd]:Momo
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:"IPアドレス"
Email Address []:

cd /etc/pki/CA/
openssl req -new -keyout securekey.pem -out csr.pem
openssl ca -out server_cert.pem -infiles csr.pem

openssl req -new -keyout privatekey.pem -out privatecsr.pem
openssl ca -out private_cert.pem -infiles privatecsr.pem
openssl pkcs12 -export -inkey privatekey.pem -in private_cert.pem -certfile testca/cacert.pem -out private_cert.p12
openssl x509 -in ca.crt -outform DER -out ca.der

cp ca.der ca.crt

このca.crtをAndroidにインストールしようとしてますが、ユーザー証明書として認識されてしまい、CA証明書として認識してくれません。
変換や発行作業に間違いがあるのでしょうか。
2. Posted by kihira-h   2012年01月25日 15:14
ももさん、こんにちは。
AndroidのCertInstallerが、証明書ファイルの何を基準にCA証明書かユーザ証明書かの判断を行なっているかは調査していませんが、恐らくx509 Basic Constraints(基本制約)のCA属性がTRUEになっているかどうかではないかと思います。
インポートしようとしている ca.crt は、CA -newcaで生成した認証局の証明書でしょうか?
$ openssl x509 -noout -text -inform DER -in ca.crt
のようなコマンドで確認してみてください。
3. Posted by chaki   2012年02月09日 12:18
5 VPN初心者です。
「AndroidからL2TP/IPsec CRT VPNに接続する」とても参考になりました。

ももさんと同様な症状になり、CA証明書をIEのルート証明書にインポートしそれをエクスポートする事により解決しました。
http://www.eduroam.jp/docs/supplicant/android/sharp.html

質問があります。
複数ユーザー環境で複数のクライアント証明書とchap-secrets中のユーザー名は紐付け可能でしょうか?
4. Posted by kihira-h   2012年02月10日 21:07
chakiさん、こんにちは。
CA証明書を、一旦インポートしてエクスポートし直すと読み込みに成功するというのは興味深いですね。
軽く手元の環境で試験してみたところ、Windows環境で"Base 64 encoded X.509"形式にてエクスポートしたファイルは、OpenSSLで出力したPEM形式の証明書とは改行コードが異なるだけで同一内容でした。
そもそも証明書の内容は電子署名されているので、有効性を保ったまま情報を変更することは難しいはずですから、こういった微妙なフォーマットの違いと機種のCertInstallerの挙動によって何かしらの問題が起きているのかもしれません。

証明書のCNとL2TPの認証に使うユーザ名の紐付けですが、Openswanとxl2tpdの間には、IPsec側での認証に用いた証明書の情報をやり取りするような連携機能は無いため難しそうです。
5. Posted by しろ   2012年02月17日 16:19
4 貴重な情報有り難うございます。さて
Fedora16等では1.3.1が使用されておりこのページで公開されているパッチはそのままでは当りません。修正した物を作成したのですが、どのようにすればよろしいでしょうか?
6. Posted by kihira-h   2012年02月17日 20:24
しろさん、こんにちは。
ご連絡頂きまして、ありがとうございます。
公式サイトのダウンロードリンクが1.3.0のままだったため、1.3.1のリリースを見落としていたようです。

パッチについては、"saref refinfo"サポートによる変更のため当たらなくなったようですね。
弊社環境の1.3.1へのアップデートついでにdiffを取り直しましたので、追記させて頂きます。
7. Posted by 通りすがりの初心者   2013年03月06日 19:19
5 素晴らしい情報をありがとうございます。

当方の環境で問題があり、どこかに情報はないかと探していたところ、貴殿のサイトを見つけました。

xl2tpd+Openswanの環境で
同一NAT配下のPCから、1台ずつは接続できるのですが、同時に接続することができません。
ただ、Androidはなぜか同時に接続できます。
# PCと同じwifiルーター経由です。

何か情報をお持ちでしたら教えていただければ幸いです。
8. Posted by kihira-h   2013年03月07日 14:05
こんにちは。
NAT配下のPCから多重アクセスが出来ない件についてですが、NATの挙動・クライアント側の実装や設定などに依存してきますので、ズバリ原因と言えるものが思いつきません。
NATトラバーサル周りが怪しいと思いますが、はっきりした原因の調査には細かなデバッグログを見ていくしかありません。

サーバ側にて、「plutodebug = all」という設定を入れるとかなり細かな分析ができるようになります。(1回の接続あたり1000行以上のログを出しますので読むのが大変ですが、その分とても情報量は多いです)
まずは、どのメッセージを受信したときに、どの理由で認証を拒否しているのかをログから見つけ出し、ネゴシエーションしている情報が正しいかどうかをRFC2409、RFC3947などを参考に読み取っていきます。
その後、Phase1の第1メッセージでプロポーザルにNAT-Tが含まれているか、第3メッセージでピアがNAT配下に居ることを検出しているかを順に見ていきます。
あとは、1台目の接続時のログと、2台目を接続した場合のログとを比較していく等の地道な作業になってしまいます。

確度の高い情報がお伝えできず、申し訳ありません。
9. Posted by 通りすがりの初心者   2013年03月08日 00:12
5 ご回答ありがとうございます。

plutodebug="control parsing"

の状態で見つけたログには、

cannot install eroute -- it is in use for "L2TP-PSK"[2] xxx.xxx.xxx.xxx #2

というのがありました。

これは、既に1台接続済みの状態で、同じNAT配下からもう1台接続した際のログです。そして、#2は先に接続している1台のSAで使用されている番号です。

実はこの問題にもう何日費やしているか分からない状況です。(^^;
参りました・・・。

今一度、ログを分析してみます。

いろいろとありがとうございました。m(__)m

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント