Webアプリを創る 😊クリエイティブWeb

カテゴリー: プログラミング

ASP.NET Core でボットネット対策(その1 middlewareを使う)

2016年12月20日
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 サポートはかなり進んできているという印象です。

サイトを ASP.NET Core + .NET Core で作り直しました

2016年2月23日

この Webサイトは、Umbracoで作っていましたが、ASP.NET Core(ASP.NET 5)+ .NET Core で作成し直してみました。Umbracoも拡張性が高くて悪くはないのですが、自分でプログラムを書きたい場合には自由度が小さくなるのとデバックの時には重くなるのが欠点でした。それで思い切って、作り直すことにしました。選択肢としては、ASP.NET MVC 5、ASP.NET Core + .NET Framework 4.6、ASP.NET Core + .NET Core の三つでしたが、思い切って NET Core + .NET Core にしてみました。

ASP.NET 5 については、スケジュールが変更され、名称も ASP.NET Core に変更になるということで、RC2 が出る直前になってドタバタしているようですが、実際使っていると問題点があることはわかります。でも、基本的なところはそれほど変わっていないし安定しているので使えないことはないです。

運用を始めたばかりで、切り替えの時に少し失敗してサーバーを止めてしまったこともありましたが、取りあえずは普通に動いています。Ubuntuサーバーの方でもテストしていますが、そちらも大きな問題はなく動作しています。

ASP.NET Core、.NET Core については、最近マイクロソフトから以下のブログが出て、方向性がよくわかるようになったと思います。

ASP.NET 5 is dead - Introducing ASP.NET Core 1.0 and .NET Core 1.0

An Update on ASP.NET Core and .NET Core

Porting to .NET Core

ASP.NET Community Standup – February 16, 2016

Porting MSBuild to .NET Core

コンパイラーが DNX から CLI に変更になるというのは賛成です。このサイトの発行時のファイルサイズは、約300MBぐらいで、その内runtimesが106MB、packagesが190MBです。runtimesの分は、ホストのOSにインストールされたランタイムを使う必要がなくなった代わりに必要になるもので、どんな小さなアプリにでも必要になります。packagesの分が大きくなっているのは、.NET Core だけでなく下の図のように Windows Phone 8 や Xamarin のものまで含まれているためです。Webサーバに関していえば、これぐらいのファイルサイズでも我慢できなくはないのですが、コンソールアプリケーションで 100MBというのはちょっと大きすぎだと思います。AOT(事前コンパイラ)をしてファイルサイズも減らして欲しいし、最初の起動時間も短縮して欲しいと思うし、コンソールアプリケーションだと、ネイティブコードにコンパイルして欲しいところです。 packages

.NET Core を使う上での支障は、ライブラリーの対応があまり進んでいないことです。「Porting to .NET Core」に書いてあるように、.NET Core でも System.Data、System.DirectoryServices、System.Drawing、System.Transactions、System.Xml.Xsl、System.Xml.Schema、System.Net.Mail、System.IO.Ports、System.Workflow、System.Xaml で、時間がなくて移行があまりできていないそうです。 そういう状況なので、サードパーティ製のライブラリーの対応状況は悪いです。

現状では、ASP.NET だと、ASP.NET Core + .NET Framework 4.6 という選択肢が現実的だし、UWP の方も windows 10 mobile が殆ど普及していないことや、デスクトップPCでも依然として主力は Windows 7 であることを考えれば、.NET Core を急ぐ必要はないと思います。

しかし、5年後を考えると .NET Core が必要というのは間違いないと思います。また、下の図は、Azure の VM の Linux と Windows の価格です。個人だとこれだけ価格に差が出てくると Linux の方を使いたいと思ってしまいます。それで、これからは、 Linux のことも書いていきたいと思っています。 azure

Web をするなら PHP だけでなく JavaScript を使ってみよう

2016年2月5日

JavaScript は、Webデザイナーのための簡易言語だと言われたり、まつもとゆきひろ氏には「人類のためにJavaScriptは何とかしたほうがいい(出典:エンジニアライフ)」と言われたりしたプログラム言語ですが、ECMAScript 2015 が承認されたことでモダンな言語に変身しつつあります。

2ヶ月ほど前の話ですが、PHP Advent Calendar 2015 の「PHPを使いもせずDISってる君達へ」という記事をみて、JavaScript は、PHP より遙かにモダンな言語になると思ったのでメモしておきます。

その記事では例を、Ruby で書いてありますが、JavaScript の場合はどうなるかというと、ECMAScript 5.1(ES5)では以下のように記述できます。IE8 は対応していませんが、それ以外の普通に使っているブラウザーは対応しています。

var a = [2,4,6,8,10]
    .filter(function(num){return num <= 8})
    .map(function(num){return num * num})
    .filter(function(num){return num >= 20})  
    .reduce(function(previous, current, index, array){return previous * current});  

また、ECMAScript 2015(ES6)では、アロー関数式が使えるので、Ruby と同様に以下のように簡潔に記述できます。

var a = [2,4,6,8,10]
    .filter((num) => num <= 8)    
    .map((num) => num * num)    
    .filter((num) => num >= 20)    
    .reduce((previous, current) => previous * current);      

やはり、PHP の配列処理はやはり不便です。PHP も配列などのコレクションの処理をするときには、高階関数を使って処理できるようになってほしいと思います。

JavaScript の ECMAScript 2015 への対応状況ですが、ECMAScript Compatibility Tableというサイトで調べると、Edge13 83%、Firefox45 85%、Chrome49 91%、Safari9 54% という状況で対応が進んできていますす。

ECMAScript Compatibility Table

ブラウザーではIE11が全く対応していないので、直接使うことは厳しい状況ですが、TypeScript を使えば、ソースコードとして使えるし、Node.jsだと、近いうちに直接書くことができるようになると思います。

JavaScript は Node.js を使えばサーバー側でも動くし、PHP では動かないブラウザー上でも動作します。これからは、Webをしたいのであれば、PHP だけでなく JavaScript を使った方がいいと思っています。

参考までに、C#の場合は、LINQを使って以下のように書けます。C#は、以前は Ruby の方が効率的にプログラムが書けると思っていましたが、C# 3.0 で LINQが使えるようになり、C# 5.0では async/await で非同期処理ができるようになって Ruby に追いついたと思います。配列処理のことについては、C# やるなら LINQ を使おうに詳しい解説があるので、ここでは説明を省略します。

var a = new int[] { 2, 4, 6, 8, 10 }
    .Where((num) => num <= 8)    
    .Select((num) => num * num)    
    .Where((num) => num >= 20)    
    .Aggregate((previous, current) => previous * current);  

ASP.NET 5 Preview を使ってみた

2015年3月2日

Visual Studio 2015 CTP 6 が公開されるとともに、スコット・ガスリー氏が「Introducing ASP.NET 5」とういうブログを公開しました。その記事を読んで、今回の ASP.NET 5 は、なかなか面白そうだと思って、Visual Studio 2015 CTP 6 をインストールして、ASP.NET 5 を使ってみました。ASP.NET 関係で使ってみたいなと思ったのは本当に久しぶりのことです。余談ですが、Visual Studio 2015 CTP 6 は、容量が多くてインストールが大変でした。コントロールパネルをみると容量が実に11.5GBになっていてマンモスです。

image

まず、既存のWEbアプリの移行にどれぐらい手間がかかるかを、ASP.NET MVC 4 で作成した weather.ecitizen.jp を使って試して見ました。Visual Studio 2015 CTP で ASP.NET 5 の」プロジェクトを作ると左の図のように、ファイルが wwwroot の静的部分とASP.NET部分に別れるのが大きな特徴です。現時点では移行ツールはないのですが、Models、Views、Controllers といったメインの部分は、コピーして少し修正してやれば動作するので、移行に少し手間はかかるけどそれほど難しくはないと感じました。

クライアント関係のライブラリーの管理については大きく変更されています。ASP.NET のサイトに「Manage Client-Side Web Development in Visual Studio 2015, Using Grunt and Bower」という解説がでているので、それを見て設定してみました。ライブラリーの管理は、NuGetではなく Bower で行い、その後の設定を Grunt でするようになっています。Bower、Grunt も実際に使ってみるとかなり使いやすいツールです。Visual Studio 2013 では、そういう作業には NuGet と Web Essentials 2013 を使っていましたが、Web Essentials にはまだバグらしきものがあるので、何でも独自に作成するよりは実績のあるツールを使った方が確かにベターだと思います。Web Essentials 2015 CTP 6 も公開されていますが、以前よりもずっとシンプルになっています。また、LESS、Sass、CoffeeScript 等のコンパイル機能は、Grunt 等のツールを使えということで、対応しないそうです。

Visual Studio 2015 では、従来の ASP.NET MVC 5 と互換のある ASP.NET MVC 5.2 の Webアプリケーションも開発できますが、HTML 5 を使うような Webアプリの開発だと Bower、Grunt を使わないと不便になるという気がします。自分のような場合だと、Visual Studio 2015 に移行するときには、できるだけ早く ASP.NET 5 に移行した方がベターなように感じました。

最後に、現在の weather.ecitizen.jp と ASP.NET 5 に移行したもののスピードを「WebPagetest」を使って比較してみました。ASP.NET 5 アプリは、.NET Core CLR か 従来からの .NET CLR でも発行できます。.NET CLR で発行すると容量は約100MB、.NET Core CLR だと約70MBあります。それを Windows 2012 R2 サーバーにインストールしました。結果は以下のとおりで、上が ASP.NET 5 アプリで、下が従来のものです。DNS Lookup は、ASP.NET 5 アプリの方が時間が掛かっていますが、それ以外ではレスポンスの時間には特に差はありませんでした。DNS Lookup については、サブドメインを新たに作成したばかりなので伝播があまりできていなかった影響がでているかもわかりません。ASP.NET 5 はまだ CTP の段階なので、リリース版までにはチューンナップがされると思うので処理速度の面でも期待したいと思いす。

image

image

 

 

Xamarin.Forms をテストしたら Label より WebView の方が早かった

2015年1月30日

Xamarin.Forms は、iPhone と Android の両方のアプリが一つのソースで作れるという魅力的な開発ツールです。それで、アプリを作ろうと思って試してみました。

自分にとっての一番の問題点は、Xamarin.Forms の画面表示が意外と遅いことです。Grid と Label を使って、カレンダー1年分の表示をする下の図のようなプログラムを作って、Nexus 5 で表示させてみたら、ボタンを押してから表示されるまでに3秒以上かかりました。iPhone 5s だとそれよりはかなり早くなりますが、それでも1秒以上かかっている感じです。

今回のカレンダーだと日付だけですが、実際のアプリにするためには、さらに色を変えたりアイコンを追加したりする処理が必要なので、日付だけでこれだけ時間が掛かってしまうと厳しいと思います。

ところが、WebView を使って HTML の Table でカレンダーを作ってみると、今度はほとんど遅延せずに動作します。Grid+Label より WebView の方が遙かに早いようで、Android では特にその傾向が強いように見えます。

なお、プログラムは、GitHub の方に SpeedTest として公開していますので、興味のある方は実際に試して見てください。

01cfb898adea3aa4e34d8c5afecf4798d870b486c9

Xamarin.Forms の Grid+Label を使った画面表示が遅い原因を調べてみると、stackoverflowに Why is Xamarin.Forms so slow when displaying a few labels (especially on Android)? という質問があって、どうも Label の表示が遅いようです。

stackoverflow 方では、40個のLabelで、表示に 100ms 以上かかり、Android の場合は約300ms かかるとなっています。今回のカレンダーの場合は、約460個のLabelをつかっていて stackoverflow の約10倍なので話としては一致しています。

stackoverflow には、改善策も書いてあます。Xamarin.Forms で処理を速くする方法としては、UI にはできるだけネイティブを使えばいいようです。

自分の場合は、Web の方を主力にしたいので、UI にネイティブを使うのは時間的にはかなり厳しいです。それで、当面は WebView を使ったハイブリッドアプリを検討していこうと思っています。そして、Xamarin.Forms の処理が早くなることに期待したいと思います。