kubo/test/cli/autoconf/dns_test.go
Marcin Rataj ccb49de852
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
feat(config): AutoConf with "auto" placeholders (#10883)
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>
2025-08-20 05:59:11 +02:00

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")
}