Hateburo: kazeburo hatenablog

SRE / 運用系小姑 / Goを書くPerl Monger

ISUCON13のベンチマーカーのDNS水責め攻撃について

この記事はさくらインターネット Advent Calendar 2023の12月3日の記事になります。

先日行われました ISUCON13 の作問を担当しました。参加者の皆様、スタッフの皆様ありがとうございました。

このエントリではISUCON13のDNSに関わる要素とベンチマーカーから行われたDNS水責めについて紹介します。

ISUCON13の問題の講評と解説は以下のエントリーでも行っていますので読んでいただけると嬉しいです

isucon.net

こんいす〜

ISUCON13における名前解決

上記のエントリーにもある通り、今回のISUCONではDNSが問題の一部として出てきます。

これまでポータルから参加者は割り振られたサーバの中から負荷をかけるサーバ1台選択し、ポータルはそのサーバに対して負荷走行を行うことが多くありましたが、今回はサーバ1台を選択したら、ベンチマーカーはそのサーバの UDP/53 で起動しているDNSサーバ(PowerDNS MySQL backendを使用)に対して名前解決のリクエストを行い、得られた結果のIPアドレスに対して接続をし、負荷走行を行います。

もちろん、得られたIPアドレスが参加者に割り振られたサーバでなかった場合、エラーとなります。

ベンチマーカーの実装で利用しているGo言語の標準的なHTTPクライアントでは、TCP接続を行う関数を差し替えることができ、そこで独自の名前解決をいれています。

transport := &http.Transport{
    DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
        // ここに独自の名前解決を実装する
        return dialer.DialContext(ctx, network, addr)
    },
}
return &http.Client{
    Transport: transport,
}

名前解決にはGo言語標準の net.Resolver ではなく、レスポンスコードなどを確認するため、miekg/dns を使っています。

github.com

ISUCON13におけるDNS水責め攻撃

ISUCON13でDNSを導入した狙いの一つがDNS水責め攻撃を負荷走行中に再現することでした。さくらインターネットでもDNS水責め攻撃をうけてさまざまな対策をしています。実際の攻撃や対策の内容は以下のスライドを参考にしてください。

speakerdeck.com

当日のマニュアルにもDNSへの攻撃は記載しています。

負荷走行中、DNSサーバに対していわゆる「DNS水責め攻撃」が行われます。 DNS水責め攻撃はランダムなサブドメインを生成し、大量のアクセスを行うことでDNSサーバの応答に影響を与えることを目的とする攻撃手法です。 ベンチマーカーはDNS水責め攻撃およびスクレイピングを行い、名前解決ができると HTTPS によりアクセスを試みる動作を行います。失敗(NXDOMAINや応答なし)では来ません。

ISUCON13のDNS水責め攻撃はスコアに影響をしない、負荷走行中のサブ要素となっています。あまりにDNS名前解決やスクレイピング的な動作が負荷とならないよう、最大 3000 QPS以上にならないようにしてありました。また、一定のパフォーマンスを満たしている状態で徐々に並列数が上がるようにしてあるため、負荷が上がりすぎず、負荷走行中には通常の名前解決と合わせて最大15万から18万クエリ程度が最大だったと思われます。

DNSおよびDNS水責め攻撃への対応

ISUCON13におけるDNSへの対応ですが、以下のものを想定していました

  1. MySQLスキーマでわざと消してあるインデックスを追加
  2. DNSをアプリケーションのホストと切り離す
  3. TTLおよびPowerDNSのチューニング
  4. MySQLからBINDゾーンファイルバックエンドに切り替え、ワイルドカードレコードを設定
  5. DNSサーバの自作、dnsdistや自作DNSサーバで水責め攻撃の応答を遅延させる

1. インデックスの追加

データベースのログ解析、プロファイリングを行なったチームはインデックスが不足しているのを見つけることができたのではないかと思います。

ISUCON13の初期実装では全体的にインデックスが「ない」状態ですが、こちらもインデックスも欠けています。レコードを管理するrecordsテーブルにおいて、name に対してあるべきインデックスがなく、作成する必要があります。

CREATE TABLE records (
  id                    BIGINT AUTO_INCREMENT,
  domain_id             INT DEFAULT NULL,
  name                  VARCHAR(255) DEFAULT NULL,
  type                  VARCHAR(10) DEFAULT NULL,
  content               VARCHAR(64000) DEFAULT NULL,
  ttl                   INT DEFAULT NULL,
  prio                  INT DEFAULT NULL,
  disabled              TINYINT(1) DEFAULT 0,
  ordername             VARCHAR(255) BINARY DEFAULT NULL,
  auth                  TINYINT(1) DEFAULT 1,
  PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';

ALTER TABLE records ADD INDEX idx_name (records);

PowerDNSのMySQLバックエンドのスキーマはこちらでも確認できます。

doc.powerdns.com

2. DNSを別サーバに移動する

今回のベンチマーカーではDNSから返すIPを変更することで負荷走行を行うサーバを変更できるので、それを利用してDNSサーバとアプリケーションサーバを別のサーバにする構成変更ができます。制限されているとはいえ、3000 QPSはそれなりの規模であり、負荷を分散する意味は十分にあります。

上記のインデックスと構成変更を行うことで一旦はDNSについては傍においておけるようになっていたのではないかと思います。

3. TTLとPowerDNSの設定

DNSは分散システムであり、キャッシュを適切に利用することで成り立っているシステムです。ところがISUCON13の初期状態ではまったくキャッシュをしないように設定しているため、これを変更するだけでもサーバに対する負荷は削減できます。

(なぜキャッシュを無効化していたかは、DNSの反映を可能な限り高速にするためというストーリーと考えていただけると幸いです)

PowerDNSのゾーン情報はアプリケーションの初期化フェーズでゾーンファイルから読み込まれるので、ここのTTLを変更します。

diff --git a/webapp/pdns/u.isucon.dev.zone b/webapp/pdns/u.isucon.dev.zone
index bd387a1..e1fa270 100644
--- a/webapp/pdns/u.isucon.dev.zone
+++ b/webapp/pdns/u.isucon.dev.zone
@@ -10,7 +10,7 @@ $TTL 3600
 @        0 IN NS ns1.u.isucon.dev.
 @        0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
 ns1      0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
-pipe     0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
+pipe     60 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
 test001  0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
 
 www              0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>

マニュアルでもDNSの動作としてTTLを設定すれば、適切にキャッシュされるとしていました。

pipe.u.isucon.dev が主にベンチマーカーからアクセスされるドメインであり、これだけTTLを長めに設定すれば十分な効果が得られます。ただ、DNSラウンドロビンなどDNSによる負荷分散を行う場合、この設定をしてしまうと狙った効果が得られなかったかもしれません。

また、PowerDNSの設定ファイル /etc/powerdns/pdns.conf においてもデフォルトから意図的に変更されているものがあり、いくつか変更した方がいい項目があります。

negquery-cache-ttl=0
query-cache-ttl=0

あたりは設定しておいても良さそうです。PowerDNSのパフォーマンスチューニングについては以下のドキュメントが参考になります。自分もよくみました。

doc.powerdns.com

PowerDNSの設定などはデフォルトであるべきものがなかったりと、インフラよりのやや謎解き要素になっていたかもしれません。

4. BIND zone file backendとワイルドカード

さくらのクラウドではDNS水責め攻撃の対策として、MySQL backendからBIND zone file backendに切り替えています。

水責め攻撃ではキャッシュは有効に働きませんので、MySQL backendではDNS問い合わせの都度SQLが発行されてしまい、パフォーマンスに大きな影響がでます。一方BIND zone file backendでは、静的な設定ファイルを読み込むことでPowerDNSのメモリ内だけで処理を行うので、レスポンスが高速になることが期待できます。

ただ、BIND zone file backendでは動的にレコードを追加変更することができません。今回の問題ではユーザが増えるたびにDNSのレコードを追加する必要があるので、このままでは使えません(ユーザ追加の都度ゾーンファイルを読み込み直す対応はあったようです)。

そこで、DNSレコードとして、ワイルドカードレコードを追加しておくことで、ユーザ追加ごとにDNSレコード操作の必要をなくすという手が考えられます。

diff --git a/webapp/pdns/u.isucon.dev.zone b/webapp/pdns/u.isucon.dev.zone
index bd387a1..55a2fe4 100644
--- a/webapp/pdns/u.isucon.dev.zone
+++ b/webapp/pdns/u.isucon.dev.zone
@@ -10,8 +10,9 @@ $TTL 3600
 @        0 IN NS ns1.u.isucon.dev.
 @        0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
 ns1      0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
-pipe     0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
+pipe     60 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
 test001  0 IN A  <ISUCON_SUBDOMAIN_ADDRESS>
+*        60 IN A  <ISUCON_SUBDOMAIN_ADDRESS>

ただこれに行うと、DNS水責め攻撃のクエリに対してもIPアドレスが返ってしまい、マニュアルにもあるHTTPSによるスクレイピング(という設定の余計なアクセス)が発生し、アプリケーションが十分に高速化されていないとかえって負荷になるという状態になります。

5. DNSサーバの自作、あるいは意図的な遅延

DNS水責め攻撃はDoS攻撃手法の一種です。大量のクエリを投げつけて相手のサービスに影響を与えることが狙いだとされています。

水責め攻撃への対応としては攻撃を受け付けるだけの容量、パフォーマンスを備えるというのもありますが、検知してレートリミットをかけたり、フィルタリングする対策も当然あります。

ISUCON13の水責め攻撃でも検知しての対策ができます。名前解決が行われるドイメインは username.u.isucon.dev となっており、usernameが存在していなかった場合、それは水責め攻撃のクエリとみなして、遅延をいれるという手があります。

遅延をいれることで、水責め攻撃のスループットを落とすことでき、DNSサーバのみならず、ベンチマーカー側のCPU負荷をも抑えることができます。他の処理に回せるCPUが増え、結果として全体のスコアが上昇する可能性もありました。

遅延を実現する方法として以下のような方法が考えられます。

  • アプリケーションの中にDNSサーバを組み込み、アプリケーション中でユーザの存在確認をし、遅延をいれる
  • PowerDNSのリモートバックエンドを利用する。アプリケーション中にDNS名前解決用のAPIエンドポイントを追加
  • dnsdistをPowerDNSの前に置き、NXDOMAIN(ドメインが見つからない応答)の場合に遅延挿入

DNSproxyサーバであるdnsdistではさまざまな条件でリクエストやレスポンスに処理を加えることができますが、以下のように設定でレスポンスがNXDOMAINであった場合に遅延挿入が実現できます。dnsdistを起動する前にPowerDNSは1053ポートで動くように変更しておきます。

addLocal("0.0.0.0:53", {reusePort=true,tcpListenQueueSize=4096})
newServer({address="127.0.0.1:1053",useClientSubnet=true,name="backend1"})
addACL("0.0.0.0/0")
addACL("::0/0")

addResponseAction(
  RCodeRule(DNSRCode.NXDOMAIN),
  DelayResponseAction(1000)
)

DNSの遅延をいれることで、DNS水責めの回転数は圧倒的に落ち、名前解決数も減ります。もし、名前解決で使用するCPUがボトルネックになっている場合はスコアが上がるかもしれません。

さくらインターネットの企業賞の条件は、「DNS名前解決数の最も多かったチーム」であり名前解決のパフォーマンスも出しながら、スコアのアップも目指したチームへの賞とさせていただいています。

まとめ

ISUCON13のDNSに関わる要素とベンチマーカーから行われたDNS水責めについて紹介しました。今ではDNSサーバを運用する機会はほぼなく、今後ISUCONでDNSが題材にあがることはないかもしれませんが、DNSはインターネットの大事なコンポーネントであり、参加者の皆様の今後の開発のなんらかの参考になれば幸いです。