最近の5件分。
libvirtでKVMゲストを自動起動(autostart)するように設定しても上手く起動してくれないときはブリッジされているデバイスのfowarding_delayが0になっているか確認すること。ウェブ上で見つかる設定例によっては0以外になっていることがあるが、STPが無効の状態でも待機自体はしてしまうようなのでその間にゲストが起動するとインターフェースをつかみ損ねて上手く起動しない。STPを使わないのであればforwarding_delayは0で問題ない。libvirtの機能でブリッジを作っている場合はおそらく問題無いだろうが、手動でブリッジを設定している場合は気をつける必要がある。
同じような例として、ブリッジデバイス上でbindやDHCPDなどをListenする場合はforwarding_delayが長いとインターフェースのIPを取得できずにプロセスが即終了することが有るようだ。
Ubuntu11.04などのディストリビューションでIPv6関係のパケット(ICMPv6を含む)がbrctlによって作られるブリッジデバイスを通過できない問題が有るように思われる。
なぜこの問題に気がついたかというと、KVMのゲストがどうやってもルータから流れてくるRAを受信できず、tcpdumpで確認したところtapを物理ethと接続するブリッジを境にパケットが途絶えていた。原因としてはip6tablesでフィルタリングされている可能性が考えられるが、全てルールが空でポリシーもACCEPTの状態でも破棄されてしまう。
解決策としては、sysctlで次のキーを0にしてしまえば良いようだ。ブリッジでのipb6tablesが無効になってしまうようなので環境によっては注意して使う必要がある。
sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=0
なんとなく関連してそうなパッチがカーネル2.6.39でコミットされているようなので次のバージョンのUbuntuでは解決しているかもしれない。現状ではDebianなどの他のディストリビューションでも影響を受ける可能性が有る。
「普段はISP1を使いつつ、サーバ用には固定IPを貰える(i-revoなどの)ISP0を使いたい」といった、よくあるPPPoEマルチセッション環境におけるNATとポートフォワーディングの問題を解決する。
まずは前提条件を。
iptablesのパケットフロー図を見ながら読んで欲しい。
まず、内部ホストからの外向きの通信(通常の外出)はマスカレードして ppp1 から発信する。これはiptablesで簡単に設定できる。
iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o ppp1 -j MASQUERADE
このルールは、パケットのソースIPアドレスがLAN内のネットワークアドレスであり、そのパケットがppp1から出て行く場合に、いわゆる「NAT」として働く。
この時点でルータ内のデフォルトルートは当然 ppp1 である。外向きのパケットは全て ppp1 から外に出て行き、ppp0 から外に出て行くことはない。ここまでは単に ppp0 が余計に存在するだけの、普通のNAT環境といえる。
次に、サーバ向けの ppp0 宛にやってきた通信を ppp0 から返信するように設定する。ppp0から入ってきたパケットに対する返信は何もしなくてもppp0から出て行くかと思いきや、そんなことはない。返信だろうが何だろうが、IPレベルでのルーティングはOSが持っているルーティングテーブルに従って行われるので、「ppp0に振られているIP X.X.X.X をソースIPにもつパケット」が ppp1 から送信されてしまう。ルーティングにおいてIPアドレスとIFは概念上分離している。
このような動作はしばしばトラブルとなる。ISPによってはソースIPが自分のネットワークに含まれていないパケットをスプーフィングと見なして転送してくれない事がある。さらに、ISP1の回線が接続されていないときにISP0経由の通信まで繋がらなくなってしまうし、ISP0の帯域を使いたい通信が全てISP1に流れてしまうのも問題であろう。
ppp0 (に振られているIP)宛のパケットに対する返信をイメージ通りにppp0から流すためにはルーティングテーブルを動的に切り替える必要がある。そこで、ルータ内のデフォルトルートは ppp1 のままにしつつ、iproute2 をつかって特定ルールにマッチする場合のみ ppp0 へのルーティングを行う。
まずは ppp0 用のルーティングテーブルを新たに定義する。ip routeというコマンドで設定できる。
ip route add 0/0 dev ppp0 table 10
これは 10(別に1でも3でも100でも何でもいい)という名前のルーティングテーブルにppp0をデフォルトゲートウェイとして追加している。このルーティングテーブルを参照した場合、あらゆるパケットがppp0経由で送信される。
ちなみに、普段使うrouteコマンドで見えるルーティングテーブルはiproute2上ではmainという名前のテーブルとして扱われている。つまりrouteコマンドはiproute2に定義されているデフォルトのテーブル「main」を扱っているに過ぎないのだ。
次に、このルーティングテーブルを参照する条件を定義する。ip ruleで定義できる。
ip rule add from X.X.X.X table 10 pref 100
これはつまり、ソースIPが X.X.X.X(ppp0のIPアドレス) のパケットについては、(mainではなく)10のテーブルを見ろと言うことである。prefは優先度なのだが、複雑なルールを作らない限りはあんまり気にしなくていい。適当に100ぐらいで定義しておこう。
なお、既存のルールは
ip rule
とだけ打てば参照できる。デフォルト状態だと以下のような感じになっているはずだ。
0: from all lookup local 32766: from all lookup main 32767: from all lookup default
最も低い優先度に普段用のmainテーブルを参照するためのルールがあるので、これより高い優先度で先ほどのルールを定義すれば良い。
ここまでの設定で、ppp0 宛の通信に対する返信パケットは正しくppp0から送信されていくようになっているはずだ。返信パケットは当然 ppp0 に振られている X.X.X.X をソースIPとして送信されるので、新たに定義したルールに従って10番ルーティングテーブルが参照される。そこには ppp0 がデフォルトルートとして定義されているので、返信パケットは ppp0 から送信されていく。ためしにX.X.X.にpingしてみればきちんとppp0から返信のパケットが飛ぶ。なにも問題はない。ここまでは。
次にいわゆるポートフォワーディングの設定を行う。内部にあるウェブサーバーのためにppp0に来たポート80への通信だけ192.168.0.2に転送する。これはiptablesのDNATで簡単に設定できる。
iptables -A PREROUTING -t nat -p tcp -i ppp0 --dport 80 -j DNAT --to 192.168.0.2:80
DNATはデスティネーション(宛先)NATの事で、ルーティングが行われる前(PREROUTING)にディスティネーションIPを内部にあるサーバのIPに書き換える。今回の例では ppp0 に届くパケットについて、宛先ポートが80番であれば、192.168.0.2のポート80宛に書き換えを行う。書き換え後にルーティングが行われるので、LAN内にあるウェブサーバー宛に正常にパケットが配送されていく。
ではウェブサーバーから戻ってきたパケットはどうなるだろうか。ウェブサーバーに届くパケットのソースIPアドレスは特に書き換えられていないので、外部にいるクライアントのIPアドレス192.0.2.4のままとなっている。当然ウェブサーバーはそのIPアドレス宛に返信パケットを送信するが、返信パケットのソースIPアドレスはウェブサーバーの持つローカルIPアドレス192.168.0.2となる。
このパケットをそのまま外部に出すことは出来ないので、パケットを受け取ったルータのiptablesは逆DNATを行う。これは特に定義しなくても先ほど定義したDNATのルールに従って自動で行ってくれる。逆DNATによりソースIPアドレスが ppp0 に振られている X.X.X.X に書き換えられるので、外部から見ればまるでルーターが直接受け答えしているかのように見える。すばらしい。
| ソース | デスティネーション | |
|---|---|---|
| ルータが受け取るパケット | 192.0.2.4 | X.X.X.X |
| ルータがDNATしたパケット(ウェブサーバーが受け取るパケット) | 192.0.2.4 | 192.168.0.2 |
| ウェブサーバーが返信するパケット | 192.168.0.2 | 192.0.2.4 |
| ルータが逆DNATしたパケット | X.X.X.X | 192.0.2.4 |
だが現実はそううまくはいかない。実は(なぜそうなっているのかは知らないが)逆DNATはルーティングが行われた後に行われる。つまり、最初のDNATはPREROUTINGに行われるのに対し、帰りの逆DNATはあたかもPOSTROUTINGのタイミングで行われる。
だからどうしたと思うかもしれないが、さっき定義したiproute2のルールを思い出して欲しい。そこでは「ソースIPアドレスが X.X.X.X の時は」という定義だった。しかし逆DNATがルーティングの後に行われるとすると、ルーティング時におけるソースIPアドレスはまだ、ウェブサーバーの192.168.0.2のままになっている。よってルーティングはデフォルトのmainテーブルを参照して行われ、パケットは ppp0ではなく ppp1から送信される。ルーティング決定後に逆DNATでソースIPアドレスの書き換えが行われ、ソースIPアドレスがX.X.X.XとなったパケットがISP0のppp0ではなく、ISP1のppp1から送信されることになる。……さっきと同じ問題だ。
ここまでで分かることは、ソースIPアドレスによるip ruleの定義はルータがサーバーを兼務する場合にはうまく動作するが、内部の異なるホストへポートフォワードした場合は役に立たないと言うことだ。
これを解決するにはmarkを使わなければならない。markについての詳しい話は省略するが、要はパケットに印を付ける仕組みだ。iptablesでもiproute2でも使うことが出来るので、両者の連携に用いることが出来る。ただ、カーネル内での仕組みなのでホスト間での連携は出来ないので注意して欲しい。
markをどのように使うかというと、まずはiptablesを使ってppp0宛のコネクションには印を付ける。そしてルーティング時にその印を見てルーティングテーブルを切り替える。
まずは印を付けるためにiptablesのmangleテーブルを使う。mangleテーブルはパケットフロー図を見れば分かるとおり、常に他のテーブルより先に処理されるので、パケットの前処理に使用することが出来る。今回はppp0宛に来たコネクションに関してのみ、10(例によって3でも100でもなんでもいい)をマークとして付ける。
iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -i ppp0 -j MARK --set-mark 10
iptables -t mangle -A PREROUTING -j CONNMARK --save-mark
ここで若干話がややこしくなるのだが、本来markはパケット単位で付けるものである。上の3行で言えば真ん中の行がそれに当たる。ここでは、ppp0にきたパケットについて10という値を付けている。ただ、これだけだと外からやってくるウェブサーバーへのパケットはマーク付きで処理できるのだが、ウェブサーバーからやってくる返信パケットにはマークが付かない。そこで、コネクション単位でのマーキングを行うためにCONNMARKを使う。CONNMARKは--save-markでパケットマークをコネクションマークとして記録し、--restore-markで逆のことを行う。少なくともtcpに関しては、この方法でコネクション単位でのマーキングが可能となる。
さて、コネクションにマークが付いたので、あとはこのマークを元にルーティングテーブルの切り替えを行うだけだ。
ip rule add iif eth1 fwmark 10 table 10 pref 99
LAN側(eth1)から流れてきたパケットに関して、10のマークが付いている場合は先に定義した10番のルーティングテーブルを参照する。これでウェブサーバーから流れてきた返信のパケットが正しくppp0から発信される。
以上で全ての問題が解決した。普段の外出はppp1から発信され、ppp0にやってくるパケットに関しては、返信がppp0から流れていく。
KVMの最近のバージョンにはvhost-netという機能が追加されていて、公式の見解ではレイテンシが10%軽減され通常のvirtioの7倍から8倍近いスループットが発揮されるらしい。是非使いたい機能だが、Ubuntu 10.04はおろか10.10でも標準で用意されているパッケージだけでは使用できない。今回はすこし頑張ってUbuntu 10.04にいくつかのパッケージを追加してvhost-netを使用出来るようにしてみた。
まず、vhost-netを使うにはカーネル側の対応が必要となる。機能自体はカーネル2.6.34以降で取り込まれているようなのだが、10.04のカーネルは2.6.32なので対応していない。10.10であればデフォルトで対応している。次に、qemu-kvmのバージョンアップも必要となる。こちらはバージョン0.13から機能が取り込まれているのだが10.04でも10.10でも対応してない。
10.04の場合、まずはカーネルのバージョンを上げる。これは比較的簡単である。marverickのlinux-image-2.6.35-22-*パッケージがlucid-updatesに公式でbackportsされているのでapt-getでインストールするだけである。flavorは使用環境に合わせて選ぶ必要があが、たとえばサーバー版ならばsudo apt-get install linux-image-2.6.35-22-serverでよい。
qemu-kvmのアップデートは若干手間がかかる、maverickのqemu-kvmもバージョンが0.12.5なので使用できない。幸い、現在テスト中のnatty(11.04)のパッケージが公開されていて、そちらはバージョンが0.13なのでこれを使う。もちろんテスト版なので不具合の可能性は否定できないので注意が必要である。ダウンロードはパッケージに関する詳細ページの一番下のリンクから可能だ。
debファイルをダウンロードしてきたらdpkg -i [ファイル名]でインストールできるが、おそらく依存関係で失敗する。qemu-kvmに関連したパッケージをいくつか追加でダウンロードする必要があるだろう。手元では最終的に以下のパッケージを使うことになった。
全てがインストールできたらvhost-netモジュールをカーネルに組み込む必要がある(CONFIG_VHOST_NETがyのカーネルならば自動で組み込まれているかもしれないがnattyはmだった)。sudo modprove vhost-netで読み込む。これだけだと再起動時に元に戻ってしまうので、/etc/modulesに書き加えておくのがいい
あとはいつも通りvirshでゲストを開始する。libvirt-bin自体のバージョンも上がっているので、vhost-netは自動で有効になる。psコマンドなどで実際に実行されているコマンドを見るとvhost=onのような記述が追加されているはずだ。追加されていない場合、ゲストのカーネルが古くてvhost-netを使用できない状況が考えられる。ゲスト側はカーネル2.6.31以降で、CONFIG_PCI_MSI=y付きでビルドされている必要があるようだ。
というわけで、ゲストは通常のlucidなら問題無いはずなのだが、手元の環境だとCPU負荷がかかっていないのにloadが非常に高くなる現象が発生し、まともに使える状況ではなくなってしまった。そもそもvirshで起動する際に「cpu0 unhandled rdmsr」というエラーが出る上に、ゲスト側でもCPUがスタックしたというメッセージが表示される。適当に調べてみたところ「そのエラーは問題無いので無視して良い」という情報もあったのだが、どうやってもおかしい。問題が起きたのはlucidのゲストだけで、maverickのゲストでは問題が起こらなかったため、試しにゲスト側もカーネルだけ同じ方法でbackportsから2.6.35に更新してみたところ解決した。
以上でUbuntu 10.04でvhost-netが使用できるようになった。netperfでゲスト-ホスト間のスループットをはかってみると確かに7Gbps程度の速度が出る。ただし、フルスピード時のCPU負荷は結構厳しい。ゲスト間だと3Gbps程度で通信できる。単位帯域に対するCPU負荷は結構な割合で下がっているように思われるのでとりあえず導入しておけばそれなりに役に立つと思われる。
どうも原因不明らしい。Debian Lenny 5.0.5 amd64で再現した。同じDellマシンでBroadcomのNICを積んでるのでハードウェアかドライバが怪しい気がするが詳細は不明。
なお、完全仮想化(HVM)だと問題が起こるのだが、準仮想化だと問題無いようである。