slowloris対策として、Apacheの2.1.15から入ったモジュールにmod_reqtimeoutというのがあります。

RequestReadTimeout header=10 body=30

このように設定することで、headerの受信が10秒以内、bodyの受信が30秒以内に完了しない場合、「408」エラーとできます。簡単で便利そうですね

公式ドキュメント http://httpd.apache.org/docs/2.2/en/mod/mod_reqtimeout.html


ただし、

Apacheをreverse proxyとして使用している場合はTimeoutにならず、リクエストの一部がproxy先に送られるという問題があるので注意が必要というか、はまったのでその話。

ちなみに、すでにBugzillaには上がっているけど、2.2系ではまだ対応完了してない https://issues.apache.org/bugzilla/show_bug.cgi?id=51103




以下再現方法。

まず、reverse proxy側で RequestReadTimeout と ProxyPass を設定します

RequestReadTimeout header=10 body=5
ProxyPass /backend http://127.0.0.1:5000

そして、backendとなるStarletには以下のpatchをあてます。

diff --git a/lib/Starlet/Server.pm b/lib/Starlet/Server.pm
index ea5a34a..f99f360 100644
--- a/lib/Starlet/Server.pm
+++ b/lib/Starlet/Server.pm
@@ -17,6 +17,8 @@ use Socket qw(IPPROTO_TCP TCP_NODELAY);
 use Try::Tiny;
 use Time::HiRes qw(time);

+use Log::Minimal;
+
 use constant MAX_REQUEST_SIZE => 131072;
 use constant MSWin32          => $^O eq 'MSWin32';

@@ -155,6 +157,10 @@ sub handle_connection {
         undef $can_exit;
         my $reqlen = parse_http_request($buf, $env);
         if ($reqlen >= 0) {
+            {
+                local $Log::Minimal::AUTODUMP=1;
+                warnf $env;
+            }
             # handle request
             if ($use_keepalive) {
                 if (my $c = $env->{HTTP_CONNECTION}) {
@@ -177,6 +183,7 @@ sub handle_connection {
                             $conn, \$chunk, $cl, 0, $self->{timeout})
                             or return;
                     }
+warnf "chunk size: %d", length($chunk);
                     $buffer->print($chunk);
                     $cl -= length $chunk;
                 }

関係ないけどStarletはシンプルなのでこういう検証に便利ですね

そしてtest用のpsgiアプリケーションを書いて

use Plack::Request;

sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $content = $req->content();
    my $length = length($content);
    [200,['Content-Type'=>'text/plain','Content-Length'=>length($length)+1],["$length\n"]];
};

起動、

$ plackup -s Starlet test.psgi

今度は、Bodyを送るのに5秒以上掛かるように細工したクライアントを書きます

#!/usr/bin/perl

use strict;
use warnings;
use IO::Socket::INET;

my $sock = IO::Socket::INET->new(
    PeerAddr => '127.0.0.1',
    PeerPort => '8080',
);

my $header = <<EOF;
POST /backend/foo HTTP/1.1
Host: example.com
User-Agent: slowpost
Content-Length: 32768
EOF

$header =~ s/\n/\r\n/g;

$sock->syswrite("$header\r\n");

for (1..8) {
    $sock->syswrite("1"x4096);
    sleep 1;
}

my $res;
$sock->sysread( $res, 8192 );

print $res;

これを使って、Apacheに向けてリクエストを送ってみると、、

$ perl slowpost.pl
HTTP/1.1 400 Bad Request
Date: Thu, 16 Feb 2012 02:28:51 GMT
Content-Length: 226
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
</body></html>

Bodyの送信に5秒以上掛かっているので、期待としては「408」が返って来そうですが「400」となりました。

Apacheのエラーログには

[Thu Feb 16 11:28:56 2012] [error] proxy: pass request body failed to 127.0.0.1:5000 (127.0.0.1) from 127.0.0.1 ()

このように記録されています。

ふむふむ、bodyのproxyに失敗しただけかと思うのですが、実際にはbodyも一部だけがproxyされ途中で切れています。

以下がplackupを起動したターミナルの出力

2012-02-16T11:28:54 [WARN] {'psgi.multiprocess' => 1,'SCRIPT_NAME' => '','SERVER_NAME' => 0,'HTTP_CONNECTION' => 'close','HTTP_X_FORWARDED_SERVER' => 'kazeburomba.local','PATH_INFO' => '/foo','CONTENT_LENGTH' => '32768','REQUEST_METHOD' => 'POST','psgi.multithread' => '','HTTP_USER_AGENT' => 'slowpost','QUERY_STRING' => '','REMOTE_PORT' => 55700,'SERVER_PORT' => 5000,'psgix.input.buffered' => 1,'REMOTE_ADDR' => '127.0.0.1','HTTP_X_FORWARDED_HOST' => 'example.com','SERVER_PROTOCOL' => 'HTTP/1.1','HTTP_X_FORWARDED_FOR' => '127.0.0.1','psgi.streaming' => 1,'psgi.errors' => *::STDERR,'REQUEST_URI' => '/foo','psgi.version' => [1,1],'psgi.nonblocking' => '','psgix.io' => bless( \*Symbol::GEN1, 'IO::Socket::INET' ),'psgi.url_scheme' => 'http','psgi.run_once' => '','HTTP_HOST' => '127.0.0.1:5000'} at /Users/hoge/perl5/perlbrew/perls/perl-5.12.2/lib/site_perl/5.12.2/Starlet/Server.pm line 162
2012-02-16T11:28:54 [WARN] chunk size:16384 at /Users/hoge/perl5/perlbrew/perls/perl-5.12.2/lib/site_perl/5.12.2/Starlet/Server.pm line 184

ヘッダも送られてきて、Bodyも16384byte読み込んでいます。

実際、StarletではBody部分がContent-Lengthの長さにならなければリクエストの処理に移らないので、問題とはならないのですが、Content-Lengthと異なっていても正常に処理をしてしまった場合(libapreqを使うmod_perlがそうだった気がする)問題となる可能性があります。

こわいこわい

おそらく原因としては、mod_reqtimeoutもmod_proxyもfilterで動いていて、reqtimeoutがエラーと判断した時にはすでにmod_proxyがbackendにコンテンツを送ってしまっていて、mod_proxyでリクエストのBofyの続きが読めなくなって「400」エラーって事なんだろうなぁと思ってます。

対策としては、RequestReadTimeout を使わない、もしくは使う場所を限定する(アップロードなど通信時間長くなるところには使わない)、Content-LengthやChunked-Transferを正しく扱うサーバを使ってのが考えられます。

そういえば昔、同じような問題に、LimitRequestBodyでもあたったことがあって、ApacheのFilter機構は難しいなぁと思った次第。

このブログ記事について

このページは、Masahiro Naganoが2012年2月16日 11:59に書いたブログ記事です。

ひとつ前のブログ記事は「nginx-1.1.x で httpなupstreamにもkeepaliveができるようになったので検証してみた」です。

次のブログ記事は「GrowthForecastに1分更新グラフ作成とサマリーなどのJSONフォーマットでの出力機能追加」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 4.27-ja