diff --git a/libp2p/nameresolving/dnsresolver.nim b/libp2p/nameresolving/dnsresolver.nim index 300ec11708..699ff1f526 100644 --- a/libp2p/nameresolving/dnsresolver.nim +++ b/libp2p/nameresolving/dnsresolver.nim @@ -1,5 +1,5 @@ # Nim-LibP2P -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * MIT license ([LICENSE-MIT](LICENSE-MIT)) @@ -10,7 +10,7 @@ {.push raises: [].} import - std/[streams, strutils, sets, sequtils], + std/[streams, sets, sequtils], chronos, chronicles, stew/byteutils, @@ -39,24 +39,32 @@ proc questionToBuf(address: string, kind: QKind): seq[byte] = var buf = newSeq[byte](dataLen) discard requestStream.readData(addr buf[0], dataLen) - return buf - except CatchableError as exc: + buf + except IOError as exc: info "Failed to created DNS buffer", description = exc.msg - return newSeq[byte](0) + newSeq[byte](0) + except OSError as exc: + info "Failed to created DNS buffer", description = exc.msg + newSeq[byte](0) + except ValueError as exc: + info "Failed to created DNS buffer", description = exc.msg + newSeq[byte](0) proc getDnsResponse( dnsServer: TransportAddress, address: string, kind: QKind -): Future[Response] {.async.} = +): Future[Response] {. + async: (raises: [CancelledError, IOError, OSError, TransportError, ValueError]) +.} = var sendBuf = questionToBuf(address, kind) if sendBuf.len == 0: raise newException(ValueError, "Incorrect DNS query") - let receivedDataFuture = newFuture[void]() + let receivedDataFuture = Future[void].Raising([CancelledError]).init() proc datagramDataReceived( transp: DatagramTransport, raddr: TransportAddress - ): Future[void] {.async, closure.} = + ): Future[void] {.async: (raises: []), closure.} = receivedDataFuture.complete() let sock = @@ -68,27 +76,41 @@ proc getDnsResponse( try: await sock.sendTo(dnsServer, addr sendBuf[0], sendBuf.len) - await receivedDataFuture or sleepAsync(5.seconds) #unix default - - if not receivedDataFuture.finished: + try: + await receivedDataFuture.wait(5.seconds) #unix default + except AsyncTimeoutError: raise newException(IOError, "DNS server timeout") let rawResponse = sock.getMessage() - # parseResponse can has a raises: [Exception, ..] because of - # https://github.com/nim-lang/Nim/commit/035134de429b5d99c5607c5fae912762bebb6008 - # it can't actually raise though - return exceptionToAssert: + try: parseResponse(string.fromBytes(rawResponse)) + except IOError as exc: + raise exc + except OSError as exc: + raise exc + except ValueError as exc: + raise exc + except Exception as exc: + # Nim 1.6: parseResponse can has a raises: [Exception, ..] because of + # https://github.com/nim-lang/Nim/commit/035134de429b5d99c5607c5fae912762bebb6008 + # it can't actually raise though + raiseAssert exc.msg finally: await sock.closeWait() method resolveIp*( self: DnsResolver, address: string, port: Port, domain: Domain = Domain.AF_UNSPEC -): Future[seq[TransportAddress]] {.async.} = +): Future[seq[TransportAddress]] {. + async: (raises: [CancelledError, TransportAddressError]) +.} = 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]] + var responseFutures: seq[ + Future[Response].Raising( + [CancelledError, IOError, OSError, TransportError, ValueError] + ) + ] if domain == Domain.AF_INET or domain == Domain.AF_UNSPEC: responseFutures.add(getDnsResponse(server, address, A)) @@ -103,23 +125,32 @@ method resolveIp*( var resolvedAddresses: OrderedSet[string] resolveFailed = false + template handleFail(e): untyped = + info "Failed to query DNS", address, error = e.msg + resolveFailed = true + break + for fut in responseFutures: try: let resp = await fut for answer in resp.answers: - # toString can has a raises: [Exception, ..] because of - # https://github.com/nim-lang/Nim/commit/035134de429b5d99c5607c5fae912762bebb6008 - # it can't actually raise though - resolvedAddresses.incl(exceptionToAssert(answer.toString())) + 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 + except IOError as e: + handleFail(e) + except OSError as e: + handleFail(e) + except TransportError as e: + handleFail(e) + except Exception as e: + # Nim 1.6: answer.toString can has a raises: [Exception, ..] because of + # https://github.com/nim-lang/Nim/commit/035134de429b5d99c5607c5fae912762bebb6008 + # it can't actually raise though + raiseAssert e.msg if resolveFailed: self.nameServers.add(self.nameServers[0]) @@ -132,27 +163,39 @@ method resolveIp*( debug "Failed to resolve address, returning empty set" return @[] -method resolveTxt*(self: DnsResolver, address: string): Future[seq[string]] {.async.} = +method resolveTxt*( + self: DnsResolver, address: string +): Future[seq[string]] {.async: (raises: [CancelledError]).} = trace "Resolving TXT using DNS", address, servers = self.nameServers.mapIt($it) for _ in 0 ..< self.nameServers.len: let server = self.nameServers[0] - try: - # toString can has a raises: [Exception, ..] because of - # https://github.com/nim-lang/Nim/commit/035134de429b5d99c5607c5fae912762bebb6008 - # it can't actually raise though - let response = await getDnsResponse(server, address, TXT) - return exceptionToAssert: - trace "Got TXT response", - server = $server, answer = response.answers.mapIt(it.toString()) - response.answers.mapIt(it.toString()) - except CancelledError as e: - raise e - except CatchableError as e: + template handleFail(e): untyped = info "Failed to query DNS", address, error = e.msg self.nameServers.add(self.nameServers[0]) self.nameServers.delete(0) continue + 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 IOError as e: + handleFail(e) + except OSError as e: + handleFail(e) + except TransportError as e: + handleFail(e) + except ValueError as e: + handleFail(e) + except Exception as e: + # Nim 1.6: toString can has a raises: [Exception, ..] because of + # https://github.com/nim-lang/Nim/commit/035134de429b5d99c5607c5fae912762bebb6008 + # it can't actually raise though + raiseAssert e.msg + debug "Failed to resolve TXT, returning empty set" return @[] diff --git a/libp2p/nameresolving/mockresolver.nim b/libp2p/nameresolving/mockresolver.nim index 5a63daf771..418678a090 100644 --- a/libp2p/nameresolving/mockresolver.nim +++ b/libp2p/nameresolving/mockresolver.nim @@ -1,5 +1,5 @@ # Nim-LibP2P -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * MIT license ([LICENSE-MIT](LICENSE-MIT)) @@ -25,17 +25,25 @@ type MockResolver* = ref object of NameResolver method resolveIp*( self: MockResolver, address: string, port: Port, domain: Domain = Domain.AF_UNSPEC -): Future[seq[TransportAddress]] {.async.} = +): Future[seq[TransportAddress]] {. + async: (raises: [CancelledError, TransportAddressError]) +.} = + var res: seq[TransportAddress] + if domain == Domain.AF_INET or domain == Domain.AF_UNSPEC: for resp in self.ipResponses.getOrDefault((address, false)): - result.add(initTAddress(resp, port)) + res.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)) + res.add(initTAddress(resp, port)) + + res -method resolveTxt*(self: MockResolver, address: string): Future[seq[string]] {.async.} = - return self.txtResponses.getOrDefault(address) +method resolveTxt*( + self: MockResolver, address: string +): Future[seq[string]] {.async: (raises: [CancelledError]).} = + self.txtResponses.getOrDefault(address) proc new*(T: typedesc[MockResolver]): T = T() diff --git a/libp2p/nameresolving/nameresolver.nim b/libp2p/nameresolving/nameresolver.nim index 90c44f77f9..e6d0320da5 100644 --- a/libp2p/nameresolving/nameresolver.nim +++ b/libp2p/nameresolving/nameresolver.nim @@ -1,5 +1,5 @@ # Nim-LibP2P -# Copyright (c) 2023 Status Research & Development GmbH +# Copyright (c) 2023-2024 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * MIT license ([LICENSE-MIT](LICENSE-MIT)) @@ -9,7 +9,7 @@ {.push raises: [].} -import std/[sugar, sets, sequtils, strutils] +import std/[sets, sequtils, strutils] import chronos, chronicles, stew/endians2 import ".."/[multiaddress, multicodec] @@ -20,19 +20,17 @@ type NameResolver* = ref object of RootObj method resolveTxt*( self: NameResolver, address: string -): Future[seq[string]] {.async, base.} = +): Future[seq[string]] {.async: (raises: [CancelledError]), base.} = ## Get TXT record - ## - - doAssert(false, "Not implemented!") + raiseAssert "Not implemented!" method resolveIp*( self: NameResolver, address: string, port: Port, domain: Domain = Domain.AF_UNSPEC -): Future[seq[TransportAddress]] {.async, base.} = +): Future[seq[TransportAddress]] {. + async: (raises: [CancelledError, TransportAddressError]), base +.} = ## Resolve the specified address - ## - - doAssert(false, "Not implemented!") + raiseAssert "Not implemented!" proc getHostname*(ma: MultiAddress): string = let @@ -46,30 +44,40 @@ proc getHostname*(ma: MultiAddress): string = proc resolveOneAddress( self: NameResolver, ma: MultiAddress, domain: Domain = Domain.AF_UNSPEC, prefix = "" -): Future[seq[MultiAddress]] {.async.} = - #Resolve a single address +): Future[seq[MultiAddress]] {. + async: (raises: [CancelledError, MaError, TransportAddressError]) +.} = + # Resolve a single address + let portPart = ma[1].valueOr: + raise maErr error var pbuf: array[2, byte] - - var dnsval = getHostname(ma) - - if ma[1].tryGet().protoArgument(pbuf).tryGet() == 0: - raise newException(MaError, "Incorrect port number") + let plen = portPart.protoArgument(pbuf).valueOr: + raise maErr error + if plen == 0: + raise maErr "Incorrect port number" let port = Port(fromBytesBE(uint16, pbuf)) + dnsval = getHostname(ma) resolvedAddresses = await self.resolveIp(prefix & dnsval, port, domain) - return collect(newSeqOfCap(4)): - for address in resolvedAddresses: - var createdAddress = MultiAddress.init(address).tryGet()[0].tryGet() - for part in ma: - if DNS.match(part.tryGet()): - continue - createdAddress &= part.tryGet() - createdAddress + resolvedAddresses.mapIt: + let address = MultiAddress.init(it).valueOr: + raise maErr error + var createdAddress = address[0].valueOr: + raise maErr error + for part in ma: + let part = part.valueOr: + raise maErr error + if DNS.match(part): + continue + createdAddress &= part + createdAddress proc resolveDnsAddr*( self: NameResolver, ma: MultiAddress, depth: int = 0 -): Future[seq[MultiAddress]] {.async.} = +): Future[seq[MultiAddress]] {. + async: (raises: [CancelledError, MaError, TransportAddressError]) +.} = if not DNSADDR.matchPartial(ma): return @[ma] @@ -78,54 +86,67 @@ proc resolveDnsAddr*( info "Stopping DNSADDR recursion, probably malicious", ma return @[] - var dnsval = getHostname(ma) - - let txt = await self.resolveTxt("_dnsaddr." & dnsval) + let + dnsval = getHostname(ma) + txt = await self.resolveTxt("_dnsaddr." & dnsval) trace "txt entries", txt - var result: seq[MultiAddress] + const codec = multiCodec("p2p") + let maCodec = block: + let hasCodec = ma.contains(codec).valueOr: + raise maErr error + if hasCodec: + ma[codec] + else: + (static(default(MaResult[MultiAddress]))) + + var res: seq[MultiAddress] for entry in txt: if not entry.startsWith("dnsaddr="): continue - let entryValue = MultiAddress.init(entry[8 ..^ 1]).tryGet() - - if entryValue.contains(multiCodec("p2p")).tryGet() and - ma.contains(multiCodec("p2p")).tryGet(): - if entryValue[multiCodec("p2p")] != ma[multiCodec("p2p")]: - continue + let + entryValue = MultiAddress.init(entry[8 ..^ 1]).valueOr: + raise maErr error + entryHasCodec = entryValue.contains(multiCodec("p2p")).valueOr: + raise maErr error + if entryHasCodec and maCodec.isOk and entryValue[codec] != maCodec: + continue let resolved = await self.resolveDnsAddr(entryValue, depth + 1) for r in resolved: - result.add(r) + res.add(r) - if result.len == 0: + if res.len == 0: debug "Failed to resolve a DNSADDR", ma - return @[] - return result + res proc resolveMAddress*( self: NameResolver, address: MultiAddress -): Future[seq[MultiAddress]] {.async.} = +): Future[seq[MultiAddress]] {. + async: (raises: [CancelledError, MaError, TransportAddressError]) +.} = var res = initOrderedSet[MultiAddress]() - if not DNS.matchPartial(address): res.incl(address) else: - let code = address[0].tryGet().protoCode().tryGet() - let seq = - case code - of multiCodec("dns"): - await self.resolveOneAddress(address) - of multiCodec("dns4"): - await self.resolveOneAddress(address, Domain.AF_INET) - of multiCodec("dns6"): - await self.resolveOneAddress(address, Domain.AF_INET6) - of multiCodec("dnsaddr"): - await self.resolveDnsAddr(address) - else: - assert false - @[address] - for ad in seq: + let + firstPart = address[0].valueOr: + raise maErr error + code = firstPart.protoCode().valueOr: + raise maErr error + ads = + case code + of multiCodec("dns"): + await self.resolveOneAddress(address) + of multiCodec("dns4"): + await self.resolveOneAddress(address, Domain.AF_INET) + of multiCodec("dns6"): + await self.resolveOneAddress(address, Domain.AF_INET6) + of multiCodec("dnsaddr"): + await self.resolveDnsAddr(address) + else: + raise maErr("Unsupported codec " & $code) + for ad in ads: res.incl(ad) - return res.toSeq + res.toSeq