Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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応答が切り詰められる主なシナリオは以下の通りです。

  1. DNSSEC (DNS Security Extensions): DNSSECはDNS応答の認証と完全性を保証するための拡張機能であり、署名データを含むため応答サイズが大きくなる傾向があります。
  2. 多数のレコード: 特定のドメイン名に対して非常に多くのリソースレコード(例: 多数のAレコード、MXレコードなど)が存在する場合、応答が512バイトを超えることがあります。
  3. 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フォールバックロジックを追加したことです。

  1. exchange関数の変更:

    • exchange関数は、DNSクエリを送信し、応答を受信する役割を担います。
    • この関数にuseTCPという新しいブール変数が導入されました。これは、現在使用しているコネクションがTCPベースであるかどうかを判断するために使用されます。
    • TCPコネクションの場合、DNSメッセージの先頭に2バイトの長さフィールド(ネットワークバイトオーダー)を追加する処理が追加されました。これは、TCP上でDNSメッセージを送信する際の標準的な方法です(RFC 1035の4.2.2節)。
    • 応答の読み込み部分も変更されました。TCPコネクションの場合、まずio.ReadFullを使用して2バイトの長さフィールドを読み込み、その長さに従って残りのメッセージを読み込むように修正されました。これにより、TCPストリームから完全なDNSメッセージを正確に読み取ることができます。
  2. tryOneName関数の変更:

    • tryOneName関数は、複数のDNSサーバーに対してクエリを試行するロジックを含んでいます。
    • UDPによるexchange呼び出しの後、受信したDNSメッセージのtruncatedフラグ(msg.truncated)がチェックされます。
    • もしtruncatedフラグがtrueであれば、RFC 5966の推奨に従い、同じDNSサーバーに対してTCPコネクションを確立し、再度exchange関数を呼び出してクエリを再試行します。
    • TCPでの再試行が成功した場合、その応答が使用されます。失敗した場合は、エラーが伝播されます。
  3. テストの追加:

    • 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

  1. import "io" の追加:

    • TCPストリームから正確なバイト数を読み込むために、io.ReadFull関数を使用するため、ioパッケージがインポートされました。
  2. 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応答を確実に取得できます。
  3. 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解決機能を提供できるようになりました。

関連リンク

参考にした情報源リンク

  • 上記の関連リンクに記載された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応答が切り詰められる主なシナリオは以下の通りです。

  1. DNSSEC (DNS Security Extensions): DNSSECはDNS応答の認証と完全性を保証するための拡張機能であり、署名データを含むため応答サイズが大きくなる傾向があります。
  2. 多数のレコード: 特定のドメイン名に対して非常に多くのリソースレコード(例: 多数のAレコード、MXレコードなど)が存在する場合、応答が512バイトを超えることがあります。
  3. 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フォールバックロジックを追加したことです。

  1. exchange関数の変更:

    • exchange関数は、DNSクエリを送信し、応答を受信する役割を担います。
    • この関数にuseTCPという新しいブール変数が導入されました。これは、現在使用しているコネクションがTCPベースであるかどうかを判断するために使用されます。
    • TCPコネクションの場合、DNSメッセージの先頭に2バイトの長さフィールド(ネットワークバイトオーダー)を追加する処理が追加されました。これは、TCP上でDNSメッセージを送信する際の標準的な方法です(RFC 1035の4.2.2節)。
    • 応答の読み込み部分も変更されました。TCPコネクションの場合、まずio.ReadFullを使用して2バイトの長さフィールドを読み込み、その長さに従って残りのメッセージを読み込むように修正されました。これにより、TCPストリームから完全なDNSメッセージを正確に読み取ることができます。
  2. tryOneName関数の変更:

    • tryOneName関数は、複数のDNSサーバーに対してクエリを試行するロジックを含んでいます。
    • UDPによるexchange呼び出しの後、受信したDNSメッセージのtruncatedフラグ(msg.truncated)がチェックされます。
    • もしtruncatedフラグがtrueであれば、RFC 5966の推奨に従い、同じDNSサーバーに対してTCPコネクションを確立し、再度exchange関数を呼び出してクエリを再試行します。
    • TCPでの再試行が成功した場合、その応答が使用されます。失敗した場合は、エラーが伝播されます。
  3. テストの追加:

    • 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

  1. import "io" の追加:

    • TCPストリームから正確なバイト数を読み込むために、io.ReadFull関数を使用するため、ioパッケージがインポートされました。
  2. 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応答を確実に取得できます。
  3. 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解決機能を提供できるようになりました。

関連リンク

参考にした情報源リンク

  • 上記の関連リンクに記載されたRFCドキュメントとGoのIssue/Gerrit変更リスト。
  • Go言語のnetパッケージのソースコード。
  • DNSプロトコルに関する一般的な知識。
  • UDPとTCPの特性に関する一般的なネットワーク知識。
  • io.ReadFull関数のGoドキュメント。
  • RFC 5966に関するWeb検索結果。