master
go 143 lines 5 KB
Raw
1 package cli
2
3 import (
4 "strings"
5 "testing"
6 "time"
7
8 "github.com/ipfs/kubo/config"
9 "github.com/ipfs/kubo/test/cli/harness"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12 )
13
14 // testDomainSuffix is the default p2p-forge domain used in tests
15 const testDomainSuffix = config.DefaultDomainSuffix // libp2p.direct
16
17 // TestDNSResolversApplyToMultiaddr is a regression test for:
18 // https://github.com/ipfs/kubo/issues/9199
19 //
20 // It verifies that DNS.Resolvers config is used when resolving /dnsaddr,
21 // /dns, /dns4, /dns6 multiaddrs during peer connections, not just for
22 // DNSLink resolution.
23 func TestDNSResolversApplyToMultiaddr(t *testing.T) {
24 t.Parallel()
25
26 t.Run("invalid DoH resolver causes multiaddr resolution to fail", func(t *testing.T) {
27 t.Parallel()
28
29 node := harness.NewT(t).NewNode().Init("--profile=test")
30
31 // Set an invalid DoH resolver that will fail when used.
32 // If DNS.Resolvers is properly wired to multiaddr resolution,
33 // swarm connect to a /dnsaddr will fail with an error mentioning
34 // the invalid resolver URL.
35 invalidResolver := "https://invalid.broken.resolver.test/dns-query"
36 node.SetIPFSConfig("DNS.Resolvers", map[string]string{
37 ".": invalidResolver,
38 })
39
40 // Clear bootstrap peers to prevent background connection attempts
41 node.SetIPFSConfig("Bootstrap", []string{})
42
43 node.StartDaemon()
44 defer node.StopDaemon()
45
46 // Give daemon time to fully start
47 time.Sleep(2 * time.Second)
48
49 // Verify daemon is responsive
50 result := node.RunIPFS("id")
51 require.Equal(t, 0, result.ExitCode(), "daemon should be responsive")
52
53 // Try to connect to a /dnsaddr peer - this should fail because
54 // the DNS.Resolvers config points to an invalid DoH server
55 result = node.RunIPFS("swarm", "connect", "/dnsaddr/bootstrap.libp2p.io")
56
57 // The connection should fail
58 require.NotEqual(t, 0, result.ExitCode(),
59 "swarm connect should fail when DNS.Resolvers points to invalid DoH server")
60
61 // The error should mention the invalid resolver, proving DNS.Resolvers
62 // is being used for multiaddr resolution
63 stderr := result.Stderr.String()
64 assert.True(t,
65 strings.Contains(stderr, "invalid.broken.resolver.test") ||
66 strings.Contains(stderr, "no such host") ||
67 strings.Contains(stderr, "lookup") ||
68 strings.Contains(stderr, "dial"),
69 "error should indicate DNS resolution failure using custom resolver. got: %s", stderr)
70 })
71
72 t.Run("libp2p.direct resolves locally even with broken DNS.Resolvers", func(t *testing.T) {
73 t.Parallel()
74
75 h := harness.NewT(t)
76 nodes := h.NewNodes(2).Init("--profile=test")
77
78 // Configure node0 with a broken DNS resolver
79 // This would break all DNS resolution if libp2p.direct wasn't resolved locally
80 invalidResolver := "https://invalid.broken.resolver.test/dns-query"
81 nodes[0].SetIPFSConfig("DNS.Resolvers", map[string]string{
82 ".": invalidResolver,
83 })
84
85 // Clear bootstrap peers on both nodes
86 for _, n := range nodes {
87 n.SetIPFSConfig("Bootstrap", []string{})
88 }
89
90 nodes.StartDaemons()
91 defer nodes.StopDaemons()
92
93 // Get node1's peer ID in base36 format (what p2p-forge uses in DNS hostnames)
94 // DNS is case-insensitive, and base36 is lowercase-only, making it ideal for DNS
95 idResult := nodes[1].RunIPFS("id", "--peerid-base", "base36", "-f", "<id>")
96 require.Equal(t, 0, idResult.ExitCode())
97 node1IDBase36 := strings.TrimSpace(idResult.Stdout.String())
98 node1ID := nodes[1].PeerID().String()
99 node1Addrs := nodes[1].SwarmAddrs()
100
101 // Find a TCP address we can use
102 var tcpAddr string
103 for _, addr := range node1Addrs {
104 addrStr := addr.String()
105 if strings.Contains(addrStr, "/tcp/") && strings.Contains(addrStr, "/ip4/127.0.0.1") {
106 tcpAddr = addrStr
107 break
108 }
109 }
110 require.NotEmpty(t, tcpAddr, "node1 should have a local TCP address")
111
112 // Extract port from address like /ip4/127.0.0.1/tcp/12345/...
113 parts := strings.Split(tcpAddr, "/")
114 var port string
115 for i, p := range parts {
116 if p == "tcp" && i+1 < len(parts) {
117 port = parts[i+1]
118 break
119 }
120 }
121 require.NotEmpty(t, port, "should find TCP port in address")
122
123 // Construct a libp2p.direct hostname that encodes 127.0.0.1
124 // Format: /dns4/<ip-encoded>.<peerID-base36>.libp2p.direct/tcp/<port>/p2p/<peerID>
125 // p2p-forge uses base36 peerIDs in DNS hostnames (lowercase, DNS-safe)
126 libp2pDirectAddr := "/dns4/127-0-0-1." + node1IDBase36 + "." + testDomainSuffix + "/tcp/" + port + "/p2p/" + node1ID
127
128 // This connection should succeed because libp2p.direct is resolved locally
129 // even though DNS.Resolvers points to a broken server
130 result := nodes[0].RunIPFS("swarm", "connect", libp2pDirectAddr)
131
132 // The connection should succeed - local resolution bypasses broken DNS
133 assert.Equal(t, 0, result.ExitCode(),
134 "swarm connect to libp2p.direct should succeed with local resolution. stderr: %s",
135 result.Stderr.String())
136
137 // Verify the connection was actually established
138 result = nodes[0].RunIPFS("swarm", "peers")
139 require.Equal(t, 0, result.ExitCode())
140 assert.Contains(t, result.Stdout.String(), node1ID,
141 "node0 should be connected to node1")
142 })
143 }