feat(nameresolving): Add {.async: (raises).} annotations (#1214)

Modernize `nameresolving` modules to track `{.async: (raises).}`.

---------

Co-authored-by: kaiserd <1684595+kaiserd@users.noreply.github.com>
Co-authored-by: richΛrd <info@richardramos.me>
This commit is contained in:
Etan Kissling 2024-12-09 12:43:21 +01:00 committed by GitHub
parent b7e0df127f
commit dd2c74d413
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 174 additions and 102 deletions

View File

@ -1,5 +1,5 @@
# Nim-LibP2P # Nim-LibP2P
# Copyright (c) 2023 Status Research & Development GmbH # Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of # Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT)) # * MIT license ([LICENSE-MIT](LICENSE-MIT))
@ -10,7 +10,7 @@
{.push raises: [].} {.push raises: [].}
import import
std/[streams, strutils, sets, sequtils], std/[streams, sets, sequtils],
chronos, chronos,
chronicles, chronicles,
stew/byteutils, stew/byteutils,
@ -39,24 +39,32 @@ proc questionToBuf(address: string, kind: QKind): seq[byte] =
var buf = newSeq[byte](dataLen) var buf = newSeq[byte](dataLen)
discard requestStream.readData(addr buf[0], dataLen) discard requestStream.readData(addr buf[0], dataLen)
return buf buf
except CatchableError as exc: except IOError as exc:
info "Failed to created DNS buffer", description = exc.msg 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( proc getDnsResponse(
dnsServer: TransportAddress, address: string, kind: QKind dnsServer: TransportAddress, address: string, kind: QKind
): Future[Response] {.async.} = ): Future[Response] {.
async: (raises: [CancelledError, IOError, OSError, TransportError, ValueError])
.} =
var sendBuf = questionToBuf(address, kind) var sendBuf = questionToBuf(address, kind)
if sendBuf.len == 0: if sendBuf.len == 0:
raise newException(ValueError, "Incorrect DNS query") raise newException(ValueError, "Incorrect DNS query")
let receivedDataFuture = newFuture[void]() let receivedDataFuture = Future[void].Raising([CancelledError]).init()
proc datagramDataReceived( proc datagramDataReceived(
transp: DatagramTransport, raddr: TransportAddress transp: DatagramTransport, raddr: TransportAddress
): Future[void] {.async, closure.} = ): Future[void] {.async: (raises: []), closure.} =
receivedDataFuture.complete() receivedDataFuture.complete()
let sock = let sock =
@ -68,27 +76,41 @@ proc getDnsResponse(
try: try:
await sock.sendTo(dnsServer, addr sendBuf[0], sendBuf.len) await sock.sendTo(dnsServer, addr sendBuf[0], sendBuf.len)
await receivedDataFuture or sleepAsync(5.seconds) #unix default try:
await receivedDataFuture.wait(5.seconds) #unix default
if not receivedDataFuture.finished: except AsyncTimeoutError:
raise newException(IOError, "DNS server timeout") raise newException(IOError, "DNS server timeout")
let rawResponse = sock.getMessage() let rawResponse = sock.getMessage()
# parseResponse can has a raises: [Exception, ..] because of try:
# https://github.com/nim-lang/Nim/commit/035134de429b5d99c5607c5fae912762bebb6008
# it can't actually raise though
return exceptionToAssert:
parseResponse(string.fromBytes(rawResponse)) 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: finally:
await sock.closeWait() await sock.closeWait()
method resolveIp*( method resolveIp*(
self: DnsResolver, address: string, port: Port, domain: Domain = Domain.AF_UNSPEC 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 trace "Resolving IP using DNS", address, servers = self.nameServers.mapIt($it), domain
for _ in 0 ..< self.nameServers.len: for _ in 0 ..< self.nameServers.len:
let server = self.nameServers[0] 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: if domain == Domain.AF_INET or domain == Domain.AF_UNSPEC:
responseFutures.add(getDnsResponse(server, address, A)) responseFutures.add(getDnsResponse(server, address, A))
@ -103,23 +125,32 @@ method resolveIp*(
var var
resolvedAddresses: OrderedSet[string] resolvedAddresses: OrderedSet[string]
resolveFailed = false resolveFailed = false
template handleFail(e): untyped =
info "Failed to query DNS", address, error = e.msg
resolveFailed = true
break
for fut in responseFutures: for fut in responseFutures:
try: try:
let resp = await fut let resp = await fut
for answer in resp.answers: for answer in resp.answers:
# toString can has a raises: [Exception, ..] because of resolvedAddresses.incl(answer.toString())
# https://github.com/nim-lang/Nim/commit/035134de429b5d99c5607c5fae912762bebb6008
# it can't actually raise though
resolvedAddresses.incl(exceptionToAssert(answer.toString()))
except CancelledError as e: except CancelledError as e:
raise e raise e
except ValueError as e: except ValueError as e:
info "Invalid DNS query", address, error = e.msg info "Invalid DNS query", address, error = e.msg
return @[] return @[]
except CatchableError as e: except IOError as e:
info "Failed to query DNS", address, error = e.msg handleFail(e)
resolveFailed = true except OSError as e:
break 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: if resolveFailed:
self.nameServers.add(self.nameServers[0]) self.nameServers.add(self.nameServers[0])
@ -132,27 +163,39 @@ method resolveIp*(
debug "Failed to resolve address, returning empty set" debug "Failed to resolve address, returning empty set"
return @[] 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) trace "Resolving TXT using DNS", address, servers = self.nameServers.mapIt($it)
for _ in 0 ..< self.nameServers.len: for _ in 0 ..< self.nameServers.len:
let server = self.nameServers[0] let server = self.nameServers[0]
try: template handleFail(e): untyped =
# 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:
info "Failed to query DNS", address, error = e.msg info "Failed to query DNS", address, error = e.msg
self.nameServers.add(self.nameServers[0]) self.nameServers.add(self.nameServers[0])
self.nameServers.delete(0) self.nameServers.delete(0)
continue 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" debug "Failed to resolve TXT, returning empty set"
return @[] return @[]

View File

@ -1,5 +1,5 @@
# Nim-LibP2P # Nim-LibP2P
# Copyright (c) 2023 Status Research & Development GmbH # Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of # Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT)) # * MIT license ([LICENSE-MIT](LICENSE-MIT))
@ -25,17 +25,25 @@ type MockResolver* = ref object of NameResolver
method resolveIp*( method resolveIp*(
self: MockResolver, address: string, port: Port, domain: Domain = Domain.AF_UNSPEC 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: if domain == Domain.AF_INET or domain == Domain.AF_UNSPEC:
for resp in self.ipResponses.getOrDefault((address, false)): 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: if domain == Domain.AF_INET6 or domain == Domain.AF_UNSPEC:
for resp in self.ipResponses.getOrDefault((address, true)): for resp in self.ipResponses.getOrDefault((address, true)):
result.add(initTAddress(resp, port)) res.add(initTAddress(resp, port))
method resolveTxt*(self: MockResolver, address: string): Future[seq[string]] {.async.} = res
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 = proc new*(T: typedesc[MockResolver]): T =
T() T()

View File

@ -1,5 +1,5 @@
# Nim-LibP2P # Nim-LibP2P
# Copyright (c) 2023 Status Research & Development GmbH # Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of # Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT)) # * MIT license ([LICENSE-MIT](LICENSE-MIT))
@ -9,7 +9,7 @@
{.push raises: [].} {.push raises: [].}
import std/[sugar, sets, sequtils, strutils] import std/[sets, sequtils, strutils]
import chronos, chronicles, stew/endians2 import chronos, chronicles, stew/endians2
import ".."/[multiaddress, multicodec] import ".."/[multiaddress, multicodec]
@ -20,19 +20,17 @@ type NameResolver* = ref object of RootObj
method resolveTxt*( method resolveTxt*(
self: NameResolver, address: string self: NameResolver, address: string
): Future[seq[string]] {.async, base.} = ): Future[seq[string]] {.async: (raises: [CancelledError]), base.} =
## Get TXT record ## Get TXT record
## raiseAssert "Not implemented!"
doAssert(false, "Not implemented!")
method resolveIp*( method resolveIp*(
self: NameResolver, address: string, port: Port, domain: Domain = Domain.AF_UNSPEC 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 ## Resolve the specified address
## raiseAssert "Not implemented!"
doAssert(false, "Not implemented!")
proc getHostname*(ma: MultiAddress): string = proc getHostname*(ma: MultiAddress): string =
let let
@ -46,30 +44,40 @@ proc getHostname*(ma: MultiAddress): string =
proc resolveOneAddress( proc resolveOneAddress(
self: NameResolver, ma: MultiAddress, domain: Domain = Domain.AF_UNSPEC, prefix = "" self: NameResolver, ma: MultiAddress, domain: Domain = Domain.AF_UNSPEC, prefix = ""
): Future[seq[MultiAddress]] {.async.} = ): Future[seq[MultiAddress]] {.
#Resolve a single address async: (raises: [CancelledError, MaError, TransportAddressError])
.} =
# Resolve a single address
let portPart = ma[1].valueOr:
raise maErr error
var pbuf: array[2, byte] var pbuf: array[2, byte]
let plen = portPart.protoArgument(pbuf).valueOr:
var dnsval = getHostname(ma) raise maErr error
if plen == 0:
if ma[1].tryGet().protoArgument(pbuf).tryGet() == 0: raise maErr "Incorrect port number"
raise newException(MaError, "Incorrect port number")
let let
port = Port(fromBytesBE(uint16, pbuf)) port = Port(fromBytesBE(uint16, pbuf))
dnsval = getHostname(ma)
resolvedAddresses = await self.resolveIp(prefix & dnsval, port, domain) resolvedAddresses = await self.resolveIp(prefix & dnsval, port, domain)
return collect(newSeqOfCap(4)): resolvedAddresses.mapIt:
for address in resolvedAddresses: let address = MultiAddress.init(it).valueOr:
var createdAddress = MultiAddress.init(address).tryGet()[0].tryGet() raise maErr error
for part in ma: var createdAddress = address[0].valueOr:
if DNS.match(part.tryGet()): raise maErr error
continue for part in ma:
createdAddress &= part.tryGet() let part = part.valueOr:
createdAddress raise maErr error
if DNS.match(part):
continue
createdAddress &= part
createdAddress
proc resolveDnsAddr*( proc resolveDnsAddr*(
self: NameResolver, ma: MultiAddress, depth: int = 0 self: NameResolver, ma: MultiAddress, depth: int = 0
): Future[seq[MultiAddress]] {.async.} = ): Future[seq[MultiAddress]] {.
async: (raises: [CancelledError, MaError, TransportAddressError])
.} =
if not DNSADDR.matchPartial(ma): if not DNSADDR.matchPartial(ma):
return @[ma] return @[ma]
@ -78,54 +86,67 @@ proc resolveDnsAddr*(
info "Stopping DNSADDR recursion, probably malicious", ma info "Stopping DNSADDR recursion, probably malicious", ma
return @[] return @[]
var dnsval = getHostname(ma) let
dnsval = getHostname(ma)
let txt = await self.resolveTxt("_dnsaddr." & dnsval) txt = await self.resolveTxt("_dnsaddr." & dnsval)
trace "txt entries", txt 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: for entry in txt:
if not entry.startsWith("dnsaddr="): if not entry.startsWith("dnsaddr="):
continue continue
let entryValue = MultiAddress.init(entry[8 ..^ 1]).tryGet() let
entryValue = MultiAddress.init(entry[8 ..^ 1]).valueOr:
if entryValue.contains(multiCodec("p2p")).tryGet() and raise maErr error
ma.contains(multiCodec("p2p")).tryGet(): entryHasCodec = entryValue.contains(multiCodec("p2p")).valueOr:
if entryValue[multiCodec("p2p")] != ma[multiCodec("p2p")]: raise maErr error
continue if entryHasCodec and maCodec.isOk and entryValue[codec] != maCodec:
continue
let resolved = await self.resolveDnsAddr(entryValue, depth + 1) let resolved = await self.resolveDnsAddr(entryValue, depth + 1)
for r in resolved: 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 debug "Failed to resolve a DNSADDR", ma
return @[] res
return result
proc resolveMAddress*( proc resolveMAddress*(
self: NameResolver, address: MultiAddress self: NameResolver, address: MultiAddress
): Future[seq[MultiAddress]] {.async.} = ): Future[seq[MultiAddress]] {.
async: (raises: [CancelledError, MaError, TransportAddressError])
.} =
var res = initOrderedSet[MultiAddress]() var res = initOrderedSet[MultiAddress]()
if not DNS.matchPartial(address): if not DNS.matchPartial(address):
res.incl(address) res.incl(address)
else: else:
let code = address[0].tryGet().protoCode().tryGet() let
let seq = firstPart = address[0].valueOr:
case code raise maErr error
of multiCodec("dns"): code = firstPart.protoCode().valueOr:
await self.resolveOneAddress(address) raise maErr error
of multiCodec("dns4"): ads =
await self.resolveOneAddress(address, Domain.AF_INET) case code
of multiCodec("dns6"): of multiCodec("dns"):
await self.resolveOneAddress(address, Domain.AF_INET6) await self.resolveOneAddress(address)
of multiCodec("dnsaddr"): of multiCodec("dns4"):
await self.resolveDnsAddr(address) await self.resolveOneAddress(address, Domain.AF_INET)
else: of multiCodec("dns6"):
assert false await self.resolveOneAddress(address, Domain.AF_INET6)
@[address] of multiCodec("dnsaddr"):
for ad in seq: await self.resolveDnsAddr(address)
else:
raise maErr("Unsupported codec " & $code)
for ad in ads:
res.incl(ad) res.incl(ad)
return res.toSeq res.toSeq