Apacheでマトリョーシカを作ってみる

以前から、「Apache1000本ノック」という単語だけ頭に明滅していたのですが、時間が取れたのでそれに近いものを作ってみました。

が、実際にやってみるとノックというよりはマトリョーシカに近いな……と思ったので、ここでは「Apacheマトリョーシカ」として紹介します。あるいはドミノ倒しというイメージも近いかもしれません。

なお、本稿の実用性はゼロです。お遊びです。

環境の概要

環境 VirtualBox 4.3.10
OS CentOS 6.5(64bit)
Apache Ver. 2.2.27

Apacheマトリョーシカとは何か

コンセプトとしては、Apacheのproxy_httpを使ってポートフォワードを多段接続してみよう! ということです。

イメージはこんな感じ。

WebブラウザでHigh Portに接続すると、そこから内部でひたすらポートフォワードしまくって、一番奥底にある80/tcpに到達するとようやくコンテンツが取り出せます。

ここでは大きめのポートとして10080/tcpを仮定してみたので、http://:10080/に接続すると、10080/tcp→9980/tcp→9880/tcp→……と少しずつ小さいポートにリダイレクトされていき、最終的に「……→280/tcp→180/tcp→80/tcp」と80/tcpに到達して、コンテンツが取り出されます。つまり100個入れ子マトリョーシカです。

そんなことして何の意味があるの? と言われると、何もありません。

構築のしかた

やり方は色々とあると思いますが、ここではmod_proxy_httpを使って構築してみます。

具体的には、1リダイレクトごとに1つのコンフィグファイルを作ります。そして「ポートnからポートn-100へリダイレクトする」confファイルは以下のような設定とします。これはn=2480の例ですね。

$ cat proxy_2480.conf
Listen 2480

<VirtualHost _default_:2480>
  ProxyPass / http://127.0.0.1:2380/
</VirtualHost>

ファイル名は、「proxy_(受付ポート番号).conf」とします。ここでは一番外側のマトリョーシカを10080/tcpとしましたから、proxy_10080.confからproxy_180.confまで、100個のファイルができます。

これらをApacheのconf.dディレクトリに格納し、httpd.confから「Include conf.d/*.conf」として100個読み込みます。

マトリョーシカ状態の確認

Apacheを起動した段階で、ssコマンドを使って待ち受けポートの確認をします。なおssコマンドとは"another utility to investigate sockets"で、netstatコマンドと似た機能を持ちます。

ここでは、ssコマンドの-l(LISTEN状態のソケット表示)・-n(名前解決をしない)・-t(TCPソケットのみを表示)のオプションを利用しています。80/tcpで待ち受けているのが一番内側のApacheで、その「外側の容器」として9580/tcp,8780/tcp,7980/tcpなどの末尾80のポートがずらりと並んでいるのが分かります。

通信のキャプチャ

このApacheマトリョーシカは内部でリダイレクトしているだけなので、通常のログレベルではログが出力されずリダイレクトの様子が分かりません。そこでtcpdumpでパケットキャプチャして確認してみましょう。

まず、iptablesをOFFにしておきます。

# service iptables stop

マトリョーシカの一番外側には以下のようにwgetでアクセスします。

$ wget http://127.0.0.1:10080/

上記wgetコマンドを、tcpdumpでパケットをダンプしながら実行します。

# tcpdump -i lo -w /var/tmp/tmp.pcap
tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
^C539 packets captured
2052 packets received by filter
971 packets dropped by kernel

……む、実行してみると"packets dropped by kernel”と表示されました。パケットがドロップされ、取りこぼしが出ています。Apacheのポートフォワード動作を少し遅くする必要があるかな?

Apacheの動作を意図的に遅くするには

前述の通り、そのままだとtcpdumpがパケットを取りこぼしてしまうため、Apacheの動作に少し遅延を入れることにしました。本当は「全てのリクエストに対してm秒の遅延を入れる」みたいな設定ができればいいなと思って探しましたが、「Apacheで遅延をいかに減らすか」という記事は山ほど見つかるものの、意図的に動作を遅くする設定は無いようです(あるかも。そんなに本気で調べていないので)。

それよりも、Apacheのソースに手を入れて、全てのリクエストに決め打ちのディレイをusleep(3)で入れた方が簡単そうです。というわけでこのアプローチを取ることにしました。

なお、この際には2ヶ所に遅延を入れる必要があります。

  • HTTPリクエストを投げる部分
  • HTTPレスポンスを返す部分

リクエストに遅延を入れないと、マトリョーシカを「開けていく」部分が早すぎてtcpdumpがパケットを取りこぼします。また、返されるレスポンスにも遅延を入れて、マトリョーシカを「閉めていく」部分もパケットを取りこぼさないようにする必要があります。

HTTPリクエストに遅延を入れる

ApacheのHTTPリクエストを処理しているのは、server/protocol.cのap_read_request関数です。というわけで、この関数の最後にusleepを入れてやれば良いでしょう。なおApacheソースコードWebブラウザで見るには、公式のSubversionリポジトリが見やすいです(2.2.27/server/protocol.c)。

request_rec *ap_read_request(conn_rec *conn)
{
.....(省略).....
            ap_run_log_transaction(r);
            return r;
        }
    }

    /* 50ミリ秒の遅延 */
    usleep(50000);

    return r;
}

usleepではマイクロ秒を指定しますので、ここでは50000マイクロ秒=50ミリ秒=0.05秒の遅延を1リクエストごとに入れています。

HTTPレスポンスに遅延を入れる

ApacheのHTTPレスポンスに遅延を入れるのには、同じserver/protocol.cというファイルのap_finalize_request_protocol関数を利用しました。名前の通り、レスポンスのファイナライズに呼ばれる関数ですから、ここにusleepを入れておけば遅延をかけることができます。

/* finalize_request_protocol is called at completion of sending the
* response.  Its sole purpose is to send the terminating protocol
* information for any wrappers around the response message body
* (i.e., transfer encodings).  It should have been named finalize_response.
*/
AP_DECLARE(void) ap_finalize_request_protocol(request_rec *r)
{
    (void) ap_discard_request_body(r);

    /* tell the filter chain there is no more content coming */
    if (!r->eos_sent) {
        end_output_stream(r);
    }


    /* 50ミリ秒の遅延 */
    usleep(50000);
}
ソースから再ビルド

ソースをいじったので、手でビルドしてインストールします。

$ ./configure --prefix=/var/tmp/httpd-2.2.27 --enable-proxy --enable-proxy-http
$ make
$ make install
設定ファイルに追記&再起動

まず、以下行をhttpd.confの最後に入れます。

Include conf.d/*.conf

そして/var/tmp/httpd-2.2.27/conf.d/ ディレクトリに、先ほどの「proxy_(受付ポート番号).conf」という100個のファイルを入れて、Apacheを起動します。

# cd /var/tmp/httpd-2.2.27/bin/
# ./apachectl start

パケットキャプチャ結果

最初に、結果が分かりやすくなるように、Wiresharkの設定で[Edit]→[Preferences]から[User Interface]→[Columns]を選び、[Add]でSource PortとDest Portを表示するようにしておきましょう。

まずリクエスト部分のパケットキャプチャを見てみます。

3点ほど注目ポイントがあります。

  • 画像右上の[Dst Port]。HTTPリクエストが10080/tcp→9980/tcp→9880/tcp→……とドミノ倒しのように小さいポートへと順々に流れているのが分かります。ここが、マトリョーシカを「開けて」いる部分です。
  • 画像左上の[Time]を見ると、先ほどソースを改造して50ミリ秒の遅延を入れたことから、それぞれのリクエスト間隔がほぼ50ミリ秒+αとなっています。
  • 画像下部のパケットの中身を見ると、HTTPヘッダに[X-Forwarded-Host]が付き、そこに10080/tcpからここまで順々に開けてきたマトリョーシカが記載されていることが分かります。ApacheのProxyPassディレクティブは、このようにしてリクエストをプロキシしているわけです。

続いて、マトリョーシカの一番奥、中身に到達したところを見てみます。

Dst Port:80へのGETリクエストが発行されたあと、今度はSrc Portが順に100ずつ増えていっています。ここが折り返し地点で、HTTPリクエストが一番奥まで通って、HTTPレスポンスが始まる部分です。80/tcp→180/tcp→280/tcp→……と、順にマトリョーシカを「閉めて」いきます。

なお、HTTPレスポンスについても先ほどApacheのソースに手を入れて50ミリ秒の遅延を入れたことから、[Time]のカラムを見るとおおむね50ミリ秒ほどの間隔になっています。

最後に、マトリョーシカの一番外側を閉じてリクエストが完了した部分です。

リクエストした10080/tcpから、無事に応答が返りました。これでApacheマトリョーシカが完了です。

結論

特になし。