Modify nodes verification (#398)

* Modify nodes verification

* Move nodes verification to separate module

By moving verification to separate module it can be re-used
in different contexts not only in discoveryv5.
This commit is contained in:
KonradStaniec 2021-09-13 14:20:26 +02:00 committed by GitHub
parent e219547d64
commit a95b205cf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 105 additions and 79 deletions

View File

@ -0,0 +1,85 @@
{.push raises: [Defect].}
import
std/[sets, options],
stew/results, stew/shims/net, chronicles, chronos,
"."/[node, enr, routing_table]
logScope:
topics = "nodes-verification"
proc validIp(sender, address: IpAddress): bool =
let
s = initTAddress(sender, Port(0))
a = initTAddress(address, Port(0))
if a.isAnyLocal():
return false
if a.isMulticast():
return false
if a.isLoopback() and not s.isLoopback():
return false
if a.isSiteLocal() and not s.isSiteLocal():
return false
# TODO: Also check for special reserved ip addresses:
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
return true
proc verifyNodesRecords(enrs: openarray[Record], fromNode: Node, nodesLimit: int,
distances: Option[seq[uint16]]): seq[Node] =
## Verify and convert ENRs to a sequence of nodes. Only ENRs that pass
## verification will be added. ENRs are verified for duplicates, invalid
## addresses and invalid distances if those are specified.
var seen: HashSet[Node]
var count = 0
for r in enrs:
# Check and allow for processing of maximum `findNodeResultLimit` ENRs
# returned. This limitation is required so no huge lists of invalid ENRs
# are processed for no reason, and for not overwhelming a routing table
# with nodes from a malicious actor.
# The discovery v5 specification specifies no limit on the amount of ENRs
# that can be returned, but clients usually stick with the bucket size limit
# as in original Kademlia. Because of this it is chosen not to fail
# immediatly, but still process maximum `findNodeResultLimit`.
if count >= nodesLimit:
debug "Too many ENRs", enrs = enrs.len(),
limit = nodesLimit, sender = fromNode.record.toURI
break
count.inc()
let node = newNode(r)
if node.isOk():
let n = node.get()
# Check for duplicates in the nodes reply. Duplicates are checked based
# on node id.
if n in seen:
trace "Duplicate node ids",
record = n.record.toURI, id = n.id, sender = fromNode.record.toURI
continue
# Check if the node has an address and if the address is public or from
# the same local network or lo network as the sender. The latter allows
# for local testing.
if not n.address.isSome() or not
validIp(fromNode.address.get().ip, n.address.get().ip):
trace "Invalid ip-address",
record = n.record.toURI, node = n, sender = fromNode.record.toURI
continue
# Check if returned node has one of the requested distances.
if distances.isSome():
if (not distances.get().contains(logDist(n.id, fromNode.id))):
debug "Incorrect distance",
record = n.record.toURI, sender = fromNode.record.toURI
continue
# No check on UDP port and thus any port is allowed, also the so called
# "well-known" ports.
seen.incl(n)
result.add(n)
proc verifyNodesRecords*(enrs: openarray[Record], fromNode: Node, nodesLimit: int): seq[Node] =
verifyNodesRecords(enrs, fromNode, nodesLimit, none[seq[uint16]]())
proc verifyNodesRecords*(enrs: openarray[Record], fromNode: Node, nodesLimit: int, distances: seq[uint16]): seq[Node] =
verifyNodesRecords(enrs, fromNode, nodesLimit, some[seq[uint16]](distances))

View File

@ -78,7 +78,7 @@ import
stew/shims/net as stewNet, json_serialization/std/net,
stew/endians2, chronicles, chronos, stint, bearssl, metrics,
".."/../[rlp, keys, async_utils],
"."/[messages, encoding, node, routing_table, enr, random2, sessions, ip_vote]
"."/[messages, encoding, node, routing_table, enr, random2, sessions, ip_vote, nodes_verification]
import nimcrypto except toHex
@ -449,23 +449,6 @@ proc processClient(transp: DatagramTransport, raddr: TransportAddress):
proto.receive(a, buf)
proc validIp(sender, address: IpAddress): bool =
let
s = initTAddress(sender, Port(0))
a = initTAddress(address, Port(0))
if a.isAnyLocal():
return false
if a.isMulticast():
return false
if a.isLoopback() and not s.isLoopback():
return false
if a.isSiteLocal() and not s.isSiteLocal():
return false
# TODO: Also check for special reserved ip addresses:
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
return true
proc replaceNode(d: Protocol, n: Node) =
if n.record notin d.bootstrapRecords:
d.routingTable.replaceNode(n)
@ -496,58 +479,6 @@ proc waitMessage(d: Protocol, fromNode: Node, reqId: RequestId):
res.complete(none(Message))
d.awaitedMessages[key] = result
proc verifyNodesRecords*(enrs: openarray[Record], fromNode: Node,
distances: varargs[uint16]): seq[Node] =
## Verify and convert ENRs to a sequence of nodes. Only ENRs that pass
## verification will be added. ENRs are verified for duplicates, invalid
## addresses and invalid distances.
var seen: HashSet[Node]
var count = 0
for r in enrs:
# Check and allow for processing of maximum `findNodeResultLimit` ENRs
# returned. This limitation is required so no huge lists of invalid ENRs
# are processed for no reason, and for not overwhelming a routing table
# with nodes from a malicious actor.
# The discovery v5 specification specifies no limit on the amount of ENRs
# that can be returned, but clients usually stick with the bucket size limit
# as in original Kademlia. Because of this it is chosen not to fail
# immediatly, but still process maximum `findNodeResultLimit`.
if count >= findNodeResultLimit:
debug "Response on findnode returned too many ENRs", enrs = enrs.len(),
limit = findNodeResultLimit, sender = fromNode.record.toURI
break
count.inc()
let node = newNode(r)
if node.isOk():
let n = node.get()
# Check for duplicates in the nodes reply. Duplicates are checked based
# on node id.
if n in seen:
trace "Nodes reply contained records with duplicate node ids",
record = n.record.toURI, id = n.id, sender = fromNode.record.toURI
continue
# Check if the node has an address and if the address is public or from
# the same local network or lo network as the sender. The latter allows
# for local testing.
if not n.address.isSome() or not
validIp(fromNode.address.get().ip, n.address.get().ip):
trace "Nodes reply contained record with invalid ip-address",
record = n.record.toURI, node = n, sender = fromNode.record.toURI
continue
# Check if returned node has one of the requested distances.
if not distances.contains(logDist(n.id, fromNode.id)):
debug "Nodes reply contained record with incorrect distance",
record = n.record.toURI, sender = fromNode.record.toURI
continue
# No check on UDP port and thus any port is allowed, also the so called
# "well-known" ports.
seen.incl(n)
result.add(n)
proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId):
Future[DiscResult[seq[Record]]] {.async.} =
## Wait for one or more nodes replies.
@ -625,7 +556,7 @@ proc findNode*(d: Protocol, toNode: Node, distances: seq[uint16]):
let nodes = await d.waitNodes(toNode, reqId)
if nodes.isOk:
let res = verifyNodesRecords(nodes.get(), toNode, distances)
let res = verifyNodesRecords(nodes.get(), toNode, findNodeResultLimit, distances)
d.routingTable.setJustSeen(toNode)
return ok(res)
else:

View File

@ -5,7 +5,7 @@ import
chronos, chronicles, stint, testutils/unittests, stew/shims/net,
stew/byteutils, bearssl,
../../eth/keys,
../../eth/p2p/discoveryv5/[enr, node, routing_table, encoding, sessions, messages],
../../eth/p2p/discoveryv5/[enr, node, routing_table, encoding, sessions, messages, nodes_verification],
../../eth/p2p/discoveryv5/protocol as discv5_protocol,
./discv5_test_helper
@ -471,7 +471,8 @@ procSuite "Discovery v5 Tests":
some(port), some(port))[]
fromNode = newNode(fromNoderecord)[]
pk = PrivateKey.random(rng[])
targetDistance = logDist(fromNode.id, pk.toPublicKey().toNodeId())
targetDistance = @[logDist(fromNode.id, pk.toPublicKey().toNodeId())]
limit = 16
block: # Duplicates
let
@ -481,7 +482,7 @@ procSuite "Discovery v5 Tests":
# Exact duplicates
var records = @[record, record]
var nodes = verifyNodesRecords(records, fromNode, targetDistance)
var nodes = verifyNodesRecords(records, fromNode, limit, targetDistance)
check nodes.len == 1
# Node id duplicates
@ -489,7 +490,7 @@ procSuite "Discovery v5 Tests":
1, pk, some(ValidIpAddress.init("212.13.14.15")),
some(port), some(port))[]
records.add(recordSameId)
nodes = verifyNodesRecords(records, fromNode, targetDistance)
nodes = verifyNodesRecords(records, fromNode, limit, targetDistance)
check nodes.len == 1
block: # No address
@ -497,7 +498,7 @@ procSuite "Discovery v5 Tests":
recordNoAddress = enr.Record.init(
1, pk, none(ValidIpAddress), some(port), some(port))[]
records = [recordNoAddress]
test = verifyNodesRecords(records, fromNode, targetDistance)
test = verifyNodesRecords(records, fromNode, limit, targetDistance)
check test.len == 0
block: # Invalid address - site local
@ -506,7 +507,7 @@ procSuite "Discovery v5 Tests":
1, pk, some(ValidIpAddress.init("10.1.2.3")),
some(port), some(port))[]
records = [recordInvalidAddress]
test = verifyNodesRecords(records, fromNode, targetDistance)
test = verifyNodesRecords(records, fromNode, limit, targetDistance)
check test.len == 0
block: # Invalid address - loopback
@ -515,7 +516,7 @@ procSuite "Discovery v5 Tests":
1, pk, some(ValidIpAddress.init("127.0.0.1")),
some(port), some(port))[]
records = [recordInvalidAddress]
test = verifyNodesRecords(records, fromNode, targetDistance)
test = verifyNodesRecords(records, fromNode, limit, targetDistance)
check test.len == 0
block: # Invalid distance
@ -524,9 +525,18 @@ procSuite "Discovery v5 Tests":
1, pk, some(ValidIpAddress.init("12.13.14.15")),
some(port), some(port))[]
records = [recordInvalidDistance]
test = verifyNodesRecords(records, fromNode, 0'u16)
test = verifyNodesRecords(records, fromNode, limit, @[0'u16])
check test.len == 0
block: # Invalid distance but distance validation is disabled
let
recordInvalidDistance = enr.Record.init(
1, pk, some(ValidIpAddress.init("12.13.14.15")),
some(port), some(port))[]
records = [recordInvalidDistance]
test = verifyNodesRecords(records, fromNode, limit)
check test.len == 1
test "Calculate lookup distances":
# Log distance between zeros is zero
let dist = lookupDistances(u256(0), u256(0))