master
go 288 lines 7.61 KB
Raw
1 package autoconf
2
3 import (
4 "encoding/base64"
5 "fmt"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "sync"
11 "testing"
12
13 "github.com/ipfs/kubo/test/cli/harness"
14 "github.com/miekg/dns"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17 )
18
19 func TestAutoConfDNS(t *testing.T) {
20 t.Parallel()
21
22 t.Run("DNS resolution with auto DoH resolver", func(t *testing.T) {
23 t.Parallel()
24 testDNSResolutionWithAutoDoH(t)
25 })
26
27 t.Run("DNS errors are handled properly", func(t *testing.T) {
28 t.Parallel()
29 testDNSErrorHandling(t)
30 })
31 }
32
33 // mockDoHServer implements a simple DNS-over-HTTPS server for testing
34 type mockDoHServer struct {
35 t *testing.T
36 server *httptest.Server
37 mu sync.Mutex
38 requests []string
39 responseFunc func(name string) *dns.Msg
40 }
41
42 func newMockDoHServer(t *testing.T) *mockDoHServer {
43 m := &mockDoHServer{
44 t: t,
45 requests: []string{},
46 }
47
48 // Default response function returns a dnslink TXT record
49 m.responseFunc = func(name string) *dns.Msg {
50 msg := &dns.Msg{}
51 msg.SetReply(&dns.Msg{Question: []dns.Question{{Name: name, Qtype: dns.TypeTXT}}})
52
53 if strings.HasPrefix(name, "_dnslink.") {
54 // Return a valid dnslink record
55 rr := &dns.TXT{
56 Hdr: dns.RR_Header{
57 Name: name,
58 Rrtype: dns.TypeTXT,
59 Class: dns.ClassINET,
60 Ttl: 300,
61 },
62 Txt: []string{"dnslink=/ipfs/QmYNQJoKGNHTpPxCBPh9KkDpaExgd2duMa3aF6ytMpHdao"},
63 }
64 msg.Answer = append(msg.Answer, rr)
65 }
66
67 return msg
68 }
69
70 mux := http.NewServeMux()
71 mux.HandleFunc("/dns-query", m.handleDNSQuery)
72
73 m.server = httptest.NewServer(mux)
74 return m
75 }
76
77 func (m *mockDoHServer) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
78 m.mu.Lock()
79 defer m.mu.Unlock()
80
81 var dnsMsg *dns.Msg
82
83 if r.Method == "GET" {
84 // Handle GET with ?dns= parameter
85 dnsParam := r.URL.Query().Get("dns")
86 if dnsParam == "" {
87 http.Error(w, "missing dns parameter", http.StatusBadRequest)
88 return
89 }
90
91 data, err := base64.RawURLEncoding.DecodeString(dnsParam)
92 if err != nil {
93 http.Error(w, "invalid base64", http.StatusBadRequest)
94 return
95 }
96
97 dnsMsg = &dns.Msg{}
98 if err := dnsMsg.Unpack(data); err != nil {
99 http.Error(w, "invalid DNS message", http.StatusBadRequest)
100 return
101 }
102 } else if r.Method == "POST" {
103 // Handle POST with DNS wire format
104 data, err := io.ReadAll(r.Body)
105 if err != nil {
106 http.Error(w, "failed to read body", http.StatusBadRequest)
107 return
108 }
109
110 dnsMsg = &dns.Msg{}
111 if err := dnsMsg.Unpack(data); err != nil {
112 http.Error(w, "invalid DNS message", http.StatusBadRequest)
113 return
114 }
115 } else {
116 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
117 return
118 }
119
120 // Log the DNS query
121 if len(dnsMsg.Question) > 0 {
122 qname := dnsMsg.Question[0].Name
123 m.requests = append(m.requests, qname)
124 m.t.Logf("DoH server received query for: %s", qname)
125 }
126
127 // Generate response
128 response := m.responseFunc(dnsMsg.Question[0].Name)
129 responseData, err := response.Pack()
130 if err != nil {
131 http.Error(w, "failed to pack response", http.StatusInternalServerError)
132 return
133 }
134
135 w.Header().Set("Content-Type", "application/dns-message")
136 _, _ = w.Write(responseData)
137 }
138
139 func (m *mockDoHServer) getRequests() []string {
140 m.mu.Lock()
141 defer m.mu.Unlock()
142 return append([]string{}, m.requests...)
143 }
144
145 func (m *mockDoHServer) close() {
146 m.server.Close()
147 }
148
149 func testDNSResolutionWithAutoDoH(t *testing.T) {
150 // Create mock DoH server
151 dohServer := newMockDoHServer(t)
152 defer dohServer.close()
153
154 // Create autoconf data with DoH resolver for "foo." domain
155 autoConfData := fmt.Sprintf(`{
156 "AutoConfVersion": 2025072302,
157 "AutoConfSchema": 1,
158 "AutoConfTTL": 86400,
159 "SystemRegistry": {
160 "AminoDHT": {
161 "Description": "Test AminoDHT system",
162 "NativeConfig": {
163 "Bootstrap": []
164 }
165 }
166 },
167 "DNSResolvers": {
168 "foo.": ["%s/dns-query"]
169 },
170 "DelegatedEndpoints": {}
171 }`, dohServer.server.URL)
172
173 // Create autoconf server
174 autoConfServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
175 w.Header().Set("Content-Type", "application/json")
176 _, _ = w.Write([]byte(autoConfData))
177 }))
178 defer autoConfServer.Close()
179
180 // Create IPFS node with auto DNS resolver
181 node := harness.NewT(t).NewNode().Init("--profile=test")
182 node.SetIPFSConfig("AutoConf.URL", autoConfServer.URL)
183 node.SetIPFSConfig("AutoConf.Enabled", true)
184 node.SetIPFSConfig("DNS.Resolvers", map[string]string{"foo.": "auto"})
185
186 // Start daemon
187 node.StartDaemon()
188 defer node.StopDaemon()
189
190 // Verify config still shows "auto" for DNS resolvers
191 result := node.RunIPFS("config", "DNS.Resolvers")
192 require.Equal(t, 0, result.ExitCode())
193 dnsResolversOutput := result.Stdout.String()
194 assert.Contains(t, dnsResolversOutput, "foo.", "DNS resolvers should contain foo. domain")
195 assert.Contains(t, dnsResolversOutput, "auto", "DNS resolver config should show 'auto'")
196
197 // Try to resolve a .foo domain
198 result = node.RunIPFS("resolve", "/ipns/example.foo")
199 require.Equal(t, 0, result.ExitCode())
200
201 // Should resolve to the IPFS path from our mock DoH server
202 output := strings.TrimSpace(result.Stdout.String())
203 assert.Equal(t, "/ipfs/QmYNQJoKGNHTpPxCBPh9KkDpaExgd2duMa3aF6ytMpHdao", output,
204 "Should resolve to the path returned by DoH server")
205
206 // Verify DoH server received the DNS query
207 requests := dohServer.getRequests()
208 require.Greater(t, len(requests), 0, "DoH server should have received at least one request")
209
210 foundDNSLink := false
211 for _, req := range requests {
212 if strings.Contains(req, "_dnslink.example.foo") {
213 foundDNSLink = true
214 break
215 }
216 }
217 assert.True(t, foundDNSLink, "DoH server should have received query for _dnslink.example.foo")
218 }
219
220 func testDNSErrorHandling(t *testing.T) {
221 // Create DoH server that returns NXDOMAIN
222 dohServer := newMockDoHServer(t)
223 defer dohServer.close()
224
225 // Configure to return NXDOMAIN
226 dohServer.responseFunc = func(name string) *dns.Msg {
227 msg := &dns.Msg{}
228 msg.SetReply(&dns.Msg{Question: []dns.Question{{Name: name, Qtype: dns.TypeTXT}}})
229 msg.Rcode = dns.RcodeNameError // NXDOMAIN
230 return msg
231 }
232
233 // Create autoconf data with DoH resolver
234 autoConfData := fmt.Sprintf(`{
235 "AutoConfVersion": 2025072302,
236 "AutoConfSchema": 1,
237 "AutoConfTTL": 86400,
238 "SystemRegistry": {
239 "AminoDHT": {
240 "Description": "Test AminoDHT system",
241 "NativeConfig": {
242 "Bootstrap": []
243 }
244 }
245 },
246 "DNSResolvers": {
247 "bar.": ["%s/dns-query"]
248 },
249 "DelegatedEndpoints": {}
250 }`, dohServer.server.URL)
251
252 // Create autoconf server
253 autoConfServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
254 w.Header().Set("Content-Type", "application/json")
255 _, _ = w.Write([]byte(autoConfData))
256 }))
257 defer autoConfServer.Close()
258
259 // Create IPFS node
260 node := harness.NewT(t).NewNode().Init("--profile=test")
261 node.SetIPFSConfig("AutoConf.URL", autoConfServer.URL)
262 node.SetIPFSConfig("AutoConf.Enabled", true)
263 node.SetIPFSConfig("DNS.Resolvers", map[string]string{"bar.": "auto"})
264
265 // Start daemon
266 node.StartDaemon()
267 defer node.StopDaemon()
268
269 // Try to resolve a non-existent domain
270 result := node.RunIPFS("resolve", "/ipns/nonexistent.bar")
271 require.NotEqual(t, 0, result.ExitCode(), "Resolution should fail for non-existent domain")
272
273 // Should contain appropriate error message
274 stderr := result.Stderr.String()
275 assert.Contains(t, stderr, "could not resolve name",
276 "Error should indicate DNS resolution failure")
277
278 // Verify DoH server received the query
279 requests := dohServer.getRequests()
280 foundQuery := false
281 for _, req := range requests {
282 if strings.Contains(req, "_dnslink.nonexistent.bar") {
283 foundQuery = true
284 break
285 }
286 }
287 assert.True(t, foundQuery, "DoH server should have received query even for failed resolution")
288 }