1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package resolver
import (
"context"
"io"
"net"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"golang.org/x/net/dns/dnsmessage"
)
// NewThrough asks its servers over the connections dial opens (tunnels
// through Mozilla in the program), caches the answer, and never falls
// back to the system resolver.
func TestThroughUsesDialAndNoSystemFallback(t *testing.T) {
var hits atomic.Int32
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
body, _ := io.ReadAll(r.Body)
w.Header().Set("Content-Type", dohContentType)
_, _ = w.Write(answerFor(t, body, [4]byte{93, 184, 215, 14}, 120))
}))
defer srv.Close()
oldServers, oldClient := ThroughServers, throughClient
t.Cleanup(func() { ThroughServers, throughClient = oldServers, oldClient })
ThroughServers = []string{"https://dns.invalid/dns-query"}
var dialed atomic.Int32
var dialedTo atomic.Value
throughClient = func(dial DialFunc) *http.Client {
client := srv.Client()
transport := client.Transport.(*http.Transport).Clone()
transport.DialContext = dial
transport.TLSClientConfig.ServerName = "example.com" // httptest's certificate
client.Transport = transport
return client
}
r := NewThrough(func(ctx context.Context, network, address string) (net.Conn, error) {
dialed.Add(1)
dialedTo.Store(address)
var d net.Dialer
return d.DialContext(ctx, "tcp", srv.Listener.Addr().String())
})
res, err := r.Lookup(context.Background(), "blocked.example")
if err != nil || len(res.Addrs) != 1 || res.Addrs[0].String() != "93.184.215.14" {
t.Fatalf("lookup: %+v %v", res, err)
}
if dialed.Load() == 0 || dialedTo.Load() != "dns.invalid:443" {
t.Fatalf("the query must use dial for the server's address: %d %v", dialed.Load(), dialedTo.Load())
}
if again, err := r.Lookup(context.Background(), "blocked.example"); err != nil || !again.Cached || hits.Load() != 1 {
t.Fatalf("second lookup: %+v %v hits=%d", again, err, hits.Load())
}
failing := NewThrough(func(context.Context, string, string) (net.Conn, error) {
return nil, io.ErrUnexpectedEOF
})
if res, err := failing.Lookup(context.Background(), "localhost.example"); err == nil {
t.Fatalf("a failed query must not fall back to the system resolver: %+v", res)
}
}
// Exchange passes a client's whole DNS message, of any type, to DNS over
// HTTPS through dial and returns the answer unchanged.
func TestExchangePassesTheMessageAsItIs(t *testing.T) {
var got atomic.Value
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
got.Store(body)
w.Header().Set("Content-Type", dohContentType)
_, _ = w.Write(append([]byte("answer:"), body...))
}))
defer srv.Close()
oldServers, oldClient := ThroughServers, throughClient
t.Cleanup(func() { ThroughServers, throughClient = oldServers, oldClient })
ThroughServers = []string{"https://dns.invalid/dns-query"}
throughClient = func(dial DialFunc) *http.Client {
client := srv.Client()
transport := client.Transport.(*http.Transport).Clone()
transport.DialContext = dial
transport.TLSClientConfig.ServerName = "example.com" // httptest's certificate
client.Transport = transport
return client
}
r := NewThrough(func(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "tcp", srv.Listener.Addr().String())
})
name, _ := dnsmessage.NewName("example.com.")
query, err := (&dnsmessage.Message{
Header: dnsmessage.Header{ID: 4242, RecursionDesired: true},
Questions: []dnsmessage.Question{{Name: name, Type: dnsmessage.TypeMX, Class: dnsmessage.ClassINET}},
}).Pack()
if err != nil {
t.Fatal(err)
}
answer, err := r.Exchange(context.Background(), query)
if err != nil {
t.Fatal(err)
}
if string(got.Load().([]byte)) != string(query) || string(answer) != "answer:"+string(query) {
t.Fatalf("sent %q, answered %q", got.Load(), answer)
}
// Only DNS over HTTPS can carry a whole message.
plain, err := New([]string{"udp://9.9.9.9"}, false)
if err != nil {
t.Fatal(err)
}
if _, err := plain.Exchange(context.Background(), query); err == nil {
t.Fatal("a resolver without DNS over HTTPS exchanged a message")
}
}