mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 18:37:45 +08:00
Some checks failed
CodeQL / codeql (push) Has been cancelled
Docker Build / docker-build (push) Has been cancelled
Gateway Conformance / gateway-conformance (push) Has been cancelled
Gateway Conformance / gateway-conformance-libp2p-experiment (push) Has been cancelled
Go Build / go-build (push) Has been cancelled
Go Check / go-check (push) Has been cancelled
Go Lint / go-lint (push) Has been cancelled
Go Test / go-test (push) Has been cancelled
Interop / interop-prep (push) Has been cancelled
Sharness / sharness-test (push) Has been cancelled
Spell Check / spellcheck (push) Has been cancelled
Interop / helia-interop (push) Has been cancelled
Interop / ipfs-webui (push) Has been cancelled
https://github.com/ipfs/kubo/pull/10883 https://github.com/ipshipyard/config.ipfs-mainnet.org/issues/3 --------- Co-authored-by: gammazero <gammazero@users.noreply.github.com>
289 lines
7.6 KiB
Go
289 lines
7.6 KiB
Go
package autoconf
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/ipfs/kubo/test/cli/harness"
|
|
"github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAutoConfDNS(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("DNS resolution with auto DoH resolver", func(t *testing.T) {
|
|
t.Parallel()
|
|
testDNSResolutionWithAutoDoH(t)
|
|
})
|
|
|
|
t.Run("DNS errors are handled properly", func(t *testing.T) {
|
|
t.Parallel()
|
|
testDNSErrorHandling(t)
|
|
})
|
|
}
|
|
|
|
// mockDoHServer implements a simple DNS-over-HTTPS server for testing
|
|
type mockDoHServer struct {
|
|
t *testing.T
|
|
server *httptest.Server
|
|
mu sync.Mutex
|
|
requests []string
|
|
responseFunc func(name string) *dns.Msg
|
|
}
|
|
|
|
func newMockDoHServer(t *testing.T) *mockDoHServer {
|
|
m := &mockDoHServer{
|
|
t: t,
|
|
requests: []string{},
|
|
}
|
|
|
|
// Default response function returns a dnslink TXT record
|
|
m.responseFunc = func(name string) *dns.Msg {
|
|
msg := &dns.Msg{}
|
|
msg.SetReply(&dns.Msg{Question: []dns.Question{{Name: name, Qtype: dns.TypeTXT}}})
|
|
|
|
if strings.HasPrefix(name, "_dnslink.") {
|
|
// Return a valid dnslink record
|
|
rr := &dns.TXT{
|
|
Hdr: dns.RR_Header{
|
|
Name: name,
|
|
Rrtype: dns.TypeTXT,
|
|
Class: dns.ClassINET,
|
|
Ttl: 300,
|
|
},
|
|
Txt: []string{"dnslink=/ipfs/QmYNQJoKGNHTpPxCBPh9KkDpaExgd2duMa3aF6ytMpHdao"},
|
|
}
|
|
msg.Answer = append(msg.Answer, rr)
|
|
}
|
|
|
|
return msg
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/dns-query", m.handleDNSQuery)
|
|
|
|
m.server = httptest.NewServer(mux)
|
|
return m
|
|
}
|
|
|
|
func (m *mockDoHServer) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
var dnsMsg *dns.Msg
|
|
|
|
if r.Method == "GET" {
|
|
// Handle GET with ?dns= parameter
|
|
dnsParam := r.URL.Query().Get("dns")
|
|
if dnsParam == "" {
|
|
http.Error(w, "missing dns parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
data, err := base64.RawURLEncoding.DecodeString(dnsParam)
|
|
if err != nil {
|
|
http.Error(w, "invalid base64", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
dnsMsg = &dns.Msg{}
|
|
if err := dnsMsg.Unpack(data); err != nil {
|
|
http.Error(w, "invalid DNS message", http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else if r.Method == "POST" {
|
|
// Handle POST with DNS wire format
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
dnsMsg = &dns.Msg{}
|
|
if err := dnsMsg.Unpack(data); err != nil {
|
|
http.Error(w, "invalid DNS message", http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Log the DNS query
|
|
if len(dnsMsg.Question) > 0 {
|
|
qname := dnsMsg.Question[0].Name
|
|
m.requests = append(m.requests, qname)
|
|
m.t.Logf("DoH server received query for: %s", qname)
|
|
}
|
|
|
|
// Generate response
|
|
response := m.responseFunc(dnsMsg.Question[0].Name)
|
|
responseData, err := response.Pack()
|
|
if err != nil {
|
|
http.Error(w, "failed to pack response", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/dns-message")
|
|
_, _ = w.Write(responseData)
|
|
}
|
|
|
|
func (m *mockDoHServer) getRequests() []string {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return append([]string{}, m.requests...)
|
|
}
|
|
|
|
func (m *mockDoHServer) close() {
|
|
m.server.Close()
|
|
}
|
|
|
|
func testDNSResolutionWithAutoDoH(t *testing.T) {
|
|
// Create mock DoH server
|
|
dohServer := newMockDoHServer(t)
|
|
defer dohServer.close()
|
|
|
|
// Create autoconf data with DoH resolver for "foo." domain
|
|
autoConfData := fmt.Sprintf(`{
|
|
"AutoConfVersion": 2025072302,
|
|
"AutoConfSchema": 1,
|
|
"AutoConfTTL": 86400,
|
|
"SystemRegistry": {
|
|
"AminoDHT": {
|
|
"Description": "Test AminoDHT system",
|
|
"NativeConfig": {
|
|
"Bootstrap": []
|
|
}
|
|
}
|
|
},
|
|
"DNSResolvers": {
|
|
"foo.": ["%s/dns-query"]
|
|
},
|
|
"DelegatedEndpoints": {}
|
|
}`, dohServer.server.URL)
|
|
|
|
// Create autoconf server
|
|
autoConfServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(autoConfData))
|
|
}))
|
|
defer autoConfServer.Close()
|
|
|
|
// Create IPFS node with auto DNS resolver
|
|
node := harness.NewT(t).NewNode().Init("--profile=test")
|
|
node.SetIPFSConfig("AutoConf.URL", autoConfServer.URL)
|
|
node.SetIPFSConfig("AutoConf.Enabled", true)
|
|
node.SetIPFSConfig("DNS.Resolvers", map[string]string{"foo.": "auto"})
|
|
|
|
// Start daemon
|
|
node.StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Verify config still shows "auto" for DNS resolvers
|
|
result := node.RunIPFS("config", "DNS.Resolvers")
|
|
require.Equal(t, 0, result.ExitCode())
|
|
dnsResolversOutput := result.Stdout.String()
|
|
assert.Contains(t, dnsResolversOutput, "foo.", "DNS resolvers should contain foo. domain")
|
|
assert.Contains(t, dnsResolversOutput, "auto", "DNS resolver config should show 'auto'")
|
|
|
|
// Try to resolve a .foo domain
|
|
result = node.RunIPFS("resolve", "/ipns/example.foo")
|
|
require.Equal(t, 0, result.ExitCode())
|
|
|
|
// Should resolve to the IPFS path from our mock DoH server
|
|
output := strings.TrimSpace(result.Stdout.String())
|
|
assert.Equal(t, "/ipfs/QmYNQJoKGNHTpPxCBPh9KkDpaExgd2duMa3aF6ytMpHdao", output,
|
|
"Should resolve to the path returned by DoH server")
|
|
|
|
// Verify DoH server received the DNS query
|
|
requests := dohServer.getRequests()
|
|
require.Greater(t, len(requests), 0, "DoH server should have received at least one request")
|
|
|
|
foundDNSLink := false
|
|
for _, req := range requests {
|
|
if strings.Contains(req, "_dnslink.example.foo") {
|
|
foundDNSLink = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundDNSLink, "DoH server should have received query for _dnslink.example.foo")
|
|
}
|
|
|
|
func testDNSErrorHandling(t *testing.T) {
|
|
// Create DoH server that returns NXDOMAIN
|
|
dohServer := newMockDoHServer(t)
|
|
defer dohServer.close()
|
|
|
|
// Configure to return NXDOMAIN
|
|
dohServer.responseFunc = func(name string) *dns.Msg {
|
|
msg := &dns.Msg{}
|
|
msg.SetReply(&dns.Msg{Question: []dns.Question{{Name: name, Qtype: dns.TypeTXT}}})
|
|
msg.Rcode = dns.RcodeNameError // NXDOMAIN
|
|
return msg
|
|
}
|
|
|
|
// Create autoconf data with DoH resolver
|
|
autoConfData := fmt.Sprintf(`{
|
|
"AutoConfVersion": 2025072302,
|
|
"AutoConfSchema": 1,
|
|
"AutoConfTTL": 86400,
|
|
"SystemRegistry": {
|
|
"AminoDHT": {
|
|
"Description": "Test AminoDHT system",
|
|
"NativeConfig": {
|
|
"Bootstrap": []
|
|
}
|
|
}
|
|
},
|
|
"DNSResolvers": {
|
|
"bar.": ["%s/dns-query"]
|
|
},
|
|
"DelegatedEndpoints": {}
|
|
}`, dohServer.server.URL)
|
|
|
|
// Create autoconf server
|
|
autoConfServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(autoConfData))
|
|
}))
|
|
defer autoConfServer.Close()
|
|
|
|
// Create IPFS node
|
|
node := harness.NewT(t).NewNode().Init("--profile=test")
|
|
node.SetIPFSConfig("AutoConf.URL", autoConfServer.URL)
|
|
node.SetIPFSConfig("AutoConf.Enabled", true)
|
|
node.SetIPFSConfig("DNS.Resolvers", map[string]string{"bar.": "auto"})
|
|
|
|
// Start daemon
|
|
node.StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Try to resolve a non-existent domain
|
|
result := node.RunIPFS("resolve", "/ipns/nonexistent.bar")
|
|
require.NotEqual(t, 0, result.ExitCode(), "Resolution should fail for non-existent domain")
|
|
|
|
// Should contain appropriate error message
|
|
stderr := result.Stderr.String()
|
|
assert.Contains(t, stderr, "could not resolve name",
|
|
"Error should indicate DNS resolution failure")
|
|
|
|
// Verify DoH server received the query
|
|
requests := dohServer.getRequests()
|
|
foundQuery := false
|
|
for _, req := range requests {
|
|
if strings.Contains(req, "_dnslink.nonexistent.bar") {
|
|
foundQuery = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundQuery, "DoH server should have received query even for failed resolution")
|
|
}
|