権威DNSサービスへのDDoSとハイパフォーマンスなベンチマーカ

この記事は、2023年3月19日(日)に行われたYAPC::Kyoto 2023における発表を編集部にて記事化したものです。

はじめに

さくらインターネットの長野です。CPAN IDはKAZEBUROTwitterGitHubも@kazeburoでやっています。

現在は、さくらインターネット株式会社 クラウド事業本部のSRE室というところで室長をやっています。SRE室については、今年2月にさくナレに掲載した「DNS権威サーバのクラウドサービス向けに行われた攻撃および対策 〜前編〜」にて紹介していますので、そちらをご覧ください。

余談ですが、2006年までは(本日の会場である)京都リサーチパークの4号館で働いていました。あれから16,7年経ってここで発表しているのは個人的にとても感慨深いです。

さて本日は「権威DNSサービスへのDDoSとハイパフォーマンスなベンチマーカ」というタイトルで発表させていただきます。SRE室では既存のクラウドサービスの開発運用にも携わっており、そのうちの一つに起きた実際の話からつながっています。

DNS水責め攻撃

DNS水責め攻撃とは

まずはDNSの水責め攻撃というものを紹介していきます。「『水責め攻撃』ってなんかふざけたこと言ってんな」というようなことを社内のSlackでも書かれたりしたんですが、正しい用語です。

どういう攻撃かというと、攻撃対象に、大量のランダムなサブドメイン、例えばfoo-bar-baz.example.comであればfoo-bar-bazの部分がランダムに変化します。こういった問い合わせを大量に発行して、DNSに負荷をかけて機能停止や機能低下を狙う攻撃です。

どういう風にやってくるかというと、オープンリゾルバ、最近少なくなりましたが、DNSの名前解決を外側からできるようになっているような脆弱性のある状態になっているものや、GoogleやCloudflareなどのパブリックなDNSサーバですね。こちらに対して、攻撃者はランダムなサブドメインの問い合わせを大量に発行します。ランダムな文字列なのでDNSのキャッシュサーバにはキャッシュがなく、そのドメインの権威DNSサーバに大量の問い合わせがやってきます。それによってDDoSとなる、という攻撃になります。

今回は水責め攻撃という言葉を使っていますが、ランダムサブドメイン攻撃(Pseudo-Random Subdomain Attack)と呼ばれることもあります。歴史的にはそんなに古くはなく、2014年に初めて観測されたと言われています。

先ほど説明した通り、GoogleやCloudflareからやってくるので、DNSの名前解決としては非常に正しい動作で、通信パケットとしても別に壊れているわけではありません。さらに、これらのDNSサーバから来る問い合わせは攻撃だけではないので、防ぐのが難しいです。8.8.8.8から来るDNSの名前解決をブロックするわけにもいきません。そういうわけで非常に対処が難しい攻撃であります。

実際の攻撃の記録

実際の攻撃の記録として、Mackerelのグラフをお見せします。これは去年(2022年)の12月15日から16日にかけて、1日20時間以上にわたって攻撃が来ていたというグラフになるんですけど、900万クエリ/分、秒に直すと15万qps(クエリ/秒)ぐらい、本当にランダムな文字でずーっと攻撃が来ていたというふうなものになります。この12月の時点では対策をしていたので、これによるサービスの影響はなかったんですけど、それ以前、2022年の夏ぐらいに初めて攻撃が来たときには、サービスのダウンにもつながってしまったということがありました。

ではどんな攻撃が実際に来てるのかをtcpdumpのデータでお見せします(ドメイン名をexample.comに変えています)。こういうふうなランダムな文字列でやってくるわけなんですね。何か意味のありげな言葉を組み合わせてたり、ランダムな文字列になっていたり、あとドットの数ですね、DNSではラベルと言いますが、そのラベルの数が増えたりしているのもわかるかなと思います。大文字小文字の混ざりっていうものもいくつかあります。これはGoogleのPublic DNSの仕様で、キャッシュサーバへの攻撃を防ぐためにこういったランダムな大文字小文字が混ざってるってこともあります。

なぜ「水責め攻撃」が有効か

なぜこの水責め攻撃が有効かというと、まず大量に来るっていうことが確かにあって、それによってDNSサーバに負荷が与えられるんですけど、中でもキャッシュに存在しない大量のクエリがやってくるというのが大きいです。キャッシュがあれば早く返せるんですけど、そのキャッシュに存在しないことで、DNSのリクエストのパケット処理が必要になります。

特に、最近使っているDNSサーバでは、DNSのパケットごとキャッシュしちゃうみたいなのがあって、そういうのだとパケットの中を見て計算することはなくなるんですけど、多くのDNSサーバはそうではないので、パケットの中身を解析して、中にあるDNSのクエリを処理しているので負荷がかかります。

もう一つ、今回の対象サービスであるさくらのクラウドのDNSアプライアンスというサービスにはお客様のDNSのレコードがたくさんあるので、それを管理するためにPowerDNSというDNSサーバと、そのバックエンドとしてデータベースを使っています。よって、そのデータベースが持つキャッシュにヒットしないと、都度SQLが発行されるわけです。15万qpsが来て、すべてキャッシュにヒットしないと、同じだけのSQLがバックエンドに発行されます。それだけ来てしまうとデータベースがもたなくなるので、CPU負荷が上がってサービスに影響が出るということがありました。

「水責め攻撃」への対策

ではどんな対策をしているかっていうところなんですけど、存在するレコードへのクエリ以外はDNSサーバの手前でフィルターするってことをやっています。dnsdistという、DNS専用のProxyサーバを導入しています。設定例は割愛しますが、ランダムな文字列で来ているようなもの以外の、本当に存在するドメインはdnsdistを通して、ランダムな文字列で来たらdnsdistでフィルターしてパケットをドロップするみたいなことをやってます。

しかし、新たなドメインへの攻撃、例えばexample.comについてはここでフィルタできても、今度はexample.jpにもし攻撃が来たら、その時はあわててフィルターを追加しないといけないんで、dnsdistは役に立ってはいるんですけど、まだまだやらなきゃいけないことはあるかなと思ってます。

攻撃を受け切れ

この話題を、JANOGという、ネットワークのオペレーションをするグループのイベントで発表したんですが(参考記事:前編後編)、そこでの議論では「受け切れるようにしてください」と言われました。15万qpsは少ないですと。40万とか100万とか受け切りなさいって言われたんですね。そんな…とも思うんですけどね…。Webアプリケーションで攻撃を受け切りなさいって普通言われないですよね(場内笑)。

というわけで、攻撃を受け切るパフォーマンスが必要だということです。パフォーマンス向上のための施策としては、スケールアウトやスケールアップ、負荷分散、チューニングなどですね。MySQLをずっと運用してるんで、MySQLのチューニングなどもしてたんですけど、もっと速度を出すために、このRDBMSを使うバックエンドをやめるというようなことも進めています。

ちなみに、私は「達人が教えるWebパフォーマンスチューニング」という本を共著で出しています。負荷試験やベンチマーカの実装が書かれていて、非常にためになるのでぜひ買ってください。

DNSサーバのパフォーマンスを測る

というわけで、パフォーマンスチューニングとか、その結果として本当に性能が上がってるのかどうかを確認するのが次の話になります。DNS水責め攻撃に対するDNSサーバのパフォーマンスを測るという話です。

ベンチマークツール:既製品か自作か

まず、DNSサーバのベンチマークツールはいくつかありまして、有名どころとしてはdnsperfというものがあります。これは、あらかじめ問い合わせをするクエリをファイルに10万個とか100万個とか書いて、それに対してベンチマークを行うツールになります。

これを使ってもよいかとも思ってたんですけど、せっかくなので作れるものだったら作ろうと。ランダムな文字列で大文字小文字なども混ぜたものを100万個もファイルに記述するのは面倒くさいんで、それらを動的に生み出して、長さやラベル数なども柔軟に変更してベンチマークをしたいと。そして、こういうベンチマーカを作ると、先ほど紹介した本でも最後の章にありますけど、ベンチマークの対象とするところとかDNSのパケットの中身など、DNSがどのように動いているのかという全体を把握できるので、自作するのがいいだろうってことで作ってみました。

ChatGPTでやってみた

ということで、試しにChatGPTで作ってみました。(実際にこれで作ったわけではありません)

これ結構日付が大事で、今やると全然違う結果が出てくると思うんですけど、3月10日に使ったときはこういう結果が出ました。

出てきたのはGoのコードで、mainの中でforループを1000回回して、ランダムな文字列を作ってポストしてます。ちゃんと動きそうですごいなあと思ったんですね。

で、これはベンチマークに適してるのかをもう1回ChatGPTで聞いてみました。これは3月16日にやったもので、その回答から一部抜粋しています。

いろいろ書いてあるんですけど、要するに性能評価としては適切ではありませんっていうのと、あとエラーが発生した場合には適切にエラー処理をする必要がありますということが書いてあります。ちゃんとわかっててやっぱりすごいなあと思いました。

ベンチマーカの必要条件

上記からベンチマーカの必要条件をまとめるとこんな感じですね。

まず、多数のリクエストを行うパフォーマンスが必要です。DNSサーバにもパフォーマンスが必要だし、ベンチマーカの方もパフォーマンスが必要ということです。それから、適切なエラーが取得可能であることが大事です。

適切なエラー処理

ではDNSにとって適切なエラーって何かっていうと、どこでエラーが起きたかがわかるようにすることです。ベンチマーカはテストでもあるんですよね。ISUCONのベンチマーカもテストなんですけど、意図した失敗も許容できる、つまり失敗すべきものも正しく失敗するという挙動ですね。

DNSで起きるエラーにはどんなものがあるかっていうと、まずTimeoutですね。これはDropを含みます。実際にフィルターするときはパケットをドロップして一切レスポンスしないってことができます。それからNXDOMAIN(ドメインが見つからない)だとか、SERVFAIL(サーバ側でエラーが発生している)だとか、REFUSED(拒否)っていうのを返すこともできます。

上記はGoの標準のnet.Resolverを使ったコードの例です。これを使うとLookupHostの結果としてエラーが出てくるんですけど、これだとIsNotFoundは取れますが、それ以外の、REFUSEDされたのかNXDOMAINなのか何なのかっていうのがちょっとわかりません。さらにおまけとして、LookupHostはAレコードと同時にIPv6のAAAAの名前解決も行うのですが、ベンチマークとしてはちょっと余計なことをしてることになります。

もう一つ、Goの世界では有名なDNSのライブラリとして、miekg/dnsというのがあります。そちらを使ったコードの例です。これを使うと、set.Questionのところでランダムな文字列とゾーン名をつなげて、レコードの解決だけをすることができます。こうすると、Rcodeのところにステータスコード、つまりREFUSEDとかSERVFAILなどのレスポンスコードが入るようになるので、こちらを使うと正確なエラー処理ができるようになります。ということでこちらを使っています。

パフォーマンスについて

もう一つの必要条件であるパフォーマンスについては、計測対象のパフォーマンス、今回であればDNSサーバのパフォーマンスよりもベンチマーカが強くないと、正しいパフォーマンスの計測ができません。ISUCONのベンチマーカもそうです。ISUCONも対象のWebアプリケーションがどんどん高速化していくので、ベンチマーカはそれ以上強くないといけないんですね。現代においては、Apache Bench、いわゆるabコマンドですね、こちらを使うことがあるのかなと思うんですけど、これが常に適切なツールとはならないというのと同じです。

それから、クライアント側にもハイパフォーマンスが求められます。先ほど例示したAAAAの名前解決もするのはHappy EyeBallsのためですが、ベンチマーカとしては余分な処理をしてたとするとそれを削減しないといけません。あと、パフォーマンスを出すためにはもちろん並行・並列化やチューニングをしていきます。

並行・並列化

Goの世界では並行・並列化は非常に簡単です。上記の例ではgoroutineを使っています。まずgo funcの中でgoroutineを起動して、その中でループで名前解決を実行してやることで、簡単にパフォーマンスを上げられます。

Goのパフォーマンスチューニング

さらにパフォーマンスを上げていくにはGoのパフォーマンスチューニングテクニックがいくつか必要になります。一番大事なのはメモリーのアロケーションをしないことです。それから計測をしましょう。「推測するな計測せよ」ってやつですね。そして最後の最後に、GOGCっていうのもチューニングするポイントとしてあるかなと思ってます。

メモリーアロケーションを減らす

まずメモリーアロケーションを減らす話です。

先ほどの例だと、右側のコードに、DNSクライアントを作ったり、メッセージの初期化をしたり、ホストとポートをくっつける処理があったんですが、それを左側のコードに移動して、1回作ったものをループの中で使い回すようにするとメモリーアロケーションが減ります。

あと、Goの世界で大事なこととして、文字列の変更が不可能というのがあります。上記のコード例では、文字列の連結を何度も行っています。ランダムな文字列を入れて、ドット(.)を入れて、example.comを入れて、またドットを入れて、という処理をしていますが、こうすると4回メモリーアロケーションするんですね。こういうことをするとどんどん遅くなっていくので、対策しないといけません。

その方法として採用してるのが、bytes.Bufferというのを使って、それもループの外で1回作ったものをループの中で使い回していくっていうことをやってます。右側のコードでは、bytes.Bufferにランダムな文字列を入れて、ドットを入れて、WriteStringでゾーンを入れて、最後にsb.Byteでバイト列を取ってきて、それをここではunsafe.Stringというものを使っていますが、メモリーアロケーションせずにバイト列から文字列に変換しています。

さらに、ランダムな文字列の生成においても、WriteByteというもので一文字一文字くっつけていくだけで、新たなメモリーのアロケーションをせずに文字列を生成しています。

このようなチューニングの結果、1回あたりのアロケーション回数が8だったのが3に、つまり半分以下になりました。その結果として9倍程度高速になりました。これはsetQuestionまでの部分ですね。実際の名前解決の部分はベンチマークに含まれていません。

プロファイリングによる問題箇所の特定

では、この3は何だろうと、いろんな推測をしたんですけど、「推測するな計測せよ」ってことで、プロファイリングを行ってみます。goのtestに付いているものですかね。実行時に-cpuprofileとか-memprofileというオプションを付けると、ベンチマークをしながらプロファイリングを行うことができます。

その結果がこちらですが、これを見ると、上の方にあるdns.SetQuestionのあたりが怪しいように見えます。

そこで、このdns.SetQuestionの中身を見ると、問い合わせを格納している配列を都度生成してたりだとか、uint16なランダムIdを毎回生成していることがわかりました。名前解決のときに、複数個のリクエストを投げて帰ってきたものを処理するためにランダムなIdを使うんですけど、ここではベンチマークなので、固定の数字だったり、atomicでどんどん増えていく数字に変更しました。

このようにすると、先ほど3だったアロケーションの回数がついに0になりました。そして、先ほど9倍だったのが、当初の80倍超えの高速化ができました。実際の名前解決するまでを含めてもかなり高速化の恩恵が受けられます。

GOGCのチューニング

最後にGOGCですね。これはGoでガベージコレクションを実行するためのヒープサイズの目標となるパラメータです。デフォルトが100で、大きくすることでGCの頻度を減らしてCPUコストを下げることができますが、デメリットとしてメモリ使用量が若干増加します。

こちらの実行例は、上が何の設定もしない100の状態で、下が500にしたときです。15万qpsだったのが18万qpsまで増えるので割とメリットがありますが、これはやっぱりこれまでにいろんなチューニングをした後の最後の一押しとして使うのがいいかなと思ってます。

まとめ

それではまとめです。

まず水責め攻撃は非常にやっかいです。次に、ベンチマーカを作るにあたっても大事なことが二つあって、一つは適切な情報(エラー)が取得可能であること、それからベンチマーク対象に負けないパフォーマンスが求められることです。そして、Go言語で作る上でのパフォーマンスチューニングの話をしました。ご清聴ありがとうございました。

質疑応答

Q:15万qpsのリクエストが来て、DNSサーバ自体の処理と、バックエンドのデータベース側のSQLの処理で負荷が高まっているようですが、ネットワークのトラフィックはそこまで大きな影響はなかったのでしょうか?

A:そうですね。15万qpsぐらいだったら帯域としては数Mbpsなので、大したことはないですね。もっと大きな帯域を占めるような攻撃が来た場合は、もうサーバの手前、ネットワークレイヤーで止めるってことになってます。

Q:バックエンドのRDBMSをやめた後に何を採用されたのですか?

A:今回の発表ではまったく触れていませんが、BIND backendというものがありまして、BINDという昔から使われているDNSサーバのゾーンファイル形式のデータを食わせて立ち上げることができます。これが一番パフォーマンスが高いようなので、それを使おうとしています。

おわりに

YAPC::Kyoto 2023 にて発表する機会をいただきありがとうございました。YAPC::Kyotoのrebootを見事成功させたスタッフの皆様、オンライン、オフラインでトークを聞いてくださった方、さくらインターネットのブースに足を運んでいただいた皆様に感謝しております。

イベントでは数年ぶりに顔を合わせるエンジニア、新しい参加者とともにさまざまな話をし、オフラインのイベントを十分に楽しませていただきました。この体験をより多くのエンジニアができるよう、働きかけをしていきたいと考えています。

発表はJANOG51 Meeting in Fujiyoshidaにおいて発表したDNS水責め攻撃の対策に関わるベンチマーカとGo言語のパフォーマンスチューニングについて紹介しました。

現在、攻撃に耐えることができる、より安定した価値の高いサービスを目指しシステムの改修を行っています。こちらについてもまたどこかで事例として発表できれば思います。

私のブログでもYAPC::Kyotoでの発表について書いています。よろしければご覧ください。

YAPC::Kyoto 2023でDNS水責め攻撃とGoによるベンチマーカの発表をしてきました #yapcjapan

発表資料

社会を支えるパブリッククラウドを一緒に作りませんか

最後にちょっと宣伝です。「社会を支えるパブリッククラウドを一緒に作りませんか」っていうことで、PerlとかGoとかPythonを使ってインフラの基盤からフロントエンドまでいろいろやってますので、興味がある方はエンジニア採用のページをご覧ください。