DDoS 攻撃への対応

この記事は「ASP.NET Advent Calendar 2016 - Qiita」の20日目になります。

昨日のブログでは、ボットネットからアクセスがあることを書きましたが、その中で Google Cloud Datastore は自動スケールするので役に立ちそうだということを書きました。自動スケールされると破産するのではと心配する人もいると思いますが、Google Cloud では費用制限を設定するので破産することはないし、1秒間に1万回の読み込みをしても、1時間だと $21.6 とそれほど大したことはありません。

ただし、攻撃を放置しておくと結構な料金になってしまうので、攻撃を止める方法を考えてみました。

まず、DDoS 攻撃の現状がどうなっているかについては、Googleのシンクタンク部門である Google Ideas とセキュリティ企業の Arbor Networks と協力して、DDoS攻撃の状況をリアルタイムで表示するDigital Attack Mapを公開しています。そのサイトの Understanding DDoS によると、1週間小さな組織のシステムをダウンさせる能力があるボットネットを $150 で借りられるそうです。自分の場合は、最初はこの程度でできる攻撃への対応を考えてみようと思っています。

DDoS 攻撃への対応

攻撃を止める基本は、ボットのIPアドレスを特定して、そこからのアクセスを拒否することです。IPアドレスが特定できれば、アクセスの拒否については、ファイアーウォール、リバースプロキシーでも簡単にできます。そのため、DoS攻撃の場合は、比較的簡単に防御できます。

しかし、DDoS 攻撃の場合は、大量のボットから攻撃があるので、そもそも正常なアクセスなのか攻撃なのかを区別するのが容易でないし、攻撃元を特定して遮断していく作業は容易でありません。もし、攻撃元がボットをどんどん変更して攻撃してくれば、対応は本当に困難になります。

HTTP リクエストの場合、接続する URL 以外に IPアドレス、Referer、User-Agent の情報が送られてきます。低価格で借りたボットネットであれば、これらの情報に片寄りが生じるはずなので情報をうまく解析すれば、ボットのIPアドレスは特定できるはずです。

また、Pokémon GO で有名になったコイル警備員、すなわち Google の reCAPTCHA を使うことも有効です。アクセス拒否は、もし間違って拒否をするとそのユーザーを失うことになるので、疑わしいという状況では使いたくありません。でも、コイル警備員だとユーザー側の負担は遙かに軽いので、疑わしIPアドレスに対して使う事ができます。

Middleware を作成して特定のIPアドレスからのアクセスを拒否する

まず最初に、ASP.NET Core アプリケーションで、特定のIPアドレスからのアクセスを拒否するケースを考えてみます。

この場合には、HTTP Request があって、そのリクエストが MVC の処理に入るまでに処理をする必要があるため、Middleware を作成して処理する必要があります。ASP.NET Core の Middleware の Document に、middleware pipelineの図(下図)がありますが、

middleware pipeline の画像

その図をみれば、どう処理をしたらいいかをイメージできると思います。標準の MVC Middleware コンポーネントの前にカスタム Middleware コンポーネントを作成して処理をすることになります。

Middleware の作成に Visual Studio を使う場合は、「ミドルウェア クラス」のテンプレートがあるのでそれを使うと便利です。それを少し変更して非同期にすると下のようなコードになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace MyApp
{
    // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
    public class MyMiddleware
    {
        private readonly RequestDelegate _next;
        public MyMiddleware(RequestDelegate next)
        {
            _next = next;
        }
        public async Task Invoke(HttpContext httpContext)
        {
            await _next(httpContext);
        }
    }
    // Extension method used to add the middleware to the HTTP request pipeline.
    public static class MyMiddlewareExtensions
    {
        public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<MyMiddleware>();
        }
    }
}

このコードと middleware pipeline の図を眺めていると大まかな使い方は理解できました。Invoke メソッドの、await _next(httpContext) が次のパイプラインを呼び出す処理で、その前後にコードを書くことで独自の処理を行わせることができます。

以下は、上のコードを修正して、特定のIPアドレスからのアクセスを拒否するようにしたサンプルです。

    public class AntiBotMiddleware
    {
        private readonly RequestDelegate _next;
        //拒否するアドレスの指定
        private readonly List<string> _blackIpAddress = new List<string> {"192.168.0.1", "192.168.0.2"};
        public AntiBotMiddleware(RequestDelegate next)
        {
            _next = next;
        }
        public async Task Invoke(HttpContext httpContext)
        {
            //IPアドレスの取得
            string remoteIpAddress = httpContext.Connection.RemoteIpAddress.ToString();
            if (_blackIpAddress.Contains(remoteIpAddress))
            {
                //403 を返す                
                httpContext.Response.StatusCode = 403;
                return;
            }
            await _next(httpContext);
        }
    }

_next を呼ばなければ、次のパイプラインに行かずに返ります。それで、ブラウザーでは「サーバーエラー 403アクセスが拒否されました」と表示されるようになります。

この middleware を実行させるためには、Startup.cs の Configure メソッドに登録する必要があります。

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();
            app.UseAntiBotMiddleware();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

ここで、注意すべきことは、登録の順番が重要になってくるということです。今回の middleware は、その性格からいって UseMVC より前に置く必要があります。そして、UseStaticFiles の前におけば、静的ファイルもアクセス拒否の対象になり、後ろに置けば静的ファイルを除いたものがアクセス拒否の対象になります。

このサンプルは、プロキシーサーバーを使っている場合はうまく動作しません。httpContext.Connection.RemoteIpAddress が、プロキシーサーバーのアドレスになるためです。以下は、Nginx をプロキシーサーバーに使っている場合のサンプルです。

        public async Task Invoke(HttpContext httpContext)
        {
            //IPアドレスの取得
            string remoteIpAddress = GetRequestIP(httpContext);
            if (_blackIpAddress.Contains(remoteIpAddress))
            {
                //403 を返す                
                httpContext.Response.StatusCode = 403;
                return;
            }
            await _next(httpContext);
        }
        private static string GetRequestIP(HttpContext httpContext)
        {
            string ip = GetHeaderValueAs("X-Forwarded-For", httpContext);
            if (string.IsNullOrWhiteSpace(ip))
                return  httpContext.Connection.RemoteIpAddress.ToString();
            return ip;
        }
        private static string GetHeaderValueAs(string headerName, HttpContext httpContext)
        {
            StringValues values;
            if (httpContext?.Request?.Headers?.TryGetValue(headerName, out values) ?? false)
            {
                return values[0];
            }
            return null;
        }

なお、このサンプルが動作するためには、Nginx側で、X-Forwarded-For の設定をしておく必要があります。

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Dependency Injection を使う

上のサンプルで不便なのは、拒否するアドレスの指定を初期化で行っているところです。拒否するアドレスは画面で検索できたり変更できた方がいいですね。その場合には、Dependency Injection(DI)を使うのが自然です。DI という名前を聞くと難しそうに思うのし、解説を読んでも難しいのですが、以下のように簡単に作って試してみたら動作しました。

まず、普通に Class を作ります。(正式には、最初に Interface を作るようです)

public class AntiBotServise
{
    public List<string> BlackIpAddress { get; set; }
    public AntiBotServise()
    {
        BlackIpAddress = new List<string> {"192.168.0.1", "192.168.0.2"};
    }
}

すべてのリクエストで共通に使用するので、Startup.cs の ConfigureServices で AddSingleton を使って DIサービスコンテナに登録します。

public void ConfigureServices(IServiceCollection services)
{
   services.AddSingleton<AntiBotServise>();
   // Add framework services.
   services.AddMvc();
}

Middleware の方で、次のように Injection します。

public class AntiBotMiddleware
{
    private readonly RequestDelegate _next;
    private readonly AntiBotServise _antiBotServise;
    public AntiBotMiddleware(RequestDelegate next, AntiBotServise antiBotServise)
    {
        _next = next;
        _antiBotServise = antiBotServise;
    }
    public async Task Invoke(HttpContext httpContext)
    {
        //IPアドレスの取得
        string remoteIpAddress = GetRequestIP(httpContext);
        if (_antiBotServise.BlackIpAddress.Contains(remoteIpAddress))
        {
            //403 を返す
            httpContext.Response.StatusCode = 403;
            return;
        }
        await _next(httpContext);
    }
}

Controller や View でも、同様に Injection できるので、画面で検索できたり変更したりできるようになります。

補足

長くなってきたので、middleware で eCAPTCHA を使う話は次回にします。

ASP.NET Advent Calendar では、Azure の話が多いようです。確かにエンタープライズ向けのクラウドでは Azure がベストだと思います。

しかし、DDoS の件を調べていると Google の名前がたびたび出てきます。Google は、公開用 Web サーバーが有力な収益源の一つなので、その関係のサービスは、Google の方がはるかに充実しています。また、.NET Foundation に加盟したことでもわかるように、Google Cloud Platform での .NET サポートはかなり進んできているという印象です。