権威DNSサービスへのDDoSと ハイパフォーマンスなベンチマーカ YAPC::Kyoto 2023 at Kyoto Research Park 2023/03/19
権威DNSサービスへのDDoSと ハイパフォーマンスなベンチマーカYAPC::Kyoto 2023 at Kyoto Research Park 2023/03/19さくらインターネット株式会社 クラウド事業本部 SRE室 Masahiro Nagano (kazeburo)
View Slide
Me• ⻑野雅広(ながのまさひろ)• CPAN: KAZEBURO• Twitter/GitHub @kazeburo• 2006年まで京都リサーチパーク4号館で勤務• さくらインターネット株式会社 クラウド事業本部 SRE室 室⻑• ISUCON1, 2, 9予選出題。ISUCON3, 4優勝
さくらインターネット SRE室の取り組み• ミッション• クラウドサービスの信頼性を⾼めることにより、お客様や社会のDXをしっかり⽀える• ビジョン• 社内でのSREの実践を広め、お客様への価値提供を⾏う• さくらのサービスそのものの信頼性向上、それにより価値向上を⽬指す• さくら社員がEnabling SREとして、お客様・社外のサービスの信頼性向上に携わる
さくらインターネット SRE室の取り組み• SRE as a Service• 社内における Kubernetes 基盤構築• ログ/監視基盤の研究開発• Embedded SRE / Enabling SREとしての取り組み • クラウドサービスのチーム開発/運⽤体制作り• CI/CDなどDX(Develoer Experience)向上の仕組みの構築• 既存クラウドサービスの開発運⽤
DNS⽔責め攻撃ランダムサブドメイン
「DNS⽔責め攻撃」とは• 攻撃対象に⼤量のランダムなサブドメイン(foo-bar-baz.example.com)を問い合わせてDNSの機能停⽌、機能低下を狙う攻撃• 攻撃者はオープンリゾルバ(DNSキャッシュサーバ)に対して、⼤量のランダムサブドメインの問い合わせを発⽣させる• DNSキャッシュサーバにはキャッシュがなく、権威DNSサーバに⼤量の問い合わせが発⽣し、DDoSとなる
「DNS⽔責め攻撃」とは• ランダムサブドメイン攻撃 (Pseudo-Random Subdomain Attack) と呼ばれることも• 2014年に初めて観測 (https://cybersecurity-jp.com/column/34745)• DNSによる名前解決として正しい動作、通信パケットであり防ぐのが難しい• GoogleやCloudflareのPublic DNSが利⽤されるため、単純なブロックができない
実際の攻撃の記録(1分間あたりのクエリ数)12/15から12/16まで1⽇近く、900万クエリ/分(15万qps)の攻撃が続いた
実際の攻撃(tcpdump)07:25:11.719035 IP 209.216.160.2.50051 > 133.242.64.100.53: 43104 A? meetmodeling.example.com. (50)07:25:11.719057 IP 205.171.30.238.44916 > 133.242.64.100.53: 64321% [1au] A? _.modeling.example.com. (71)07:25:11.719069 IP 172.70.109.31.63292 > 133.242.64.100.53: 40380 [1au] A? osaExpe1-pLatINUM.exAmpLe.cOm. (66)07:25:11.719071 IP 3.139.136.204.44597 > 133.242.64.100.53: 32383% [1au] A? webdirect.foster.example.com. (65)07:25:11.719113 IP 18.188.77.103.42513 > 133.242.64.100.53: 14853 [1au] A? note-modeling.example.com. (62)07:25:11.719132 IP 172.70.33.19.27971 > 133.242.64.100.53: 35379 [1au] A? indian-awarded.example.com. (63)07:25:11.719147 IP 12.121.89.48.43564 > 133.242.64.100.53: 23891 A? matchfiling.example.com. (49)07:25:11.719156 IP 74.125.181.130.64517 > 133.242.64.100.53: 25285% [1au] A? xmL.mODeLING.eXaMple.CoM. (61)07:25:11.719166 IP 165.225.41.202.17203 > 133.242.64.100.53: 53044% [1au] A? qatawarded.example.com. (59)07:25:11.719176 IP 96.114.53.67.20082 > 133.242.64.100.53: 41999 [1au] A? netherlands.filing.example.com. (67)07:25:11.719190 IP 172.253.195.196.35276 > 133.242.64.100.53: 45639% [1au] A? tdd-modeling.example.com. (61)07:25:11.719195 IP 172.253.217.12.40587 > 133.242.64.100.53: 62658% [1au] A? web.modeling.example.com. (61)07:25:11.719197 IP 172.253.9.4.50295 > 133.242.64.100.53: 37961% [1au] A? co.awarded.example.com. (59)07:25:11.719224 IP 172.71.29.39.30489 > 133.242.64.100.53: 2496 [1au] A? SfaaSobvioUs.ExamplE.Com. (61)07:25:11.719235 IP 209.66.107.33.57264 > 133.242.64.100.53: 50511 [1au] A? hap.modeling.example.com. (61)07:25:11.719275 IP 96.114.53.69.53157 > 133.242.64.100.53: 5679 [1au] A? gitcn-awarded.example.com. (62)07:25:11.719312 IP 172.70.229.30.59530 > 133.242.64.100.53: 45890 [1au] A? ipafoster.example.com. (58)07:25:11.719336 IP 172.217.46.78.59507 > 133.242.64.100.53: 60186% [1au] A? testcloud-modeling.example.com. (67)07:25:11.719351 IP 69.47.193.166.52891 > 133.242.64.100.53: 238 [1au] A? bfmpassing.example.com. (59)07:25:11.719353 IP 34.218.119.91.26001 > 133.242.64.100.53: 31511% [1au] A? signal-modeling.example.com. (64)07:25:11.719365 IP 34.218.119.91.13381 > 133.242.64.100.53: 4210% [1au] A? pairfiling.example.com. (59)
実際の攻撃(tcpdump)07:25:11.719035 IP 209.216.160.2.50051 > 133.242.64.100.53: 43104 A? meetmodeling.example.com. (50)07:25:11.719057 IP 205.171.30.238.44916 > 133.242.64.100.53: 64321% [1au] A? _.modeling.example.com. (71)07:25:11.719069 IP 172.70.109.31.63292 > 133.242.64.100.53: 40380 [1au] A? osaExpe1-pLatINUM.exAmpLe.cOm. (66)07:25:11.719071 IP 3.139.136.204.44597 > 133.242.64.100.53: 32383% [1au] A? webdirect.foster.example.com. (65)07:25:11.719113 IP 18.188.77.103.42513 > 133.242.64.100.53: 14853 [1au] A? note-modeling.example.com. (62)07:25:11.719132 IP 172.70.33.19.27971 > 133.242.64.100.53: 35379 [1au] A? indian-awarded.example.com. (63)07:25:11.719147 IP 12.121.89.48.43564 > 133.242.64.100.53: 23891 A? matchfiling.example.com. (49)07:25:11.719156 IP 74.125.181.130.64517 > 133.242.64.100.53: 25285% [1au] A? xmL.mODeLING.eXaMple.CoM. (61)07:25:11.719166 IP 165.225.41.202.17203 > 133.242.64.100.53: 53044% [1au] A? qatawarded.example.com. (59)07:25:11.719176 IP 96.114.53.67.20082 > 133.242.64.100.53: 41999 [1au] A? netherlands.filing.example.com. (67)07:25:11.719190 IP 172.253.195.196.35276 > 133.242.64.100.53: 45639% [1au] A? tdd-modeling.example.com. (61)07:25:11.719195 IP 172.253.217.12.40587 > 133.242.64.100.53: 62658% [1au] A? web.modeling.example.com. (61)07:25:11.719197 IP 172.253.9.4.50295 > 133.242.64.100.53: 37961% [1au] A? co.awarded.example.com. (59)07:25:11.719224 IP 172.71.29.39.30489 > 133.242.64.100.53: 2496 [1au] A? SfaaSobvioUs.ExamplE.Com. (61)07:25:11.719235 IP 209.66.107.33.57264 > 133.242.64.100.53: 50511 [1au] A? hap.modeling.example.com. (61)07:25:11.719275 IP 96.114.53.69.53157 > 133.242.64.100.53: 5679 [1au] A? gitcn-awarded.example.com. (62)07:25:11.719312 IP 172.70.229.30.59530 > 133.242.64.100.53: 45890 [1au] A? ipafoster.example.com. (58)07:25:11.719336 IP 172.217.46.78.59507 > 133.242.64.100.53: 60186% [1au] A? testcloud-modeling.example.com. (67)07:25:11.719351 IP 69.47.193.166.52891 > 133.242.64.100.53: 238 [1au] A? bfmpassing.example.com. (59)07:25:11.719353 IP 34.218.119.91.26001 > 133.242.64.100.53: 31511% [1au] A? signal-modeling.example.com. (64)07:25:11.719365 IP 34.218.119.91.13381 > 133.242.64.100.53: 4210% [1au] A? pairfiling.example.com. (59)• ランダムな⽂字列、単語の組み合わせ• ⼤⽂字・⼩⽂字まざり(Google Public DNS仕様)• ラベル数が増えることも
なぜ「⽔責め攻撃」が有効か• キャッシュに存在しない⼤量のクエリ• DNSサーバ側でのDNSリクエストのパケット処理が必要となる• さくらのクラウド DNSアプライアンスではお客様のDNSレコードを管理しやすくするため PowerDNSおよびRDBMS(MySQL/MariaDB) をDNSサーバのバックエンドとして利⽤• DNSサーバが持つキャッシュにヒットしないため都度SQLが発⾏• CPU負荷によりサービスに影響
「⽔責め攻撃」への対策• 存在するレコードへのクエリ以外はDNSサーバの⼿前でフィルタする• dnsdist(https://dnsdist.org/) というDNSのProxyサーバを導⼊ ✅• 新たなドメインに対する⽔責め攻撃の対策にはならず• 攻撃を受け切るパフォーマンス💪• スケールアウト/スケールアップ/負荷分散/チューニング• RDBMS を使うバックエンドをやめる🌸
DNS⽔責め攻撃に対するDNSサーバのパフォーマンスを測る
(権威)DNSサーバのベンチマークツール• dnsperf• https://github.com/DNS-OARC/dnsperf• あらかじめ問い合わせするクエリをファイルに記述しベンチマークを⾏う• ⾃作する• ランダム⽂字列の⼤⽂字⼩⽂字、⻑さや数を柔軟に変更したい• ベンチマーカから対象まで全体を把握できる。DNSについて学べる
作りました
3/10に実⾏
package mainimport ("fmt""math/rand""net""time")const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"func randString(n int) string {rand.Seed(time.Now().UnixNano())b := make([]byte, n)for i := range b {b[i] = letterBytes[rand.Intn(len(letterBytes))]}return string(b)}func main() {for i := 0; i < 1000; i++ {randStr := randString(10) // 10จࣈͷϥϯμϜจࣈྻΛੜaddrs, err := net.LookupHost(randStr)if err != nil {fmt.Printf("Failed to resolve %s: %v\n", randStr, err)} else {fmt.Printf("%s resolves to %v\n", randStr, addrs)}}} 3/10に実⾏
package mainimport ("fmt""math/rand""net""time")const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"func randString(n int) string {rand.Seed(time.Now().UnixNano())b := make([]byte, n)for i := range b {b[i] = letterBytes[rand.Intn(len(letterBytes))]}return string(b)}func main() {for i := 0; i < 1000; i++ {randStr := randString(10) // 10จࣈͷϥϯμϜจࣈྻΛੜaddrs, err := net.LookupHost(randStr)if err != nil {fmt.Printf("Failed to resolve %s: %v\n", randStr, err)} else {fmt.Printf("%s resolves to %v\n", randStr, addrs)}}}ちゃんと動きそう..?
コードの問題点を中の⼈に確認してみた• このコードを利⽤してDNSサーバの性能評価を⾏う場合、以下の問題点が考えられます。1. このコードは、⼀つのプログラムから直列でDNSリクエストを送信するため、実際のユーザーからのリクエストとは異なる負荷がかかる可能性があります。実際のユーザーからのリクエストは、同時に多数のリクエストが発⽣することがありますが、このコードでは⼀つずつリクエストが発⽣するため、DNSサーバの性能評価としては適切ではありません。2.このコードは、名前解決の際にエラーが発⽣した場合に、そのエラーを表⽰するだけで処理を続⾏します。しかし、実際のユーザーからのリクエストでは、エラーが発⽣した場合には適切にエラー処理を⾏う必要があります。3/16に実⾏(⼀部抜粋)
ベンチマーカの必要条件• 多数のリクエストを⾏うパフォーマンス 💪• 適切なエラーが取得可能であること🔍
適切なエラー処理• どこでエラーが起きたのか明らかにする• ベンチマークはテストでもある• 意図した失敗を許容できる• DNSで起きるエラー• Timeout (Drop含む)• NXDOMAIN: ドメインが⾒つからない• SERVFAIL: エラーの発⽣• REFUSED: 拒否
エラー処理(net.Resolver)func do() {r := &net.Resolver{PreferGo: true,Dial: func(ctx context.Context, network, address string) (net.Conn, error) {d := net.Dialer{Timeout: timeout,}return d.DialContext(ctx, protocol, net.JoinHostPort(host, port))},}sub := randString(10)_, err := r.LookupHost(context.Background(), sub+"."+zone)if err := err.(*net.DNSError); err.IsNotFound {}}エラーは取得できるが、詳しい内容はエラーの⽂⾔を解析しないとわからない(LookupHostはAと同時にAAAAの名前解決を⾏う)
エラー処理(miekg/dnsの)import "github.com/miekg/dns"atype = dns.StringToType["A"]func do() {c := &dns.Client{Net: protocol, Timeout: timeout}address := net.JoinHostPort(host, port)m := new(dns.Msg)sub := randString(10)m.SetQuestion(sub+”.”+zone+”.", atype)r, _, err := c.Exchange(m, address)if err != nil {}if r.Rcode == dns.RcodeRefused {}}Pure GoなシンプルなAPIとコードDNSのステータス(RCODE)が取得可能
パフォーマンス💪• 対象のパフォーマンス > ベンチマーカのパフォーマンスでは正しい計測ができない• 現代において Apache Bench が常に適切なツールとはならない• クライアント側にもハイパフォーマンスが求められる• ベンチマークにとって余分な処理の削減(Happy EyeBallsなど)• 並⾏・並列化• パフォーマンスチューニング
並⾏・並列化Use Goroutinesctx, cancel := context.WithTimeout(context.Background(),timeLimit,)defer cancel()for w := 0; w < maxWorkers; w++ {go func() {for {do()}}()}<- ctx.Done()import "github.com/miekg/dns"atype = dns.StringToType["A"]func do() {c := &dns.Client{Net: protocol, Timeout: timeout}address := net.JoinHostPort(host, port)m := new(dns.Msg)sub := randString(10)m.SetQuestion(sub+”.”+zone+”.", atype)r, _, err := c.Exchange(m, address)if err != nil {}if r.Rcode == dns.RcodeRefused {}}goroutineを起動し、名前解決をループで実⾏
Goのパフォーマンスチューニング• メモリーアロケーションしない• 計測をしましょう• 最後のGOGC
メモリーアロケーションを減らすimport "github.com/miekg/dns"ctx, cancel := context.WithTimeout(context.Background(),timeLimit,)defer cancel()for w := 0; w < maxWorkers; w++ {go func() {c := &dns.Client{Net: protocol,Timeout: timeout}m := new(dns.Msg)address := net.JoinHostPort(host, port)for {do(c, m, address)}}()}<- ctx.Done()var atype = dns.StringToType[“A"]func do(c *dns.Client,m *dns.Msg,address string) {sub := randString(10)m.SetQuestion(sub+”.”+zone+”.", atype)r, _, err := c.Exchange(m, address)if err != nil {}if r.Rcode == dns.RcodeRefused {}}ループごとに初期化する必要ない。ループの外へ移動する
⽂字列連結の⾼速化• Go⾔語では⽂字列はImmutable• ⽂字列の連結は新しい⽂字列をアロケーションするzone := randString(10)zone += “.”zone += “example.com"zone += “.”m.SetQuestion(zone, atype)
⽂字列連結の⾼速化バッファの再利⽤import (“strings"“github.com/miekg/dns")for w := 0; w < maxWorkers; w++ {go func() {c := &dns.Client{Net: protocol,Timeout: timeout}m := new(dns.Msg)address := net.JoinHostPort(host, port)bb := new(bytes.Buffer)for {do(c, m, address, bb)}}()}<- ctx.Done()var atype = dns.StringToType[“A"]func do(c *dns.Client,m *dns.Msg,address stringbb *bytes.Buffer) {defer sb.Reset()randString(bb, 10)bb.WriteByte('.')bb.WriteString(opt.Zone)bb.WriteByte('.')b := sb.Byte()z := unsafe.String(&b[0], len(b))m.SetQuestion(z, atype)r, _, err := c.Exchange(m, address)}ループの外でbufferを作成しループの中でresetして使い回す
ランダム⽂字列の⽣成// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-goconst (letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789"letterIdxBits = 6 // 6 bits to represent a letter indexletterIdxMask = 1<letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits)func randString(bb *bytes.Buffer, n int) {for i, cache, remain := n-1, int63(), letterIdxMax; i >= 0; {if remain == 0 {cache, remain = int63(), letterIdxMax}if idx := int(cache & letterIdxMask); idx < len(letterBytes) {bb.WriteByte(letterBytes[idx])i--}cache >>= letterIdxBitsremain--}}新たな⽂字列を⽣成することなくランダムな⽂字を追記していく
チューニングの結果の計測% go test -bench . -benchmem -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/kazeburo/benchcpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHzBenchmarkDo-8 2798367 1269 ns/op 28 B/op 3 allocs/opBenchmarkDoMoto-8 324367 10319 ns/op 224 B/op 8 allocs/opPASSok github.com/kazeburo/bench 8.465s9倍程度⾼速化(dns.SetQuestionまで)1回あたりのアロケーション回数(allocs/op)1回あたりのアロケーションで確保した容量(B/op)実⾏した回数 1回あたりの実⾏に掛かった時間(ns/op)
プロファイリングを⾏うgo test -cpuprofile cpu.prof -memprofile mem.prof -benchtime 3s -bench 'BenchmarkDo$'goos: darwingoarch: amd64pkg: github.com/kazeburo/benchcpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHzBenchmarkDo-8 2401633 1478 ns/opPASSok github.com/kazeburo/bench 5.362s
プロファイリング% go tool pprof -top mem.profType: alloc_spaceTime: Mar 13, 2023 at 4:08pm (JST)Showing nodes accounting for 99.73MB, 100% of 99.73MB totalflat flat% sum% cum cum%87MB 87.24% 87.24% 98MB 98.27% github.com/miekg/dns.(*Msg).SetQuestion10.50MB 10.53% 97.77% 11MB 11.03% github.com/miekg/dns.id1.16MB 1.16% 98.93% 1.16MB 1.16% runtime/pprof.StartCPUProfile0.57MB 0.57% 99.50% 0.57MB 0.57% compress/flate.newDeflateFast (inline)0.50MB 0.5% 100% 0.50MB 0.5% encoding/binary.Read0 0% 100% 0.57MB 0.57% compress/flate.(*compressor).init0 0% 100% 0.57MB 0.57% compress/flate.NewWriter0 0% 100% 0.57MB 0.57% compress/gzip.(*Writer).Write0 0% 100% 98MB 98.27% github.com/kazeburo/bench.BenchmarkDo0 0% 100% 98MB 98.27% github.com/kazeburo/bench.do0 0% 100% 1.16MB 1.16% main.main0 0% 100% 1.16MB 1.16% runtime.main…このあたりがアヤシイ
dns.SetQuestion Hack• 問い合わせを格納している配列を都度⽣成している• 配列を使い回し、問い合わせ「名」のみ差し替え• uint16なランダムIdの⽣成• ベンチマークなので uint16(atomic.AddUint64()) に変更
チューニングの確認% go test -bench . -benchmem -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/kazeburo/benchcpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHzBenchmarkDo-8 24056054 138.3 ns/op 0 B/op 0 allocs/opBenchmarkDoMoto-8 306056 10851 ns/op 224 B/op 8 allocs/opPASSok github.com/kazeburo/bench 7.234sアロケーションは0に、当初の80倍程度⾼速化1回あたりのアロケーション回数(allocs/op)1回あたりのアロケーションで確保した容量(B/op)実⾏した回数 1回あたりの実⾏に掛かった時間(ns/op)
GOGC• GoでGCを実⾏するヒープサイズの⽬標となるパラメータ• デフォルト 100 で⼤きくすることでGCの頻度を減らしCPUコストを下げる(メモリは増加する)# ./prsd-bench -P 53 -H 192.168.10.50 --time-duration 32s --max-workers 300 --max-length 8 --label 1 --zone x.y2023-03-11 18:18:56.526777432 +0900 JST m=+20.001327490 resolved: 0.000000 query/sec,refused 157012.900000 query/sec, failed 0.000000 query/sec# GOGC=500 ./prsd-bench -P 53 -H 192.168.10.50 --time-duration 32s --max-workers 300 --max-length 8 --label 1 --zone x.y2023-03-11 18:19:40.175067406 +0900 JST m=+20.001772051 resolved: 0.000000 query/sec,refused 178251.500000 query/sec, failed 0.000000 query/sec 最後の⼀押として使う
まとめ• ⽔責め攻撃はやっかい• ベンチマーカは適切な情報(エラー)が取得可能であること• ベンチマークはテストでもある• ベンチマーク対象に負けないパフォーマンス 💪• Go⾔語で作る上でのパフォーマンスチューニング
SAKURA internetࣾձΛࢧ͑Δ ύϒϦοΫΫϥυΛ Ұॹʹ࡞Γ·ͤΜ͔ʁPerl, Go, Pythonインフラ基盤からフロントエンドまで採⽤強化中!さくらインターネットではエンジニア採⽤を強化していますさくらインターネットは新たなアイデアの創出に強い熱意と情熱を持って挑戦するお客様をはじめ、私たちとつながりのあるすべての⼈たちのために、未来のあるべき姿を想い描きながら ―「やりたいこと」を「できる」に変える ― あらゆるアプローチを “インターネット”を通じて提供します。詳しくはWebサイトにて、カジュアル⾯談もやってます 👉 www.sakura.ad.jp/lp/22engineer/
ご清聴ありがとうございましたこのあとはさくらインターネットのブースにおります。質問などありましたらお気軽に~