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