mirror of https://github.com/status-im/nim-eth.git
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:
parent
e219547d64
commit
a95b205cf7
|
@ -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))
|
|
@ -78,7 +78,7 @@ import
|
||||||
stew/shims/net as stewNet, json_serialization/std/net,
|
stew/shims/net as stewNet, json_serialization/std/net,
|
||||||
stew/endians2, chronicles, chronos, stint, bearssl, metrics,
|
stew/endians2, chronicles, chronos, stint, bearssl, metrics,
|
||||||
".."/../[rlp, keys, async_utils],
|
".."/../[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
|
import nimcrypto except toHex
|
||||||
|
|
||||||
|
@ -449,23 +449,6 @@ proc processClient(transp: DatagramTransport, raddr: TransportAddress):
|
||||||
|
|
||||||
proto.receive(a, buf)
|
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) =
|
proc replaceNode(d: Protocol, n: Node) =
|
||||||
if n.record notin d.bootstrapRecords:
|
if n.record notin d.bootstrapRecords:
|
||||||
d.routingTable.replaceNode(n)
|
d.routingTable.replaceNode(n)
|
||||||
|
@ -496,58 +479,6 @@ proc waitMessage(d: Protocol, fromNode: Node, reqId: RequestId):
|
||||||
res.complete(none(Message))
|
res.complete(none(Message))
|
||||||
d.awaitedMessages[key] = result
|
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):
|
proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId):
|
||||||
Future[DiscResult[seq[Record]]] {.async.} =
|
Future[DiscResult[seq[Record]]] {.async.} =
|
||||||
## Wait for one or more nodes replies.
|
## 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)
|
let nodes = await d.waitNodes(toNode, reqId)
|
||||||
|
|
||||||
if nodes.isOk:
|
if nodes.isOk:
|
||||||
let res = verifyNodesRecords(nodes.get(), toNode, distances)
|
let res = verifyNodesRecords(nodes.get(), toNode, findNodeResultLimit, distances)
|
||||||
d.routingTable.setJustSeen(toNode)
|
d.routingTable.setJustSeen(toNode)
|
||||||
return ok(res)
|
return ok(res)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -5,7 +5,7 @@ import
|
||||||
chronos, chronicles, stint, testutils/unittests, stew/shims/net,
|
chronos, chronicles, stint, testutils/unittests, stew/shims/net,
|
||||||
stew/byteutils, bearssl,
|
stew/byteutils, bearssl,
|
||||||
../../eth/keys,
|
../../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,
|
../../eth/p2p/discoveryv5/protocol as discv5_protocol,
|
||||||
./discv5_test_helper
|
./discv5_test_helper
|
||||||
|
|
||||||
|
@ -471,7 +471,8 @@ procSuite "Discovery v5 Tests":
|
||||||
some(port), some(port))[]
|
some(port), some(port))[]
|
||||||
fromNode = newNode(fromNoderecord)[]
|
fromNode = newNode(fromNoderecord)[]
|
||||||
pk = PrivateKey.random(rng[])
|
pk = PrivateKey.random(rng[])
|
||||||
targetDistance = logDist(fromNode.id, pk.toPublicKey().toNodeId())
|
targetDistance = @[logDist(fromNode.id, pk.toPublicKey().toNodeId())]
|
||||||
|
limit = 16
|
||||||
|
|
||||||
block: # Duplicates
|
block: # Duplicates
|
||||||
let
|
let
|
||||||
|
@ -481,7 +482,7 @@ procSuite "Discovery v5 Tests":
|
||||||
|
|
||||||
# Exact duplicates
|
# Exact duplicates
|
||||||
var records = @[record, record]
|
var records = @[record, record]
|
||||||
var nodes = verifyNodesRecords(records, fromNode, targetDistance)
|
var nodes = verifyNodesRecords(records, fromNode, limit, targetDistance)
|
||||||
check nodes.len == 1
|
check nodes.len == 1
|
||||||
|
|
||||||
# Node id duplicates
|
# Node id duplicates
|
||||||
|
@ -489,7 +490,7 @@ procSuite "Discovery v5 Tests":
|
||||||
1, pk, some(ValidIpAddress.init("212.13.14.15")),
|
1, pk, some(ValidIpAddress.init("212.13.14.15")),
|
||||||
some(port), some(port))[]
|
some(port), some(port))[]
|
||||||
records.add(recordSameId)
|
records.add(recordSameId)
|
||||||
nodes = verifyNodesRecords(records, fromNode, targetDistance)
|
nodes = verifyNodesRecords(records, fromNode, limit, targetDistance)
|
||||||
check nodes.len == 1
|
check nodes.len == 1
|
||||||
|
|
||||||
block: # No address
|
block: # No address
|
||||||
|
@ -497,7 +498,7 @@ procSuite "Discovery v5 Tests":
|
||||||
recordNoAddress = enr.Record.init(
|
recordNoAddress = enr.Record.init(
|
||||||
1, pk, none(ValidIpAddress), some(port), some(port))[]
|
1, pk, none(ValidIpAddress), some(port), some(port))[]
|
||||||
records = [recordNoAddress]
|
records = [recordNoAddress]
|
||||||
test = verifyNodesRecords(records, fromNode, targetDistance)
|
test = verifyNodesRecords(records, fromNode, limit, targetDistance)
|
||||||
check test.len == 0
|
check test.len == 0
|
||||||
|
|
||||||
block: # Invalid address - site local
|
block: # Invalid address - site local
|
||||||
|
@ -506,7 +507,7 @@ procSuite "Discovery v5 Tests":
|
||||||
1, pk, some(ValidIpAddress.init("10.1.2.3")),
|
1, pk, some(ValidIpAddress.init("10.1.2.3")),
|
||||||
some(port), some(port))[]
|
some(port), some(port))[]
|
||||||
records = [recordInvalidAddress]
|
records = [recordInvalidAddress]
|
||||||
test = verifyNodesRecords(records, fromNode, targetDistance)
|
test = verifyNodesRecords(records, fromNode, limit, targetDistance)
|
||||||
check test.len == 0
|
check test.len == 0
|
||||||
|
|
||||||
block: # Invalid address - loopback
|
block: # Invalid address - loopback
|
||||||
|
@ -515,7 +516,7 @@ procSuite "Discovery v5 Tests":
|
||||||
1, pk, some(ValidIpAddress.init("127.0.0.1")),
|
1, pk, some(ValidIpAddress.init("127.0.0.1")),
|
||||||
some(port), some(port))[]
|
some(port), some(port))[]
|
||||||
records = [recordInvalidAddress]
|
records = [recordInvalidAddress]
|
||||||
test = verifyNodesRecords(records, fromNode, targetDistance)
|
test = verifyNodesRecords(records, fromNode, limit, targetDistance)
|
||||||
check test.len == 0
|
check test.len == 0
|
||||||
|
|
||||||
block: # Invalid distance
|
block: # Invalid distance
|
||||||
|
@ -524,9 +525,18 @@ procSuite "Discovery v5 Tests":
|
||||||
1, pk, some(ValidIpAddress.init("12.13.14.15")),
|
1, pk, some(ValidIpAddress.init("12.13.14.15")),
|
||||||
some(port), some(port))[]
|
some(port), some(port))[]
|
||||||
records = [recordInvalidDistance]
|
records = [recordInvalidDistance]
|
||||||
test = verifyNodesRecords(records, fromNode, 0'u16)
|
test = verifyNodesRecords(records, fromNode, limit, @[0'u16])
|
||||||
check test.len == 0
|
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":
|
test "Calculate lookup distances":
|
||||||
# Log distance between zeros is zero
|
# Log distance between zeros is zero
|
||||||
let dist = lookupDistances(u256(0), u256(0))
|
let dist = lookupDistances(u256(0), u256(0))
|
||||||
|
|
Loading…
Reference in New Issue