Add update of ENR on newly discovered address

This commit is contained in:
kdeme 2021-01-26 14:11:22 +01:00 committed by zah
parent 689eef2c11
commit bfbad64d83
10 changed files with 177 additions and 78 deletions

View File

@ -14,12 +14,12 @@ type
DiscoveryConf* = object
logLevel* {.
defaultValue: LogLevel.DEBUG
desc: "Sets the log level."
desc: "Sets the log level"
name: "log-level" .}: LogLevel
udpPort* {.
defaultValue: 9009
desc: "UDP listening port."
desc: "UDP listening port"
name: "udp-port" .}: uint16
listenAddress* {.
@ -28,32 +28,39 @@ type
name: "listen-address" }: ValidIpAddress
bootnodes* {.
desc: "ENR URI of node to bootstrap discovery with. Argument may be repeated."
desc: "ENR URI of node to bootstrap discovery with. Argument may be repeated"
name: "bootnode" .}: seq[enr.Record]
nat* {.
desc: "Specify method to use for determining public address. " &
"Must be one of: any, none, upnp, pmp, extip:<IP>."
"Must be one of: any, none, upnp, pmp, extip:<IP>"
defaultValue: "any" .}: string
enrAutoUpdate* {.
defaultValue: false
desc: "Discovery can automatically update its ENR with the IP address " &
"and UDP port as seen by other nodes it communicates with. " &
"This option allows to enable/disable this functionality"
name: "enr-auto-update" .}: bool
nodeKey* {.
desc: "P2P node private key as hex.",
desc: "P2P node private key as hex",
defaultValue: PrivateKey.random(keys.newRng()[])
name: "nodekey" .}: PrivateKey
metricsEnabled* {.
defaultValue: false
desc: "Enable the metrics server."
desc: "Enable the metrics server"
name: "metrics" .}: bool
metricsAddress* {.
defaultValue: defaultAdminListenAddress(config)
desc: "Listening address of the metrics server."
desc: "Listening address of the metrics server"
name: "metrics-address" .}: ValidIpAddress
metricsPort* {.
defaultValue: 8008
desc: "Listening HTTP port of the metrics server."
desc: "Listening HTTP port of the metrics server"
name: "metrics-port" .}: Port
case cmd* {.
@ -163,7 +170,8 @@ proc run(config: DiscoveryConf) =
let
(ip, tcpPort, udpPort) = setupNat(config)
d = newProtocol(config.nodeKey, ip, tcpPort, udpPort,
bootstrapRecords = config.bootnodes, bindIp = config.listenAddress)
bootstrapRecords = config.bootnodes, bindIp = config.listenAddress,
enrAutoUpdate = config.enrAutoUpdate)
d.open()

View File

@ -144,7 +144,8 @@ template toFieldPair*(key: string, value: auto): FieldPair =
(key, toField(value))
proc addAddress(fields: var seq[FieldPair], ip: Option[ValidIpAddress],
tcpPort, udpPort: Port) =
tcpPort, udpPort: Option[Port]) =
# It makes sense to add ports only when there is an IP provided
if ip.isSome():
let
ipExt = ip.get()
@ -152,16 +153,15 @@ proc addAddress(fields: var seq[FieldPair], ip: Option[ValidIpAddress],
fields.add(if isV6: ("ip6", ipExt.address_v6.toField)
else: ("ip", ipExt.address_v4.toField))
fields.add(((if isV6: "tcp6" else: "tcp"), tcpPort.uint16.toField))
fields.add(((if isV6: "udp6" else: "udp"), udpPort.uint16.toField))
else:
fields.add(("tcp", tcpPort.uint16.toField))
fields.add(("udp", udpPort.uint16.toField))
if tcpPort.isSome():
fields.add(((if isV6: "tcp6" else: "tcp"), tcpPort.get().uint16.toField))
if udpPort.isSome():
fields.add(((if isV6: "udp6" else: "udp"), udpPort.get().uint16.toField))
proc init*(T: type Record, seqNum: uint64,
pk: PrivateKey,
ip: Option[ValidIpAddress],
tcpPort, udpPort: Port,
tcpPort, udpPort: Option[Port],
extraFields: openarray[FieldPair] = []):
EnrResult[T] =
## Initialize a `Record` with given sequence number, private key, optional
@ -281,7 +281,7 @@ proc update*(record: var Record, pk: PrivateKey,
proc update*(r: var Record, pk: PrivateKey,
ip: Option[ValidIpAddress],
tcpPort, udpPort: Port,
tcpPort, udpPort: Option[Port] = none[Port](),
extraFields: openarray[FieldPair] = []):
EnrResult[void] =
## Update a `Record` with given ip address, tcp port, udp port and optional

View File

@ -48,14 +48,20 @@ func newNode*(r: Record): Result[Node, cstring] =
ok(Node(id: pk.get().toNodeId(), pubkey: pk.get(), record: r,
address: none(Address)))
proc updateNode*(n: Node, pk: PrivateKey, ip: Option[ValidIpAddress],
tcpPort, udpPort: Port, extraFields: openarray[FieldPair] = []):
Result[void, cstring] =
proc update*(n: Node, pk: PrivateKey, ip: Option[ValidIpAddress],
tcpPort, udpPort: Option[Port] = none[Port](),
extraFields: openarray[FieldPair] = []): Result[void, cstring] =
? n.record.update(pk, ip, tcpPort, udpPort, extraFields)
if ip.isSome():
let a = Address(ip: ip.get(), port: udpPort)
if udpPort.isSome():
let a = Address(ip: ip.get(), port: udpPort.get())
n.address = some(a)
elif n.address.isSome():
let a = Address(ip: ip.get(), port: n.address.get().port)
n.address = some(a)
else:
n.address = none(Address)
else:
n.address = none(Address)

View File

@ -1,5 +1,5 @@
# nim-eth - Node Discovery Protocol v5
# Copyright (c) 2020 Status Research & Development GmbH
# Copyright (c) 2020-2021 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, (LICENSE-APACHEv2)
# * MIT license (LICENSE-MIT)
@ -106,6 +106,8 @@ const
## refresh the routing table.
revalidateMax = 10000 ## Revalidation of a peer is done between 0 and this
## value in milliseconds
ipMajorityInterval = 5.minutes ## Interval for checking the latest IP:Port
## majority and updating this when ENR auto update is set.
initialLookups = 1 ## Amount of lookups done when populating the routing table
handshakeTimeout* = 2.seconds ## timeout for the reply on the
## whoareyou message
@ -124,9 +126,11 @@ type
awaitedMessages: Table[(NodeId, RequestId), Future[Option[Message]]]
refreshLoop: Future[void]
revalidateLoop: Future[void]
ipMajorityLoop: Future[void]
lastLookup: chronos.Moment
bootstrapRecords*: seq[Record]
ipVote: IpVote
enrAutoUpdate: bool
rng*: ref BrHmacDrbgContext
PendingRequest = object
@ -890,21 +894,46 @@ proc refreshLoop(d: Protocol) {.async, raises: [Exception, Defect].} =
trace "Discovered nodes in random target query", nodes = randomQuery.len
debug "Total nodes in discv5 routing table", total = d.routingTable.len()
let majority = d.ipVote.majority()
if majority.isSome():
let address = majority.get()
debug "Majority on voted address", address
await sleepAsync(refreshInterval)
except CancelledError:
trace "refreshLoop canceled"
proc ipMajorityLoop(d: Protocol) {.async, raises: [Exception, Defect].} =
try:
while true:
let majority = d.ipVote.majority()
if majority.isSome():
if d.localNode.address != majority:
let address = majority.get()
let previous = d.localNode.address
if d.enrAutoUpdate:
let res = d.localNode.update(d.privateKey,
ip = some(address.ip), udpPort = some(address.port))
if res.isErr:
warn "Failed updating ENR with newly discovered external address",
majority, previous, error = res.error
else:
info "Updated ENR with newly discovered external address",
majority, previous, uri = toURI(d.localNode.record)
else:
warn "Discovered new external address but ENR auto update is off",
majority, previous
else:
debug "Discovered external address matches current address", majority,
current = d.localNode.address
await sleepAsync(ipMajorityInterval)
except CancelledError:
trace "ipMajorityLoop canceled"
proc newProtocol*(privKey: PrivateKey,
externalIp: Option[ValidIpAddress], tcpPort, udpPort: Port,
externalIp: Option[ValidIpAddress],
tcpPort, udpPort: Port,
localEnrFields: openarray[(string, seq[byte])] = [],
bootstrapRecords: openarray[Record] = [],
previousRecord = none[enr.Record](),
bindIp = IPv4_any(),
enrAutoUpdate = false,
tableIpLimits = DefaultTableIpLimits,
rng = newRng()):
Protocol {.raises: [Defect].} =
@ -920,11 +949,11 @@ proc newProtocol*(privKey: PrivateKey,
var record: Record
if previousRecord.isSome():
record = previousRecord.get()
record.update(privKey, externalIp, tcpPort, udpPort,
record.update(privKey, externalIp, some(tcpPort), some(udpPort),
extraFields).expect("Record within size limits and correct key")
else:
record = enr.Record.init(1, privKey, externalIp, tcpPort, udpPort,
extraFields).expect("Record within size limits")
record = enr.Record.init(1, privKey, externalIp, some(tcpPort),
some(udpPort), extraFields).expect("Record within size limits")
let node = newNode(record).expect("Properly initialized record")
# TODO Consider whether this should be a Defect
@ -938,6 +967,7 @@ proc newProtocol*(privKey: PrivateKey,
sessions: Sessions.init(256)),
bootstrapRecords: @bootstrapRecords,
ipVote: IpVote.init(),
enrAutoUpdate: enrAutoUpdate,
rng: rng)
result.routingTable.init(node, DefaultBitsPerHop, tableIpLimits, rng)
@ -959,6 +989,7 @@ proc open*(d: Protocol) {.raises: [Exception, Defect].} =
proc start*(d: Protocol) {.raises: [Exception, Defect].} =
d.refreshLoop = refreshLoop(d)
d.revalidateLoop = revalidateLoop(d)
d.ipMajorityLoop = ipMajorityLoop(d)
proc close*(d: Protocol) {.raises: [Exception, Defect].} =
doAssert(not d.transp.closed)
@ -968,6 +999,8 @@ proc close*(d: Protocol) {.raises: [Exception, Defect].} =
d.revalidateLoop.cancel()
if not d.refreshLoop.isNil:
d.refreshLoop.cancel()
if not d.ipMajorityLoop.isNil:
d.ipMajorityLoop.cancel()
d.transp.close()
@ -979,5 +1012,7 @@ proc closeWait*(d: Protocol) {.async, raises: [Exception, Defect].} =
await d.revalidateLoop.cancelAndWait()
if not d.refreshLoop.isNil:
await d.refreshLoop.cancelAndWait()
if not d.ipMajorityLoop.isNil:
await d.ipMajorityLoop.cancelAndWait()
await d.transp.closeWait()

View File

@ -1,6 +1,6 @@
import
testutils/fuzzing, stew/shims/net, stew/byteutils,
eth/p2p/discoveryv5/[encodingv1, enr, sessions, node]
eth/p2p/discoveryv5/[encoding, enr, sessions, node]
init:
const
@ -12,13 +12,13 @@ init:
privKeyB = PrivateKey.fromHex(nodeBKey)[] # receive -> decode
enrRecA = enr.Record.init(1, privKeyA,
some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
nodeA = newNode(enrRecA).expect("Properly initialized record")
enrRecB = enr.Record.init(1, privKeyB,
some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
nodeB = newNode(enrRecB).expect("Properly initialized record")
var codecB = Codec(localNode: nodeB, privKey: privKeyB,

View File

@ -38,7 +38,7 @@ proc generateNode*(privKey: PrivateKey, port: int = 20302,
localEnrFields: openarray[FieldPair] = []): Node =
let port = Port(port)
let enr = enr.Record.init(1, privKey, some(ip),
port, port, localEnrFields).expect("Properly intialized private key")
some(port), some(port), localEnrFields).expect("Properly intialized private key")
result = newNode(enr).expect("Properly initialized node")
proc nodeAndPrivKeyAtDistance*(n: Node, rng: var BrHmacDrbgContext, d: uint32,

View File

@ -463,7 +463,7 @@ procSuite "Discovery v5 Tests":
port = Port(9000)
fromNoderecord = enr.Record.init(1, PrivateKey.random(rng[]),
some(ValidIpAddress.init("11.12.13.14")),
port, port)[]
some(port), some(port))[]
fromNode = newNode(fromNoderecord)[]
pk = PrivateKey.random(rng[])
targetDistance = logDist(fromNode.id, pk.toPublicKey().toNodeId())
@ -471,7 +471,8 @@ procSuite "Discovery v5 Tests":
block: # Duplicates
let
record = enr.Record.init(
1, pk, some(ValidIpAddress.init("12.13.14.15")), port, port)[]
1, pk, some(ValidIpAddress.init("12.13.14.15")),
some(port), some(port))[]
# Exact duplicates
var records = @[record, record]
@ -480,7 +481,8 @@ procSuite "Discovery v5 Tests":
# Node id duplicates
let recordSameId = enr.Record.init(
1, pk, some(ValidIpAddress.init("212.13.14.15")), port, port)[]
1, pk, some(ValidIpAddress.init("212.13.14.15")),
some(port), some(port))[]
records.add(recordSameId)
nodes = verifyNodesRecords(records, fromNode, targetDistance)
check nodes.len == 1
@ -488,7 +490,7 @@ procSuite "Discovery v5 Tests":
block: # No address
let
recordNoAddress = enr.Record.init(
1, pk, none(ValidIpAddress), port, port)[]
1, pk, none(ValidIpAddress), some(port), some(port))[]
records = [recordNoAddress]
test = verifyNodesRecords(records, fromNode, targetDistance)
check test.len == 0
@ -497,7 +499,7 @@ procSuite "Discovery v5 Tests":
let
recordInvalidAddress = enr.Record.init(
1, pk, some(ValidIpAddress.init("10.1.2.3")),
port, port)[]
some(port), some(port))[]
records = [recordInvalidAddress]
test = verifyNodesRecords(records, fromNode, targetDistance)
check test.len == 0
@ -505,7 +507,8 @@ procSuite "Discovery v5 Tests":
block: # Invalid address - loopback
let
recordInvalidAddress = enr.Record.init(
1, pk, some(ValidIpAddress.init("127.0.0.1")), port, port)[]
1, pk, some(ValidIpAddress.init("127.0.0.1")),
some(port), some(port))[]
records = [recordInvalidAddress]
test = verifyNodesRecords(records, fromNode, targetDistance)
check test.len == 0
@ -513,7 +516,8 @@ procSuite "Discovery v5 Tests":
block: # Invalid distance
let
recordInvalidDistance = enr.Record.init(
1, pk, some(ValidIpAddress.init("12.13.14.15")), port, port)[]
1, pk, some(ValidIpAddress.init("12.13.14.15")),
some(port), some(port))[]
records = [recordInvalidDistance]
test = verifyNodesRecords(records, fromNode, 0'u32)
check test.len == 0
@ -530,8 +534,8 @@ procSuite "Discovery v5 Tests":
let
privKey = PrivateKey.random(rng[])
enrRec = enr.Record.init(1, privKey,
some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record")
var codec = Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5))
@ -559,8 +563,8 @@ procSuite "Discovery v5 Tests":
let
privKey = PrivateKey.random(rng[])
enrRec = enr.Record.init(1, privKey,
some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record")
var codec = Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5))
for i in 0 ..< 5:
@ -590,8 +594,8 @@ procSuite "Discovery v5 Tests":
a = localAddress(20303)
privKey = PrivateKey.random(rng[])
enrRec = enr.Record.init(1, privKey,
some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record")
var codec = Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5))

View File

@ -219,13 +219,13 @@ suite "Discovery v5.1 Packet Encodings Test Vectors":
privKeyB = PrivateKey.fromHex(nodeBKey)[] # receive -> decode
enrRecA = enr.Record.init(1, privKeyA,
some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
nodeA = newNode(enrRecA).expect("Properly initialized record")
enrRecB = enr.Record.init(1, privKeyB,
some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
nodeB = newNode(enrRecB).expect("Properly initialized record")
var
@ -432,12 +432,14 @@ suite "Discovery v5.1 Additional Encode/Decode":
privKeyA = PrivateKey.random(rng[]) # sender -> encode
privKeyB = PrivateKey.random(rng[]) # receiver -> decode
enrRecA = enr.Record.init(1, privKeyA, some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
enrRecA = enr.Record.init(1, privKeyA,
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
nodeA = newNode(enrRecA).expect("Properly initialized record")
enrRecB = enr.Record.init(1, privKeyB, some(ValidIpAddress.init("127.0.0.1")), Port(9000),
Port(9000)).expect("Properly intialized private key")
enrRecB = enr.Record.init(1, privKeyB,
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
nodeB = newNode(enrRecB).expect("Properly initialized record")
var

View File

@ -60,8 +60,9 @@ suite "ENR":
let
keypair = KeyPair.random(rng[])
ip = ValidIpAddress.init("10.20.30.40")
port = some(Port(9000))
enr = Record.init(
100, keypair.seckey, some(ip), Port(9000), Port(9000),@[])[]
100, keypair.seckey, some(ip), port, port,@[])[]
typedEnr = get enr.toTypedRecord()
check:
@ -80,8 +81,9 @@ suite "ENR":
test "ENR without address":
let
keypair = KeyPair.random(rng[])
port = some(Port(9000))
enr = Record.init(
100, keypair.seckey, none(ValidIpAddress), Port(9000), Port(9000))[]
100, keypair.seckey, none(ValidIpAddress), port, port)[]
typedEnr = get enr.toTypedRecord()
check:
@ -89,11 +91,8 @@ suite "ENR":
typedEnr.secp256k1.get() == keypair.pubkey.toRawCompressed()
typedEnr.ip.isNone()
typedEnr.tcp.isSome()
typedEnr.tcp.get() == 9000
typedEnr.udp.isSome()
typedEnr.udp.get() == 9000
typedEnr.tcp.isNone()
typedEnr.udp.isNone()
typedEnr.ip6.isNone()
typedEnr.tcp6.isNone()
@ -115,7 +114,7 @@ suite "ENR":
pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
newField = toFieldPair("test", 123'u)
var r = Record.init(1, pk, none(ValidIpAddress), Port(9000), Port(9000))[]
var r = Record.init(1, pk, none(ValidIpAddress), none(Port), none(Port))[]
block: # Insert new k:v pair, update of seqNum should occur.
let updated = r.update(pk, [newField])
@ -182,20 +181,63 @@ suite "ENR":
let
pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
var r = Record.init(1, pk, none(ValidIpAddress), Port(9000), Port(9000))[]
var r = Record.init(1, pk, none(ValidIpAddress),
some(Port(9000)), some(Port(9000)))[]
block:
let updated = r.update(pk, none(ValidIpAddress), Port(9000), Port(9000))
let updated = r.update(pk, none(ValidIpAddress),
some(Port(9000)), some(Port(9000)))
check updated.isOk()
check:
r.get("tcp", uint) == 9000
r.get("udp", uint) == 9000
r.tryGet("ip", uint).isNone()
r.tryGet("tcp", uint).isNone()
r.tryGet("udp", uint).isNone()
r.seqNum == 1
block:
let updated = r.update(pk, none(ValidIpAddress), Port(9001), Port(9002))
let updated = r.update(pk, none(ValidIpAddress),
some(Port(9001)), some(Port(9002)))
check updated.isOk()
check:
r.get("tcp", uint) == 9001
r.get("udp", uint) == 9002
r.tryGet("ip", uint).isNone()
r.tryGet("tcp", uint).isNone()
r.tryGet("udp", uint).isNone()
r.seqNum == 1
block:
let updated = r.update(pk, some(ValidIpAddress.init("10.20.30.40")),
some(Port(9000)), some(Port(9000)))
check updated.isOk()
let typedEnr = r.toTypedRecord().get()
check:
typedEnr.ip.isSome()
typedEnr.ip.get() == [byte 10, 20, 30, 40]
typedEnr.tcp.isSome()
typedEnr.tcp.get() == 9000
typedEnr.udp.isSome()
typedEnr.udp.get() == 9000
r.seqNum == 2
block:
let updated = r.update(pk, some(ValidIpAddress.init("10.20.30.40")),
some(Port(9001)), some(Port(9001)))
check updated.isOk()
let typedEnr = r.toTypedRecord().get()
check:
typedEnr.ip.isSome()
typedEnr.ip.get() == [byte 10, 20, 30, 40]
typedEnr.tcp.isSome()
typedEnr.tcp.get() == 9001
typedEnr.udp.isSome()
typedEnr.udp.get() == 9001
r.seqNum == 3

View File

@ -472,8 +472,9 @@ suite "Routing Table Tests":
let updatedNode1 = generateNode(pk)
# Need to do an update to get seqNum increased
let updated = updatedNode1.updateNode(pk,
some(ValidIpAddress.init("192.168.0.1")), Port(9000), Port(9000))
let updated = updatedNode1.update(pk,
some(ValidIpAddress.init("192.168.0.1")),
some(Port(9000)), some(Port(9000)))
check updated.isOk()
check table.addNode(updatedNode1) == Existing
@ -524,8 +525,9 @@ suite "Routing Table Tests":
for i in 0..<DefaultTableIpLimits.bucketIpLimit + 1:
# Need to do an update to get seqNum increased
let updated = updatedNode1.updateNode(pk,
some(ValidIpAddress.init("192.168.0.1")), Port(9000+i), Port(9000+i))
let updated = updatedNode1.update(pk,
some(ValidIpAddress.init("192.168.0.1")),
some(Port(9000+i)), some(Port(9000+i)))
check updated.isOk()
check table.addNode(updatedNode1) == Existing