From f274bfe19db5a39ffbca177b52db7e8a7eb44537 Mon Sep 17 00:00:00 2001 From: Tanguy Cizain Date: Wed, 18 Aug 2021 09:40:12 +0200 Subject: [PATCH] DNS Addresses handling (#580) * add 'dns' multiaddr protocol * multiaddr: isWire is true for DNS protocols * resolve dns on connect * fix typo * add dns test * update resolveDns error handling * handle multiple dns entries * start of new resolver * working dns resolver * use the DnsResolver * fix json logs * small overhaul * fix dns implem in lp2p * update dnsclient repo * add dns test to testnative * dummy dns server for ut * better mocked * moved resolving to transport * moved mockresolver to libp2p * test resolve in switch test * try multiple txt & track leaks * raise e * catchable error instead of exception * save failed dns server * moved resolve back to dialer * remove nameresolver from dialer --- libp2p.nimble | 1 + libp2p/builders.nim | 13 +- libp2p/multiaddress.nim | 29 ++- libp2p/multicodec.nim | 1 + libp2p/nameresolving/dnsresolver.nim | 160 +++++++++++++++++ libp2p/nameresolving/mockresolver.nim | 46 +++++ libp2p/nameresolving/nameresolver.nim | 148 +++++++++++++++ libp2p/switch.nim | 8 +- tests/helpers.nim | 6 +- tests/testmultiaddress.nim | 6 +- tests/testnameresolve.nim | 247 ++++++++++++++++++++++++++ tests/testnative.nim | 1 + tests/testswitch.nim | 2 + 13 files changed, 655 insertions(+), 13 deletions(-) create mode 100644 libp2p/nameresolving/dnsresolver.nim create mode 100644 libp2p/nameresolving/mockresolver.nim create mode 100644 libp2p/nameresolving/nameresolver.nim create mode 100644 tests/testnameresolve.nim diff --git a/libp2p.nimble b/libp2p.nimble index c565adf63..094bbeb07 100644 --- a/libp2p.nimble +++ b/libp2p.nimble @@ -9,6 +9,7 @@ skipDirs = @["tests", "examples", "Nim", "tools", "scripts", "docs"] requires "nim >= 1.2.0", "nimcrypto >= 0.4.1", + "https://github.com/ba0f3/dnsclient.nim == 0.1.0", "bearssl >= 0.1.4", "chronicles#ba2817f1", "chronos >= 2.5.2", diff --git a/libp2p/builders.nim b/libp2p/builders.nim index 228db943d..f5ec7a705 100644 --- a/libp2p/builders.nim +++ b/libp2p/builders.nim @@ -16,6 +16,7 @@ import muxers/[muxer, mplex/mplex], protocols/[identify, secure/secure, secure/noise], connmanager, upgrademngrs/muxedupgrade, + nameresolving/nameresolver, errors export @@ -45,6 +46,7 @@ type maxConnsPerPeer: int protoVersion: string agentVersion: string + nameResolver: NameResolver proc new*(T: type[SwitchBuilder]): T = @@ -129,6 +131,10 @@ proc withAgentVersion*(b: SwitchBuilder, agentVersion: string): SwitchBuilder = b.agentVersion = agentVersion b +proc withNameResolver*(b: SwitchBuilder, nameResolver: NameResolver): SwitchBuilder = + b.nameResolver = nameResolver + b + proc build*(b: SwitchBuilder): Switch {.raises: [Defect, LPError].} = @@ -184,7 +190,8 @@ proc build*(b: SwitchBuilder): Switch muxers = muxers, secureManagers = secureManagerInstances, connManager = connManager, - ms = ms) + ms = ms, + nameResolver = b.nameResolver) return switch @@ -201,7 +208,8 @@ proc newStandardSwitch*( maxConnections = MaxConnections, maxIn = -1, maxOut = -1, - maxConnsPerPeer = MaxConnectionsPerPeer): Switch + maxConnsPerPeer = MaxConnectionsPerPeer, + nameResolver: NameResolver = nil): Switch {.raises: [Defect, LPError].} = if SecureProtocol.Secio in secureManagers: quit("Secio is deprecated!") # use of secio is unsafe @@ -216,6 +224,7 @@ proc newStandardSwitch*( .withMaxConnsPerPeer(maxConnsPerPeer) .withMplex(inTimeout, outTimeout) .withTcpTransport(transportFlags) + .withNameResolver(nameResolver) .withNoise() if privKey.isSome(): diff --git a/libp2p/multiaddress.nim b/libp2p/multiaddress.nim index ddd4220ca..322a49997 100644 --- a/libp2p/multiaddress.nim +++ b/libp2p/multiaddress.nim @@ -13,7 +13,7 @@ import pkg/chronos import std/[nativesockets, hashes] -import tables, strutils, stew/shims/net +import tables, strutils, sets, stew/shims/net import multicodec, multihash, multibase, transcoder, vbuffer, peerid, protobuf/minprotobuf, errors import stew/[base58, base32, endians2, results] @@ -377,6 +377,10 @@ const mcodec: multiCodec("unix"), kind: Path, size: 0, coder: TranscoderUnix ), + MAProtocol( + mcodec: multiCodec("dns"), kind: Length, size: 0, + coder: TranscoderDNS + ), MAProtocol( mcodec: multiCodec("dns4"), kind: Length, size: 0, coder: TranscoderDNS @@ -403,11 +407,13 @@ const ) ] + DNSANY* = mapEq("dns") DNS4* = mapEq("dns4") DNS6* = mapEq("dns6") + DNSADDR* = mapEq("dnsaddr") IP4* = mapEq("ip4") IP6* = mapEq("ip6") - DNS* = mapOr(mapEq("dnsaddr"), DNS4, DNS6) + DNS* = mapOr(DNSANY, DNS4, DNS6, DNSADDR) IP* = mapOr(IP4, IP6) TCP* = mapOr(mapAnd(DNS, mapEq("tcp")), mapAnd(IP, mapEq("tcp"))) UDP* = mapOr(mapAnd(DNS, mapEq("udp")), mapAnd(IP, mapEq("udp"))) @@ -934,13 +940,24 @@ proc `&=`*(m1: var MultiAddress, m2: MultiAddress) {. m1.append(m2).tryGet() +proc `==`*(m1: var MultiAddress, m2: MultiAddress): bool = + ## Check of two MultiAddress are equal + m1.data == m2.data + proc isWire*(ma: MultiAddress): bool = ## Returns ``true`` if MultiAddress ``ma`` is one of: ## - {IP4}/{TCP, UDP} ## - {IP6}/{TCP, UDP} ## - {UNIX}/{PATH} - var - state = 0 + + var state = 0 + const + wireProtocols = toHashSet([ + multiCodec("ip4"), multiCodec("ip6"), + ]) + wireTransports = toHashSet([ + multiCodec("tcp"), multiCodec("udp") + ]) try: for rpart in ma.items(): if rpart.isErr(): @@ -953,7 +970,7 @@ proc isWire*(ma: MultiAddress): bool = return false let code = rcode.get() - if code == multiCodec("ip4") or code == multiCodec("ip6"): + if code in wireProtocols: inc(state) continue elif code == multiCodec("unix"): @@ -968,7 +985,7 @@ proc isWire*(ma: MultiAddress): bool = return false let code = rcode.get() - if code == multiCodec("tcp") or code == multiCodec("udp"): + if code in wireTransports: inc(state) result = true else: diff --git a/libp2p/multicodec.nim b/libp2p/multicodec.nim index 175379a97..84cb3b8cd 100644 --- a/libp2p/multicodec.nim +++ b/libp2p/multicodec.nim @@ -201,6 +201,7 @@ const MultiCodecList = [ ("p2p-webrtc-direct", 0x0114), # not in multicodec list ("onion", 0x01BC), ("p2p-circuit", 0x0122), + ("dns", 0x35), ("dns4", 0x36), ("dns6", 0x37), ("dnsaddr", 0x38), diff --git a/libp2p/nameresolving/dnsresolver.nim b/libp2p/nameresolving/dnsresolver.nim new file mode 100644 index 000000000..25a956e04 --- /dev/null +++ b/libp2p/nameresolving/dnsresolver.nim @@ -0,0 +1,160 @@ +## Nim-LibP2P +## Copyright (c) 2021 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +{.push raises: [Defect].} + +import + std/[streams, strutils, sets, sequtils], + chronos, chronicles, + dnsclientpkg/[protocol, types] + +import + nameresolver + +logScope: + topics = "libp2p dnsresolver" + +type + DnsResolver* = ref object of NameResolver + nameServers*: seq[TransportAddress] + +proc questionToBuf(address: string, kind: QKind): seq[byte] = + try: + var + header = initHeader() + question = initQuestion(address, kind) + + requestStream = header.toStream() + question.toStream(requestStream) + + let dataLen = requestStream.getPosition() + requestStream.setPosition(0) + + var buf = newSeq[byte](dataLen) + discard requestStream.readData(addr buf[0], dataLen) + return buf + except CatchableError as exc: + info "Failed to created DNS buffer", msg = exc.msg + return newSeq[byte](0) + +proc getDnsResponse( + dnsServer: TransportAddress, + address: string, + kind: QKind): Future[Response] {.async.} = + + var sendBuf = questionToBuf(address, kind) + + if sendBuf.len == 0: + raise newException(ValueError, "Incorrect DNS query") + + let receivedDataFuture = newFuture[void]() + + proc datagramDataReceived(transp: DatagramTransport, + raddr: TransportAddress): Future[void] {.async, closure.} = + receivedDataFuture.complete() + + let sock = + if dnsServer.family == AddressFamily.IPv6: + newDatagramTransport6(datagramDataReceived) + else: + newDatagramTransport(datagramDataReceived) + + try: + await sock.sendTo(dnsServer, addr sendBuf[0], sendBuf.len) + + await receivedDataFuture or sleepAsync(5.seconds) #unix default + + if not receivedDataFuture.finished: + raise newException(IOError, "DNS server timeout") + + var + rawResponse = sock.getMessage() + dataStream = newStringStream() + dataStream.writeData(addr rawResponse[0], rawResponse.len) + dataStream.setPosition(0) + return parseResponse(dataStream) + finally: + await sock.closeWait() + +method resolveIp*( + self: DnsResolver, + address: string, + port: Port, + domain: Domain = Domain.AF_UNSPEC): Future[seq[TransportAddress]] {.async.} = + + trace "Resolving IP using DNS", address, servers = self.nameservers.mapIt($it), domain + for _ in 0 ..< self.nameservers.len: + let server = self.nameservers[0] + var responseFutures: seq[Future[Response]] + if domain == Domain.AF_INET or domain == Domain.AF_UNSPEC: + responseFutures.add(getDnsResponse(server, address, A)) + + if domain == Domain.AF_INET6 or domain == Domain.AF_UNSPEC: + let fut = getDnsResponse(server, address, AAAA) + if server.family == AddressFamily.IPv6: + trace "IPv6 DNS server, puting AAAA records first", server = $server + responseFutures.insert(fut) + else: + responseFutures.add(fut) + + var + resolvedAddresses: OrderedSet[string] + resolveFailed = false + for fut in responseFutures: + try: + let resp = await fut + for answer in resp.answers: + resolvedAddresses.incl(answer.toString()) + except CancelledError as e: + raise e + except ValueError as e: + info "Invalid DNS query", address, error=e.msg + return @[] + except CatchableError as e: + info "Failed to query DNS", address, error=e.msg + resolveFailed = true + break + + if resolveFailed: + self.nameservers.add(self.nameservers[0]) + self.nameservers.delete(0) + continue + + trace "Got IPs from DNS server", resolvedAddresses, server = $server + return resolvedAddresses.toSeq().mapIt(initTAddress(it, port)) + + debug "Failed to resolve address, returning empty set" + return @[] + +method resolveTxt*( + self: DnsResolver, + address: string): Future[seq[string]] {.async.} = + + trace "Resolving TXT using DNS", address, servers = self.nameservers.mapIt($it) + for _ in 0 ..< self.nameservers.len: + let server = self.nameservers[0] + try: + let response = await getDnsResponse(server, address, TXT) + trace "Got TXT response", server = $server, answer=response.answers.mapIt(it.toString()) + return response.answers.mapIt(it.toString()) + except CancelledError as e: + raise e + except CatchableError as e: + info "Failed to query DNS", address, error=e.msg + self.nameservers.add(self.nameservers[0]) + self.nameservers.delete(0) + continue + + debug "Failed to resolve TXT, returning empty set" + return @[] + +proc new*( + T: typedesc[DnsResolver], + nameServers: seq[TransportAddress]): T = + T(nameServers: nameServers) diff --git a/libp2p/nameresolving/mockresolver.nim b/libp2p/nameresolving/mockresolver.nim new file mode 100644 index 000000000..9a5368f32 --- /dev/null +++ b/libp2p/nameresolving/mockresolver.nim @@ -0,0 +1,46 @@ +## Nim-LibP2P +## Copyright (c) 2021 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +{.push raises: [Defect].} + +import + std/[streams, strutils, tables], + chronos, chronicles + +import nameresolver + +export tables + +logScope: + topics = "libp2p mockresolver" + +type MockResolver* = ref object of NameResolver + txtResponses*: Table[string, seq[string]] + # key: address, isipv6? + ipResponses*: Table[(string, bool), seq[string]] + +method resolveIp*( + self: MockResolver, + address: string, + port: Port, + domain: Domain = Domain.AF_UNSPEC): Future[seq[TransportAddress]] {.async.} = + if domain == Domain.AF_INET or domain == Domain.AF_UNSPEC: + for resp in self.ipResponses.getOrDefault((address, false)): + result.add(initTAddress(resp, port)) + + if domain == Domain.AF_INET6 or domain == Domain.AF_UNSPEC: + for resp in self.ipResponses.getOrDefault((address, true)): + result.add(initTAddress(resp, port)) + +method resolveTxt*( + self: MockResolver, + address: string): Future[seq[string]] {.async.} = + return self.txtResponses.getOrDefault(address) + +proc new*(T: typedesc[MockResolver]): T = T() diff --git a/libp2p/nameresolving/nameresolver.nim b/libp2p/nameresolving/nameresolver.nim new file mode 100644 index 000000000..38efcde22 --- /dev/null +++ b/libp2p/nameresolving/nameresolver.nim @@ -0,0 +1,148 @@ +## Nim-LibP2P +## Copyright (c) 2021 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +{.push raises: [Defect].} + +import std/[sugar, sets, sequtils, strutils] +import + chronos, + chronicles, + stew/[endians2, byteutils] +import ".."/[multiaddress, multicodec] + +logScope: + topics = "libp2p nameresolver" + +type + NameResolver* = ref object of RootObj + +method resolveTxt*( + self: NameResolver, + address: string): Future[seq[string]] {.async, base.} = + ## Get TXT record + ## + + doAssert(false, "Not implemented!") + +method resolveIp*( + self: NameResolver, + address: string, + port: Port, + domain: Domain = Domain.AF_UNSPEC): Future[seq[TransportAddress]] {.async, base.} = + ## Resolve the specified address + ## + + doAssert(false, "Not implemented!") + +proc getHostname(ma: MultiAddress): string = + var dnsbuf = newSeq[byte](256) + + let dnsLen = ma[0].get().protoArgument(dnsbuf).get() + dnsbuf.setLen(dnsLen) + return string.fromBytes(dnsbuf) + +proc resolveDnsAddress( + self: NameResolver, + ma: MultiAddress, + domain: Domain = Domain.AF_UNSPEC, + prefix = ""): Future[seq[MultiAddress]] + {.async, raises: [Defect, MaError, TransportAddressError].} = + #Resolve a single address + var pbuf: array[2, byte] + + var dnsval = getHostname(ma) + + if ma[1].tryGet().protoArgument(pbuf).tryGet() == 0: + raise newException(MaError, "Incorrect port number") + let + port = Port(fromBytesBE(uint16, pbuf)) + resolvedAddresses = await self.resolveIp(prefix & dnsval, port, domain) + + var addressSuffix = ma + return collect(newSeqOfCap(4)): + for address in resolvedAddresses: + var createdAddress = MultiAddress.init(address).tryGet()[0].tryGet() + for part in ma: + if DNS.match(part.get()): continue + createdAddress &= part.tryGet() + createdAddress + +func matchDnsSuffix(m1, m2: MultiAddress): MaResult[bool] = + for partMaybe in m1: + let part = ?partMaybe + if DNS.match(part): continue + let entryProt = ?m2[?part.protoCode()] + if entryProt != part: + return ok(false) + return ok(true) + +proc resolveDnsAddr( + self: NameResolver, + ma: MultiAddress, + depth: int = 0): Future[seq[MultiAddress]] + {.async.} = + + trace "Resolving dnsaddr", ma + if depth > 6: + info "Stopping DNSADDR recursion, probably malicious", ma + return @[] + + var dnsval = getHostname(ma) + + let txt = await self.resolveTxt("_dnsaddr." & dnsval) + + trace "txt entries", txt + + var result: seq[MultiAddress] + for entry in txt: + if not entry.startsWith("dnsaddr="): continue + let entryValue = MultiAddress.init(entry[8..^1]).tryGet() + + if not matchDnsSuffix(ma, entryValue).tryGet(): continue + + # The spec is not clear wheter only DNSADDR can be recursived + # or any DNS addr. Only handling DNSADDR because it's simpler + # to avoid infinite recursion + if DNSADDR.matchPartial(entryValue): + let resolved = await self.resolveDnsAddr(entryValue, depth + 1) + for r in resolved: + result.add(r) + else: + result.add(entryValue) + + if result.len == 0: + debug "Failed to resolve any DNSADDR", ma + return @[ma] + return result + + +proc resolveMAddresses*( + self: NameResolver, + addrs: seq[MultiAddress]): Future[seq[MultiAddress]] {.async.} = + var res = initOrderedSet[MultiAddress]() + + for address in addrs: + if not DNS.matchPartial(address): + res.incl(address) + else: + let code = address[0].get().protoCode().get() + let seq = case code: + of multiCodec("dns"): + await self.resolveDnsAddress(address) + of multiCodec("dns4"): + await self.resolveDnsAddress(address, Domain.AF_INET) + of multiCodec("dns6"): + await self.resolveDnsAddress(address, Domain.AF_INET6) + of multiCodec("dnsaddr"): + await self.resolveDnsAddr(address) + else: + @[address] + for ad in seq: + res.incl(ad) + return res.toSeq diff --git a/libp2p/switch.nim b/libp2p/switch.nim index 66bea4ef8..a6233e921 100644 --- a/libp2p/switch.nim +++ b/libp2p/switch.nim @@ -32,6 +32,7 @@ import stream/connection, muxers/muxer, utils/semaphore, connmanager, + nameresolving/nameresolver, peerid, peerstore, errors, @@ -62,6 +63,7 @@ type acceptFuts: seq[Future[void]] dialer*: Dial peerStore*: PeerStore + nameResolver*: NameResolver proc addConnEventHandler*(s: Switch, handler: ConnEventHandler, @@ -256,7 +258,8 @@ proc newSwitch*(peerInfo: PeerInfo, muxers: Table[string, MuxerProvider], secureManagers: openarray[Secure] = [], connManager: ConnManager, - ms: MultistreamSelect): Switch + ms: MultistreamSelect, + nameResolver: NameResolver = nil): Switch {.raises: [Defect, LPError].} = if secureManagers.len == 0: raise newException(LPError, "Provide at least one secure manager") @@ -267,7 +270,8 @@ proc newSwitch*(peerInfo: PeerInfo, transports: transports, connManager: connManager, peerStore: PeerStore.new(), - dialer: Dialer.new(peerInfo, connManager, transports, ms)) + dialer: Dialer.new(peerInfo, connManager, transports, ms), + nameResolver: nameResolver) switch.mount(identity) return switch diff --git a/tests/helpers.nim b/tests/helpers.nim index 548b15a96..58f9cc66c 100644 --- a/tests/helpers.nim +++ b/tests/helpers.nim @@ -16,6 +16,7 @@ export asyncunit const StreamTransportTrackerName = "stream.transport" StreamServerTrackerName = "stream.server" + DgramTransportTrackerName = "datagram.transport" trackerNames = [ LPStreamTrackerName, @@ -25,8 +26,9 @@ const BufferStreamTrackerName, TcpTransportTrackerName, StreamTransportTrackerName, - ChronosStreamTrackerName, - StreamServerTrackerName + StreamServerTrackerName, + DgramTransportTrackerName, + ChronosStreamTrackerName ] iterator testTrackers*(extras: openArray[string] = []): TrackerBase = diff --git a/tests/testmultiaddress.nim b/tests/testmultiaddress.nim index a1f3de106..eaa5e3bae 100644 --- a/tests/testmultiaddress.nim +++ b/tests/testmultiaddress.nim @@ -53,6 +53,10 @@ const "/ip4/1.2.3.4/tcp/80/unix/a/b/c/d/e/f", "/ip4/127.0.0.1/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234/unix/stdio", "/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234/unix/stdio", + "/dns/example.io/udp/65535", + "/dns4/example.io/udp/65535", + "/dns6/example.io/udp/65535", + "/dnsaddr/example.io/udp/65535", ] FailureVectors = [ @@ -257,7 +261,7 @@ const ] ), PatternVector(pattern: DNS, - good: @["/dnsaddr/example.io", "/dns4/example.io", "/dns6/example.io"], + good: @["/dns/example.io", "/dnsaddr/example.io", "/dns4/example.io", "/dns6/example.io"], bad: @["/ip4/127.0.0.1"], ), PatternVector(pattern: WebRTCDirect, diff --git a/tests/testnameresolve.nim b/tests/testnameresolve.nim new file mode 100644 index 000000000..676bf7cf1 --- /dev/null +++ b/tests/testnameresolve.nim @@ -0,0 +1,247 @@ +{.used.} + +import std/[streams, strutils, sets, sequtils, tables, algorithm] +import chronos, stew/byteutils +import ../libp2p/[stream/connection, + transports/transport, + transports/tcptransport, + upgrademngrs/upgrade, + multiaddress, + errors, + nameresolving/nameresolver, + nameresolving/dnsresolver, + nameresolving/mockresolver, + wire] + +import ./helpers +# +#Cloudflare +const fallbackDnsServers = @[ + initTAddress("1.1.1.1:53"), + initTAddress("1.0.0.1:53"), + initTAddress("[2606:4700:4700::1111]:53") +] + +const unixPlatform = defined(linux) or defined(solaris) or + defined(macosx) or defined(freebsd) or + defined(netbsd) or defined(openbsd) or + defined(dragonfly) + + +proc guessOsNameServers(): seq[TransportAddress] = + when unixPlatform: + var resultSeq = newSeqOfCap[TransportAddress](3) + try: + for l in lines("/etc/resolv.conf"): + let lineParsed = l.strip().split(seps = Whitespace + {'%'}, maxsplit = 2) + if lineParsed.len < 2: continue + if lineParsed[0].startsWith('#'): continue + + if lineParsed[0] == "nameserver": + resultSeq.add(initTAddress(lineParsed[1], Port(53))) + + if resultSeq.len > 2: break #3 nameserver max on linux + except Exception as e: + echo "Failed to get unix nameservers ", e.msg + finally: + if resultSeq.len > 0: + return resultSeq + return fallbackDnsServers + elif defined(windows): + #TODO + return fallbackDnsServers + else: + return fallbackDnsServers + + +suite "Name resolving": + suite "Generic Resolving": + var resolver {.threadvar.}: MockResolver + + proc testOne(input: string, output: seq[Multiaddress]): bool = + let resolved = waitFor resolver.resolveMAddresses(@[Multiaddress.init(input).tryGet()]) + if resolved != output: + echo "Expected ", output + echo "Got ", resolved + return false + return true + + proc testOne(input: string, output: seq[string]): bool = + testOne(input, output.mapIt(Multiaddress.init(it).tryGet())) + + proc testOne(input, output: string): bool = + testOne(input, @[Multiaddress.init(output).tryGet()]) + + asyncSetup: + resolver = MockResolver.new() + + asyncTest "test multi address dns resolve": + resolver.ipResponses[("localhost", false)] = @["127.0.0.1"] + resolver.ipResponses[("localhost", true)] = @["::1"] + + check testOne("/dns/localhost/udp/0", @["/ip4/127.0.0.1/udp/0", "/ip6/::1/udp/0"]) + check testOne("/dns4/localhost/tcp/0", "/ip4/127.0.0.1/tcp/0") + check testOne("/dns6/localhost/tcp/0", "/ip6/::1/tcp/0") + check testOne("/dns6/localhost/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", "/ip6/::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN") + + asyncTest "test non dns resolve": + resolver.ipResponses[("localhost", false)] = @["127.0.0.1"] + resolver.ipResponses[("localhost", true)] = @["::1"] + + check testOne("/ip6/::1/tcp/0", "/ip6/::1/tcp/0") + + asyncTest "test multiple resolve": + resolver.ipResponses[("localhost", false)] = @["127.0.0.1"] + resolver.ipResponses[("localhost", true)] = @["::1"] + + let resolved = waitFor resolver.resolveMAddresses(@[ + Multiaddress.init("/dns/localhost/udp/0").tryGet(), + Multiaddress.init("/dns4/localhost/udp/0").tryGet(), + Multiaddress.init("/dns6/localhost/udp/0").tryGet(), + ]) + + check resolved == @[Multiaddress.init("/ip4/127.0.0.1/udp/0").tryGet(), Multiaddress.init("/ip6/::1/udp/0").tryGet()] + + asyncTest "dnsaddr recursive test": + resolver.txtResponses["_dnsaddr.bootstrap.libp2p.io"] = @[ + "dnsaddr=/dnsaddr/sjc-1.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "dnsaddr=/dnsaddr/ams-2.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb" + ] + + resolver.txtResponses["_dnsaddr.sjc-1.bootstrap.libp2p.io"] = @[ + "dnsaddr=/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "dnsaddr=/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN" + ] + + resolver.txtResponses["_dnsaddr.ams-2.bootstrap.libp2p.io"] = @[ + "dnsaddr=/ip4/147.75.83.83/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", + "dnsaddr=/ip6/2604:1380:2000:7a00::1/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb" + ] + + check testOne("/dnsaddr/bootstrap.libp2p.io/", @[ + "/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/ip4/147.75.83.83/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", + "/ip6/2604:1380:2000:7a00::1/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", + ]) + + asyncTest "dnsaddr suffix matching test": + resolver.txtResponses["_dnsaddr.bootstrap.libp2p.io"] = @[ + "dnsaddr=/dnsaddr/ams-2.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", + "dnsaddr=/dnsaddr/sjc-1.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "dnsaddr=/dnsaddr/nrt-1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt", + "dnsaddr=/dnsaddr/ewr-1.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + ] + + resolver.txtResponses["_dnsaddr.sjc-1.bootstrap.libp2p.io"] = @[ + "dnsaddr=/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "dnsaddr=/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + ] + + resolver.txtResponses["_dnsaddr.ams-1.bootstrap.libp2p.io"] = @[ + "dnsaddr=/ip4/147.75.69.143/tcp/4001/p2p/shouldbefiltered", + "dnsaddr=/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/shouldbefiltered", + ] + + check testOne("/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", @[ + "/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + ]) + + asyncTest "dnsaddr infinite recursion": + resolver.txtResponses["_dnsaddr.bootstrap.libp2p.io"] = @["dnsaddr=/dnsaddr/bootstrap.libp2p.io"] + + check testOne("/dnsaddr/bootstrap.libp2p.io/", "/dnsaddr/bootstrap.libp2p.io/") + + suite "DNS Resolving": + teardown: + checkTrackers() + + asyncTest "test manual dns ip resolve": + ## DNS mock server + proc clientMark1(transp: DatagramTransport, + raddr: TransportAddress): Future[void] {.async.} = + var msg = transp.getMessage() + let + resp = if msg[24] == 1: #AAAA or A + "\xae\xbf\x81\x80\x00\x01\x00\x03\x00\x00\x00\x00\x06\x73\x74\x61" & + "\x74\x75\x73\x02\x69\x6d\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00" & + "\x01\x00\x00\x00\x4f\x00\x04\x68\x16\x18\xb5\xc0\x0c\x00\x01\x00" & + "\x01\x00\x00\x00\x4f\x00\x04\xac\x43\x0a\xa1\xc0\x0c\x00\x01\x00" & + "\x01\x00\x00\x00\x4f\x00\x04\x68\x16\x19\xb5" + else: + "\xe8\xc5\x81\x80\x00\x01\x00\x03\x00\x00\x00\x00\x06\x73\x74\x61" & + "\x74\x75\x73\x02\x69\x6d\x00\x00\x1c\x00\x01\xc0\x0c\x00\x1c\x00" & + "\x01\x00\x00\x00\x4f\x00\x10\x26\x06\x47\x00\x00\x10\x00\x00\x00" & + "\x00\x00\x00\x68\x16\x19\xb5\xc0\x0c\x00\x1c\x00\x01\x00\x00\x00" & + "\x4f\x00\x10\x26\x06\x47\x00\x00\x10\x00\x00\x00\x00\x00\x00\x68" & + "\x16\x18\xb5\xc0\x0c\x00\x1c\x00\x01\x00\x00\x00\x4f\x00\x10\x26" & + "\x06\x47\x00\x00\x10\x00\x00\x00\x00\x00\x00\xac\x43\x0a\xa1" + await transp.sendTo(raddr, resp) + + let server = newDatagramTransport(clientMark1) + + # The test + var dnsresolver = DnsResolver.new(@[server.localAddress]) + + check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_UNSPEC)) == + mapIt( + @["104.22.24.181:0", "172.67.10.161:0", "104.22.25.181:0", + "[2606:4700:10::6816:19b5]:0", "[2606:4700:10::6816:18b5]:0", "[2606:4700:10::ac43:aa1]:0" + ], initTAddress(it)) + check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_INET)) == + mapIt(@["104.22.24.181:0", "172.67.10.161:0", "104.22.25.181:0"], initTAddress(it)) + check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_INET6)) == + mapIt(@["[2606:4700:10::6816:19b5]:0", "[2606:4700:10::6816:18b5]:0", "[2606:4700:10::ac43:aa1]:0"], initTAddress(it)) + + await server.closeWait() + + asyncTest "test unresponsive dns server": + var unresponsiveTentatives = 0 + ## DNS mock server + proc clientMark1(transp: DatagramTransport, + raddr: TransportAddress): Future[void] {.async.} = + unresponsiveTentatives.inc() + check unresponsiveTentatives == 1 + + proc clientMark2(transp: DatagramTransport, + raddr: TransportAddress): Future[void] {.async.} = + var msg = transp.getMessage() + let resp = + "\xae\xbf\x81\x80\x00\x01\x00\x03\x00\x00\x00\x00\x06\x73\x74\x61" & + "\x74\x75\x73\x02\x69\x6d\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00" & + "\x01\x00\x00\x00\x4f\x00\x04\x68\x16\x18\xb5\xc0\x0c\x00\x01\x00" & + "\x01\x00\x00\x00\x4f\x00\x04\xac\x43\x0a\xa1\xc0\x0c\x00\x01\x00" & + "\x01\x00\x00\x00\x4f\x00\x04\x68\x16\x19\xb5" + await transp.sendTo(raddr, resp) + + let + unresponsiveServer = newDatagramTransport(clientMark1) + server = newDatagramTransport(clientMark2) + + # The test + var dnsresolver = DnsResolver.new(@[unresponsiveServer.localAddress, server.localAddress]) + + check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_INET)) == + mapIt(@["104.22.24.181:0", "172.67.10.161:0", "104.22.25.181:0"], initTAddress(it)) + + check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_INET)) == + mapIt(@["104.22.24.181:0", "172.67.10.161:0", "104.22.25.181:0"], initTAddress(it)) + + await server.closeWait() + await unresponsiveServer.closeWait() + + asyncTest "inexisting domain resolving": + var dnsresolver = DnsResolver.new(guessOsNameServers()) + let invalid = await dnsresolver.resolveIp("thisdomain.doesnot.exist", 0.Port) + check invalid.len == 0 + + asyncTest "wrong domain resolving": + var dnsresolver = DnsResolver.new(guessOsNameServers()) + let invalid = await dnsresolver.resolveIp("", 0.Port) + check invalid.len == 0 + + asyncTest "unreachable dns server": + var dnsresolver = DnsResolver.new(@[initTAddress("172.67.10.161:53")]) + let invalid = await dnsresolver.resolveIp("google.fr", 0.Port) + check invalid.len == 0 diff --git a/tests/testnative.nim b/tests/testnative.nim index f3a682c15..0f4a67460 100644 --- a/tests/testnative.nim +++ b/tests/testnative.nim @@ -17,6 +17,7 @@ import testmultibase, testpeerid import testtcptransport, + testnameresolve, testwstransport, testmultistream, testbufferstream, diff --git a/tests/testswitch.nim b/tests/testswitch.nim index 5802a4597..b7c68eadc 100644 --- a/tests/testswitch.nim +++ b/tests/testswitch.nim @@ -18,6 +18,8 @@ import ../libp2p/[errors, muxers/muxer, muxers/mplex/lpchannel, stream/lpstream, + nameresolving/nameresolver, + nameresolving/mockresolver, stream/chronosstream, transports/tcptransport] import ./helpers