[インデックス 17193] ファイルの概要
このコミットは、Go言語のnet
パッケージにおけるDNSクライアントの実装に関するものです。具体的には、UDPによるDNS応答が切り詰められた(truncated)場合に、TCPにフォールバックして再試行するメカニズムを導入しています。これにより、DNSSEC署名付き応答や大量のレコードを含む応答など、UDPのメッセージサイズ制限を超えるDNS応答の信頼性が向上します。
コミット
commit 0a3cb7ece36e4d41cd6bca558c7bff7925240435
Author: Alex A Skinner <alex@lx.lc>
Date: Tue Aug 13 09:44:12 2013 -0700
net: implement DNS TCP fallback query if UDP response is truncated
Fixes #5686.
R=golang-dev, bradfitz, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/12458043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0a3cb7ece36e4d41cd6bca558c7bff7925240435
元コミット内容
net: implement DNS TCP fallback query if UDP response is truncated
Fixes #5686.
R=golang-dev, bradfitz, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/12458043
変更の背景
DNS (Domain Name System) は、インターネット上のドメイン名をIPアドレスに変換するための分散型データベースシステムです。通常、DNSクエリはUDP (User Datagram Protocol) を使用して行われます。UDPはコネクションレスでオーバーヘッドが少ないため、高速なクエリに適しています。しかし、UDPにはメッセージサイズに制限があります。一般的なUDPパケットの最大サイズは512バイトとされており、これを超えるDNS応答は「切り詰められた(truncated)」としてマークされます。
DNS応答が切り詰められる主なシナリオは以下の通りです。
- DNSSEC (DNS Security Extensions): DNSSECはDNS応答の認証と完全性を保証するための拡張機能であり、署名データを含むため応答サイズが大きくなる傾向があります。
- 多数のレコード: 特定のドメイン名に対して非常に多くのリソースレコード(例: 多数のAレコード、MXレコードなど)が存在する場合、応答が512バイトを超えることがあります。
- EDNS0 (Extension Mechanisms for DNS): EDNS0はUDPのメッセージサイズ制限を緩和するために導入されましたが、すべてのDNSサーバーやクライアントがEDNS0を適切にサポートしているわけではありません。
DNSプロトコル(RFC 1035)では、UDP応答が切り詰められた場合(応答ヘッダのTCビットがセットされている場合)、クライアントは同じクエリをTCP (Transmission Control Protocol) を使用して再試行することが推奨されています。TCPはコネクション指向であり、信頼性の高いデータ転送を提供するため、UDPのサイズ制限に縛られません。
このコミット以前のGoのnet
パッケージのDNSクライアントは、UDP応答が切り詰められた場合にTCPへのフォールバックを自動的に行っていませんでした。そのため、切り詰められた応答を受け取ると、クエリが失敗したり、不完全な情報しか得られない可能性がありました。Issue #5686は、この問題、特にDNSSECが有効な環境での信頼性の低いDNS解決について報告しており、このコミットはその問題を解決するために導入されました。
前提知識の解説
DNS (Domain Name System)
ドメイン名をIPアドレスに変換するシステム。インターネットの電話帳のような役割を果たします。
UDP (User Datagram Protocol)
コネクションレス型のプロトコルで、データの信頼性よりも速度を重視します。DNSクエリの多くで利用されます。UDPパケットにはサイズ制限があり、通常は512バイトが推奨されます。
TCP (Transmission Control Protocol)
コネクション指向型のプロトコルで、データの信頼性と順序性を保証します。UDPよりもオーバーヘッドは大きいですが、大量のデータを確実に転送できます。DNSでは、ゾーン転送やUDP応答が切り詰められた場合のフォールバックとして利用されます。
DNSメッセージの構造
DNSメッセージはヘッダと質問、応答、権威、追加の各セクションで構成されます。ヘッダには、メッセージのタイプ(クエリか応答か)、応答コード、そして重要なフラグが含まれます。
TC (Truncation) ビット
DNS応答ヘッダのフラグの一つで、応答が切り詰められた場合にセットされます。これは、応答がUDPのメッセージサイズ制限を超えたため、完全な情報を提供できなかったことを示します。
RFC 5966 (DNS Message Size Issues)
このRFCは、DNSメッセージのサイズに関する問題と、それに対する推奨される解決策について議論しています。特に、UDP応答が切り詰められた場合のTCPフォールバックの重要性を強調しています。
Go言語のnet
パッケージ
Go言語の標準ライブラリの一部で、ネットワークプログラミングのための基本的な機能を提供します。TCP/UDPソケット、DNSルックアップなどが含まれます。
io.ReadFull
Goのio
パッケージにある関数で、指定されたバイト数だけ正確に読み込むことを保証します。指定されたバイト数に満たない場合、エラーを返します。TCPベースのプロトコルで、メッセージの長さを事前に知っている場合に便利です。
技術的詳細
このコミットの主要な変更点は、src/pkg/net/dnsclient_unix.go
内のtryOneName
関数にTCPフォールバックロジックを追加したことです。
-
exchange
関数の変更:exchange
関数は、DNSクエリを送信し、応答を受信する役割を担います。- この関数に
useTCP
という新しいブール変数が導入されました。これは、現在使用しているコネクションがTCPベースであるかどうかを判断するために使用されます。 - TCPコネクションの場合、DNSメッセージの先頭に2バイトの長さフィールド(ネットワークバイトオーダー)を追加する処理が追加されました。これは、TCP上でDNSメッセージを送信する際の標準的な方法です(RFC 1035の4.2.2節)。
- 応答の読み込み部分も変更されました。TCPコネクションの場合、まず
io.ReadFull
を使用して2バイトの長さフィールドを読み込み、その長さに従って残りのメッセージを読み込むように修正されました。これにより、TCPストリームから完全なDNSメッセージを正確に読み取ることができます。
-
tryOneName
関数の変更:tryOneName
関数は、複数のDNSサーバーに対してクエリを試行するロジックを含んでいます。- UDPによる
exchange
呼び出しの後、受信したDNSメッセージのtruncated
フラグ(msg.truncated
)がチェックされます。 - もし
truncated
フラグがtrue
であれば、RFC 5966の推奨に従い、同じDNSサーバーに対してTCPコネクションを確立し、再度exchange
関数を呼び出してクエリを再試行します。 - TCPでの再試行が成功した場合、その応答が使用されます。失敗した場合は、エラーが伝播されます。
-
テストの追加:
src/pkg/net/dnsclient_unix_test.go
という新しいテストファイルが追加されました。TestTCPLookup
というテスト関数が導入され、TCP経由でのDNSルックアップが正しく機能するかどうかを検証します。このテストは、Google Public DNS (8.8.8.8:53) に対してTCPで接続し、com.
ドメインのdnsTypeALL
クエリを実行することで、TCPベースのDNS通信が正しく行われることを確認します。
この変更により、GoのDNSクライアントは、UDPのメッセージサイズ制限によって引き起こされる問題を自動的に検出し、TCPにフォールバックすることで、より堅牢なDNS解決を提供できるようになりました。
コアとなるコードの変更箇所
src/pkg/net/dnsclient_unix.go
--- a/src/pkg/net/dnsclient_unix.go
+++ b/src/pkg/net/dnsclient_unix.go
@@ -17,6 +17,7 @@
package net
import (
+ "io"
"math/rand"
"sync"
"time"
@@ -25,6 +26,13 @@ import (
// Send a request on the connection and hope for a reply.
// Up to cfg.attempts attempts.
func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error) {
+ var useTCP bool
+ switch c.(type) {
+ case *UDPConn:
+ useTCP = false
+ case *TCPConn:
+ useTCP = true
+ }
if len(name) >= 256 {
return nil, &DNSError{Err: "name too long", Name: name}
}
@@ -38,7 +46,10 @@ func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error
if !ok {
return nil, &DNSError{Err: "internal error - cannot pack message", Name: name}
}
-
+ if useTCP {
+ mlen := uint16(len(msg))
+ msg = append([]byte{byte(mlen >> 8), byte(mlen)}, msg...)
+ }
for attempt := 0; attempt < cfg.attempts; attempt++ {
n, err := c.Write(msg)
if err != nil {
@@ -50,9 +61,19 @@ func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error
} else {
c.SetReadDeadline(time.Now().Add(time.Duration(cfg.timeout) * time.Second))
}
-
- buf := make([]byte, 2000) // More than enough.
- n, err = c.Read(buf)
+ buf := make([]byte, 2000)
+ if useTCP {
+ n, err = io.ReadFull(c, buf[:2])
+ if err != nil {
+ if e, ok := err.(Error); ok && e.Timeout() {
+ continue
+ }
+ }
+ buf = make([]byte, uint16(buf[0])<<8+uint16(buf[1]))
+ n, err = io.ReadFull(c, buf)
+ } else {
+ n, err = c.Read(buf)
+ }
if err != nil {
if e, ok := err.(Error); ok && e.Timeout() {
continue
@@ -98,6 +119,19 @@ func tryOneName(cfg *dnsConfig, name string, qtype uint16) (cname string, addrs
err = merr
continue
}
+ if msg.truncated { // see RFC 5966
+ c, cerr = Dial("tcp", server)
+ if cerr != nil {
+ err = cerr
+ continue
+ }
+ msg, merr = exchange(cfg, c, name, qtype)
+ c.Close()
+ if merr != nil {
+ err = merr
+ continue
+ }
+ }
cname, addrs, err = answer(name, server, msg, qtype)
if err == nil || err.(*DNSError).Err == noSuchHost {
break
src/pkg/net/dnsclient_unix_test.go
(新規ファイル)
--- /dev/null
+++ b/src/pkg/net/dnsclient_unix_test.go
@@ -0,0 +1,29 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package net
+
+import (
+ "runtime"
+ "testing"
+)
+
+func TestTCPLookup(t *testing.T) {
+ if runtime.GOOS == "windows" || runtime.GOOS == "plan9" {
+ t.Skip("skipping unix dns test")
+ }
+ if testing.Short() || !*testExternal {
+ t.Skip("skipping test to avoid external network")
+ }
+ c, err := Dial("tcp", "8.8.8.8:53")
+ defer c.Close()
+ if err != nil {
+ t.Fatalf("Dial failed: %v", err)
+ }
+ cfg := &dnsConfig{timeout: 10, attempts: 3}
+ _, err = exchange(cfg, c, "com.", dnsTypeALL)
+ if err != nil {
+ t.Fatalf("exchange failed: %v", err)
+ }
+}
コアとなるコードの解説
src/pkg/net/dnsclient_unix.go
-
import "io"
の追加:- TCPストリームから正確なバイト数を読み込むために、
io.ReadFull
関数を使用するため、io
パッケージがインポートされました。
- TCPストリームから正確なバイト数を読み込むために、
-
exchange
関数の変更:useTCP
変数の導入:exchange
関数に渡されるConn
インターフェースの実体が*UDPConn
か*TCPConn
かをswitch
文で判定し、useTCP
フラグを設定します。これにより、UDPとTCPで異なる処理を適用できるようになります。- TCPメッセージ長のプレフィックス追加:
if useTCP
ブロック内で、DNSメッセージのバイト列msg
の先頭に、メッセージ全体の長さを示す2バイトのプレフィックスが追加されます。mlen := uint16(len(msg))
でメッセージ長を取得し、append([]byte{byte(mlen >> 8), byte(mlen)}, msg...)
でバイト列の先頭に挿入しています。これは、TCP上でDNSメッセージを送信する際の標準的なフォーマットです。 - TCP応答の読み込みロジックの変更:
if useTCP
ブロック内で、TCPコネクションからの応答読み込み方法が変更されました。- まず
io.ReadFull(c, buf[:2])
で最初の2バイトを読み込みます。この2バイトがDNSメッセージの全長を示します。 - 次に、
buf = make([]byte, uint16(buf[0])<<8+uint16(buf[1]))
で、読み込んだ2バイトからメッセージの実際の長さを計算し、その長さの新しいバッファを作成します。 - 最後に
io.ReadFull(c, buf)
で、計算された長さのメッセージ全体を読み込みます。これにより、TCPストリームから完全なDNS応答を確実に取得できます。
- まず
-
tryOneName
関数の変更:if msg.truncated
ブロックの追加: UDPによるexchange
呼び出しから返されたdnsMsg
オブジェクトのtruncated
フィールドがtrue
(つまり、応答が切り詰められた)であるかをチェックします。- TCPフォールバック処理:
c, cerr = Dial("tcp", server)
: 現在のDNSサーバーに対してTCPコネクションを新しく確立します。msg, merr = exchange(cfg, c, name, qtype)
: 新しく確立したTCPコネクションを使用して、同じDNSクエリを再試行します。c.Close()
: TCPコネクションは使い捨てであるため、応答を受信したらすぐに閉じます。- エラーハンドリング: TCPでの再試行中にエラーが発生した場合も適切に処理し、
err
変数に格納してループを続行します。
src/pkg/net/dnsclient_unix_test.go
TestTCPLookup
関数の追加:- このテストは、GoのDNSクライアントがTCP経由でDNSクエリを正しく実行できることを検証します。
runtime.GOOS
のチェックにより、WindowsやPlan 9などの一部のOSではテストをスキップします。これは、このテストがUnix系のDNSクライアント実装に特化しているためです。testing.Short()
と*testExternal
のチェックにより、短時間テストや外部ネットワークへのアクセスを避ける設定の場合もスキップされます。これは、このテストが外部のDNSサーバー(Google Public DNS)に実際に接続するためです。Dial("tcp", "8.8.8.8:53")
でGoogle Public DNSサーバーにTCP接続を試みます。exchange(cfg, c, "com.", dnsTypeALL)
を呼び出し、TCPコネクション経由でcom.
ドメインのすべてのレコードタイプ(dnsTypeALL
)を問い合わせます。- エラーが発生した場合、
t.Fatalf
でテストを失敗させます。
これらの変更により、Goのnet
パッケージは、DNSのUDPメッセージ切り詰め問題に対して、標準的なTCPフォールバックメカニズムを実装し、より堅牢で信頼性の高いDNS解決機能を提供できるようになりました。
関連リンク
- Go Issue #5686: net: implement DNS TCP fallback query if UDP response is truncated
- Gerrit Change-Id: https://golang.org/cl/12458043
- RFC 1035 (Domain Names - Implementation and Specification): 特に4.2.1 (UDP usage) と 4.2.2 (TCP usage) セクションが関連します。
- RFC 5966 (DNS Message Size Issues): UDP応答の切り詰めとTCPフォールバックに関する詳細な議論が含まれています。
参考にした情報源リンク
- 上記の関連リンクに記載されたRFCドキュメントとGoのIssue/Gerrit変更リスト。
- Go言語の
net
パッケージのソースコード。 - DNSプロトコルに関する一般的な知識。
- UDPとTCPの特性に関する一般的なネットワーク知識。
io.ReadFull
関数のGoドキュメント。
[インデックス 17193] ファイルの概要
このコミットは、Go言語のnet
パッケージにおけるDNSクライアントの実装に関するものです。具体的には、UDPによるDNS応答が切り詰められた(truncated)場合に、TCPにフォールバックして再試行するメカニズムを導入しています。これにより、DNSSEC署名付き応答や大量のレコードを含む応答など、UDPのメッセージサイズ制限を超えるDNS応答の信頼性が向上します。
コミット
commit 0a3cb7ece36e4d41cd6bca558c7bff7925240435
Author: Alex A Skinner <alex@lx.lc>
Date: Tue Aug 13 09:44:12 2013 -0700
net: implement DNS TCP fallback query if UDP response is truncated
Fixes #5686.
R=golang-dev, bradfitz, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/12458043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0a3cb7ece36e4d41cd6bca558c7bff7925240435
元コミット内容
net: implement DNS TCP fallback query if UDP response is truncated
Fixes #5686.
R=golang-dev, bradfitz, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/12458043
変更の背景
DNS (Domain Name System) は、インターネット上のドメイン名をIPアドレスに変換するための分散型データベースシステムです。通常、DNSクエリはUDP (User Datagram Protocol) を使用して行われます。UDPはコネクションレスでオーバーヘッドが少ないため、高速なクエリに適しています。しかし、UDPにはメッセージサイズに制限があります。伝統的に、UDPパケットの最大サイズは512バイトとされており、これを超えるDNS応答は「切り詰められた(truncated)」としてマークされます。
DNS応答が切り詰められる主なシナリオは以下の通りです。
- DNSSEC (DNS Security Extensions): DNSSECはDNS応答の認証と完全性を保証するための拡張機能であり、署名データを含むため応答サイズが大きくなる傾向があります。
- 多数のレコード: 特定のドメイン名に対して非常に多くのリソースレコード(例: 多数のAレコード、MXレコードなど)が存在する場合、応答が512バイトを超えることがあります。
- EDNS0 (Extension Mechanisms for DNS): EDNS0はUDPのメッセージサイズ制限を緩和するために導入されましたが、すべてのDNSサーバーやクライアントがEDNS0を適切にサポートしているわけではありません。
DNSプロトコル(RFC 1035)では、UDP応答が切り詰められた場合(応答ヘッダのTCビットがセットされている場合)、クライアントは同じクエリをTCP (Transmission Control Protocol) を使用して再試行することが推奨されています。TCPはコネクション指向であり、信頼性の高いデータ転送を提供するため、UDPのサイズ制限に縛られません。
このコミット以前のGoのnet
パッケージのDNSクライアントは、UDP応答が切り詰められた場合にTCPへのフォールバックを自動的に行っていませんでした。そのため、切り詰められた応答を受け取ると、クエリが失敗したり、不完全な情報しか得られない可能性がありました。Issue #5686は、この問題、特にDNSSECが有効な環境での信頼性の低いDNS解決について報告しており、このコミットはその問題を解決するために導入されました。
RFC 5966「DNS Transport over TCP - Implementation Requirements」は、DNSメッセージサイズの問題に対処するためにTCPを使用する必要性を強調しています。歴史的にDNSはUDPを主に使用し、IPフラグメンテーションを防ぐために512バイトのメッセージサイズ制限がありました。しかし、IPv6アドレスやDNSSECなどの新しいレコードタイプの導入により、DNS応答がこの制限を超えることが頻繁になりました。応答がUDP制限を超えると、サーバーは応答を切り詰め、DNSヘッダに「TC」(Truncation)フラグを設定します。これはクライアントに対し、完全な応答を受け取るためにTCPを使用してクエリを再試行するよう信号を送ります。EDNS0はUDPペイロードサイズを最大64KBまで拡張することを可能にしましたが、大規模なUDPパケットは依然としてIPフラグメンテーションを引き起こす可能性があり、ネットワークの信頼性やファイアウォールポリシーの観点から懸念が残ります。RFC 5966は、特にDNSSECの採用が増加するにつれて、より大きなDNSメッセージを処理し、信頼性の高い通信を確保するために、DNS実装がTCPをサポートすることの重要性を再確認しました。このRFCは後にRFC 7766によって廃止されましたが、DNSメッセージサイズの問題に対するTCPの重要性に関するその原則は依然として関連性があります。
前提知識の解説
DNS (Domain Name System)
ドメイン名をIPアドレスに変換するシステム。インターネットの電話帳のような役割を果たします。
UDP (User Datagram Protocol)
コネクションレス型のプロトコルで、データの信頼性よりも速度を重視します。DNSクエリの多くで利用されます。UDPパケットにはサイズ制限があり、通常は512バイトが推奨されます。
TCP (Transmission Control Protocol)
コネクション指向型のプロトコルで、データの信頼性と順序性を保証します。UDPよりもオーバーヘッドは大きいですが、大量のデータを確実に転送できます。DNSでは、ゾーン転送やUDP応答が切り詰められた場合のフォールバックとして利用されます。
DNSメッセージの構造
DNSメッセージはヘッダと質問、応答、権威、追加の各セクションで構成されます。ヘッダには、メッセージのタイプ(クエリか応答か)、応答コード、そして重要なフラグが含まれます。
TC (Truncation) ビット
DNS応答ヘッダのフラグの一つで、応答が切り詰められた場合にセットされます。これは、応答がUDPのメッセージサイズ制限を超えたため、完全な情報を提供できなかったことを示します。
RFC 5966 (DNS Message Size Issues)
このRFCは、DNSメッセージのサイズに関する問題と、それに対する推奨される解決策について議論しています。特に、UDP応答が切り詰められた場合のTCPフォールバックの重要性を強調しています。
Go言語のnet
パッケージ
Go言語の標準ライブラリの一部で、ネットワークプログラミングのための基本的な機能を提供します。TCP/UDPソケット、DNSルックアップなどが含まれます。
io.ReadFull
Goのio
パッケージにある関数で、指定されたバイト数だけ正確に読み込むことを保証します。指定されたバイト数に満たない場合、エラーを返します。TCPベースのプロトコルで、メッセージの長さを事前に知っている場合に便利です。
技術的詳細
このコミットの主要な変更点は、src/pkg/net/dnsclient_unix.go
内のtryOneName
関数にTCPフォールバックロジックを追加したことです。
-
exchange
関数の変更:exchange
関数は、DNSクエリを送信し、応答を受信する役割を担います。- この関数に
useTCP
という新しいブール変数が導入されました。これは、現在使用しているコネクションがTCPベースであるかどうかを判断するために使用されます。 - TCPコネクションの場合、DNSメッセージの先頭に2バイトの長さフィールド(ネットワークバイトオーダー)を追加する処理が追加されました。これは、TCP上でDNSメッセージを送信する際の標準的な方法です(RFC 1035の4.2.2節)。
- 応答の読み込み部分も変更されました。TCPコネクションの場合、まず
io.ReadFull
を使用して2バイトの長さフィールドを読み込み、その長さに従って残りのメッセージを読み込むように修正されました。これにより、TCPストリームから完全なDNSメッセージを正確に読み取ることができます。
-
tryOneName
関数の変更:tryOneName
関数は、複数のDNSサーバーに対してクエリを試行するロジックを含んでいます。- UDPによる
exchange
呼び出しの後、受信したDNSメッセージのtruncated
フラグ(msg.truncated
)がチェックされます。 - もし
truncated
フラグがtrue
であれば、RFC 5966の推奨に従い、同じDNSサーバーに対してTCPコネクションを確立し、再度exchange
関数を呼び出してクエリを再試行します。 - TCPでの再試行が成功した場合、その応答が使用されます。失敗した場合は、エラーが伝播されます。
-
テストの追加:
src/pkg/net/dnsclient_unix_test.go
という新しいテストファイルが追加されました。TestTCPLookup
というテスト関数が導入され、TCP経由でのDNSルックアップが正しく機能するかどうかを検証します。このテストは、Google Public DNS (8.8.8.8:53) に対してTCPで接続し、com.
ドメインのdnsTypeALL
クエリを実行することで、TCPベースのDNS通信が正しく行われることを確認します。
この変更により、GoのDNSクライアントは、UDPのメッセージサイズ制限によって引き起こされる問題を自動的に検出し、TCPにフォールバックすることで、より堅牢なDNS解決を提供できるようになりました。
コアとなるコードの変更箇所
src/pkg/net/dnsclient_unix.go
--- a/src/pkg/net/dnsclient_unix.go
+++ b/src/pkg/net/dnsclient_unix.go
@@ -17,6 +17,7 @@
package net
import (
+ "io"
"math/rand"
"sync"
"time"
@@ -25,6 +26,13 @@ import (
// Send a request on the connection and hope for a reply.
// Up to cfg.attempts attempts.
func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error) {
+ var useTCP bool
+ switch c.(type) {
+ case *UDPConn:
+ useTCP = false
+ case *TCPConn:
+ useTCP = true
+ }
if len(name) >= 256 {
return nil, &DNSError{Err: "name too long", Name: name}
}
@@ -38,7 +46,10 @@ func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error
if !ok {
return nil, &DNSError{Err: "internal error - cannot pack message", Name: name}
}
-
+ if useTCP {
+ mlen := uint16(len(msg))
+ msg = append([]byte{byte(mlen >> 8), byte(mlen)}, msg...)
+ }
for attempt := 0; attempt < cfg.attempts; attempt++ {
n, err := c.Write(msg)
if err != nil {
@@ -50,9 +61,19 @@ func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error
} else {
c.SetReadDeadline(time.Now().Add(time.Duration(cfg.timeout) * time.Second))
}
-
- buf := make([]byte, 2000) // More than enough.
- n, err = c.Read(buf)
+ buf := make([]byte, 2000)
+ if useTCP {
+ n, err = io.ReadFull(c, buf[:2])
+ if err != nil {
+ if e, ok := err.(Error); ok && e.Timeout() {
+ continue
+ }
+ }
+ buf = make([]byte, uint16(buf[0])<<8+uint16(buf[1]))
+ n, err = io.ReadFull(c, buf)
+ } else {
+ n, err = c.Read(buf)
+ }
if err != nil {
if e, ok := err.(Error); ok && e.Timeout() {
continue
@@ -98,6 +119,19 @@ func tryOneName(cfg *dnsConfig, name string, qtype uint16) (cname string, addrs
err = merr
continue
}
+ if msg.truncated { // see RFC 5966
+ c, cerr = Dial("tcp", server)
+ if cerr != nil {
+ err = cerr
+ continue
+ }
+ msg, merr = exchange(cfg, c, name, qtype)
+ c.Close()
+ if merr != nil {
+ err = merr
+ continue
+ }
+ }
cname, addrs, err = answer(name, server, msg, qtype)
if err == nil || err.(*DNSError).Err == noSuchHost {
break
src/pkg/net/dnsclient_unix_test.go
(新規ファイル)
--- /dev/null
+++ b/src/pkg/net/dnsclient_unix_test.go
@@ -0,0 +1,29 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package net
+
+import (
+ "runtime"
+ "testing"
+)
+
+func TestTCPLookup(t *testing.T) {
+ if runtime.GOOS == "windows" || runtime.GOOS == "plan9" {
+ t.Skip("skipping unix dns test")
+ }
+ if testing.Short() || !*testExternal {
+ t.Skip("skipping test to avoid external network")
+ }
+ c, err := Dial("tcp", "8.8.8.8:53")
+ defer c.Close()
+ if err != nil {
+ t.Fatalf("Dial failed: %v", err)
+ }
+ cfg := &dnsConfig{timeout: 10, attempts: 3}
+ _, err = exchange(cfg, c, "com.", dnsTypeALL)
+ if err != nil {
+ t.Fatalf("exchange failed: %v", err)
+ }
+}
コアとなるコードの解説
src/pkg/net/dnsclient_unix.go
-
import "io"
の追加:- TCPストリームから正確なバイト数を読み込むために、
io.ReadFull
関数を使用するため、io
パッケージがインポートされました。
- TCPストリームから正確なバイト数を読み込むために、
-
exchange
関数の変更:useTCP
変数の導入:exchange
関数に渡されるConn
インターフェースの実体が*UDPConn
か*TCPConn
かをswitch
文で判定し、useTCP
フラグを設定します。これにより、UDPとTCPで異なる処理を適用できるようになります。- TCPメッセージ長のプレフィックス追加:
if useTCP
ブロック内で、DNSメッセージのバイト列msg
の先頭に、メッセージ全体の長さを示す2バイトのプレフィックスが追加されます。mlen := uint16(len(msg))
でメッセージ長を取得し、append([]byte{byte(mlen >> 8), byte(mlen)}, msg...)
でバイト列の先頭に挿入しています。これは、TCP上でDNSメッセージを送信する際の標準的なフォーマットです。 - TCP応答の読み込みロジックの変更:
if useTCP
ブロック内で、TCPコネクションからの応答読み込み方法が変更されました。- まず
io.ReadFull(c, buf[:2])
で最初の2バイトを読み込みます。この2バイトがDNSメッセージの全長を示します。 - 次に、
buf = make([]byte, uint16(buf[0])<<8+uint16(buf[1]))
で、読み込んだ2バイトからメッセージの実際の長さを計算し、その長さの新しいバッファを作成します。 - 最後に
io.ReadFull(c, buf)
で、計算された長さのメッセージ全体を読み込みます。これにより、TCPストリームから完全なDNS応答を確実に取得できます。
- まず
-
tryOneName
関数の変更:if msg.truncated
ブロックの追加: UDPによるexchange
呼び出しから返されたdnsMsg
オブジェクトのtruncated
フィールドがtrue
(つまり、応答が切り詰められた)であるかをチェックします。- TCPフォールバック処理:
c, cerr = Dial("tcp", server)
: 現在のDNSサーバーに対してTCPコネクションを新しく確立します。msg, merr = exchange(cfg, c, name, qtype)
: 新しく確立したTCPコネクションを使用して、同じDNSクエリを再試行します。c.Close()
: TCPコネクションは使い捨てであるため、応答を受信したらすぐに閉じます。- エラーハンドリング: TCPでの再試行中にエラーが発生した場合も適切に処理し、
err
変数に格納してループを続行します。
src/pkg/net/dnsclient_unix_test.go
TestTCPLookup
関数の追加:- このテストは、GoのDNSクライアントがTCP経由でDNSクエリを正しく実行できることを検証します。
runtime.GOOS
のチェックにより、WindowsやPlan 9などの一部のOSではテストをスキップします。これは、このテストがUnix系のDNSクライアント実装に特化しているためです。testing.Short()
と*testExternal
のチェックにより、短時間テストや外部ネットワークへのアクセスを避ける設定の場合もスキップされます。これは、このテストが外部のDNSサーバー(Google Public DNS)に実際に接続するためです。Dial("tcp", "8.8.8.8:53")
でGoogle Public DNSサーバーにTCP接続を試みます。exchange(cfg, c, "com.", dnsTypeALL)
を呼び出し、TCPコネクション経由でcom.
ドメインのすべてのレコードタイプ(dnsTypeALL
)を問い合わせます。- エラーが発生した場合、
t.Fatalf
でテストを失敗させます。
これらの変更により、Goのnet
パッケージは、DNSのUDPメッセージ切り詰め問題に対して、標準的なTCPフォールバックメカニズムを実装し、より堅牢で信頼性の高いDNS解決機能を提供できるようになりました。
関連リンク
- Go Issue #5686: net: implement DNS TCP fallback query if UDP response is truncated
- Gerrit Change-Id: https://golang.org/cl/12458043
- RFC 1035 (Domain Names - Implementation and Specification): 特に4.2.1 (UDP usage) と 4.2.2 (TCP usage) セクションが関連します。
- RFC 5966 (DNS Message Size Issues): UDP応答の切り詰めとTCPフォールバックに関する詳細な議論が含まれています。
参考にした情報源リンク
- 上記の関連リンクに記載されたRFCドキュメントとGoのIssue/Gerrit変更リスト。
- Go言語の
net
パッケージのソースコード。 - DNSプロトコルに関する一般的な知識。
- UDPとTCPの特性に関する一般的なネットワーク知識。
io.ReadFull
関数のGoドキュメント。- RFC 5966に関するWeb検索結果。