Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 118 additions & 1 deletion eth/enr/enr.nim
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,9 @@ func init*(
extraFields: openArray[FieldPair] = []):
EnrResult[T] =
## Initialize a `Record` with given sequence number, private key, optional
## ip address, tcp port, udp port, and optional custom k:v pairs.
## IP address, TCP port, UDP port, and optional custom k:v pairs.
##
## The IP address can be an IPv4 or IPv6 address.
## Can fail in case the record exceeds the `maxEnrSize`.
doAssert(not hasPredefinedKey(extraFields), "Predefined key in custom pairs")

Expand All @@ -255,6 +256,52 @@ func init*(
fields.insert extraFields
makeEnrAux(seqNum, "v4", pk, fields)

func initDS*(
T: type Record,
seqNum: uint64, pk: PrivateKey,
ip4: Opt[IpAddress] = Opt.none(IpAddress),
tcp4Port: Opt[Port] = Opt.none(Port),
udp4Port: Opt[Port] = Opt.none(Port),
ip6: Opt[IpAddress] = Opt.none(IpAddress),
tcp6Port: Opt[Port] = Opt.none(Port),
udp6Port: Opt[Port] = Opt.none(Port),
extraFields: openArray[FieldPair] = []
): EnrResult[T] =
## Initialize a `Record` with given sequence number, private key, IPv4 and IPv6
## address, optional TCP port and UDP port for both IPv4 and IPV6, and
## optional custom k:v pairs.
##
## The tcp6 and udp6 fields will only be set if they differ from the tcp and udp
## fields.
##
## Can fail in case the record exceeds the `maxEnrSize`.
doAssert(not hasPredefinedKey(extraFields), "Predefined key in custom pairs")

var fields = newSeq[FieldPair]()

if ip4.isSome() and ip4.value().family == IpAddressFamily.IPv4:
fields.insert(("ip", ip4.value().address_v4.toField))
if ip6.isSome() and ip6.value().family == IpAddressFamily.IPv6:
fields.insert(("ip6", ip6.value().address_v6.toField))

if tcp4Port.isSome():
fields.insert(("tcp", tcp4Port.value().uint16.toField))
if udp4Port.isSome():
fields.insert(("udp", udp4Port.value().uint16.toField))

# From the ENR specification:
# "Declaring the same port number in both tcp, tcp6 or udp, udp6 should be avoided
# but doesn't render the record invalid."
if tcp6Port.isSome():
if tcp6Port != tcp4Port:
fields.insert(("tcp6", tcp6Port.value().uint16.toField))
if udp6Port.isSome():
if udp6Port != udp4Port:
fields.insert(("udp6", udp6Port.value().uint16.toField))

fields.insert extraFields
makeEnrAux(seqNum, "v4", pk, fields)

func getField(r: Record, name: string, field: var Field): bool =
# It might be more correct to do binary search,
# as the fields are sorted, but it's unlikely to
Expand Down Expand Up @@ -362,6 +409,76 @@ func update*(

ok()

func updateDS*(
record: var Record,
pk: PrivateKey,
ip4: Opt[IpAddress] = Opt.none(IpAddress),
tcp4Port: Opt[Port] = Opt.none(Port),
udp4Port: Opt[Port] = Opt.none(Port),
ip6: Opt[IpAddress] = Opt.none(IpAddress),
tcp6Port: Opt[Port] = Opt.none(Port),
udp6Port: Opt[Port] = Opt.none(Port),
extraFields: openArray[FieldPair] = []):
EnrResult[void] =
## Update a `Record` with given IPv4 and IPv6 address, optional TCP port and
## UDP port for both IPv4 and IPV6, and optional custom k:v pairs.
##
## This function is to be used when running in IPv4 and IPv6 dual stack mode.
## The tcp6 and udp6 fields will only be set if they differ from the tcp and udp
## fields.
##
## If none of the k:v pairs are changed, the sequence number of the `Record`
## will still be incremented and a new signature will be applied.
##
## Providing an `Opt.none` for `tcp4Port`/`udp4Port`/`tcp6Port`/`udp6Port`/
## will leave the corresponding field untouched.
##
## Can fail in case of wrong `PrivateKey`, if the size of the resulting record
## exceeds `maxEnrSize` or if maximum sequence number is reached. The `Record`
## will not be altered in these cases.
# TODO: deprecate this call and have individual functions for updating?
doAssert(not hasPredefinedKey(extraFields), "Predefined key in custom pairs")

var r = record

let pubkey = r.get(PublicKey)
if pubkey.isNone() or pubkey.get() != pk.toPublicKey():
return err("Public key does not correspond with given private key")

var fields = newSeq[FieldPair]()

if ip4.isSome() and ip4.value().family == IpAddressFamily.IPv4:
fields.insert(("ip", ip4.value().address_v4.toField))
if ip6.isSome() and ip6.value().family == IpAddressFamily.IPv6:
fields.insert(("ip6", ip6.value().address_v6.toField))

if tcp4Port.isSome():
fields.insert(("tcp", tcp4Port.value().uint16.toField))
if udp4Port.isSome():
fields.insert(("udp", udp4Port.value().uint16.toField))

# From the ENR specification:
# "Declaring the same port number in both tcp, tcp6 or udp, udp6 should be avoided
# but doesn't render the record invalid."
if tcp6Port.isSome():
if tcp6Port != tcp4Port:
fields.insert(("tcp6", tcp6Port.value().uint16.toField))
if udp6Port.isSome():
if udp6Port != udp4Port:
fields.insert(("udp6", udp6Port.value().uint16.toField))

r.pairs.insert fields
r.pairs.insert extraFields

if r.seqNum == high(type r.seqNum): # highly unlikely
return err("Maximum sequence number reached")
r.seqNum.inc()

r.raw = ? makeEnrRaw(r.seqNum, pk, r.pairs)
record = r

ok()

func tryGet*(r: Record, key: string, T: type): Opt[T] =
## Get the value from the provided key.
## Return `none` if the key does not exist or if the value is invalid
Expand Down
19 changes: 19 additions & 0 deletions eth/net/nat.nim
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ proc getRoutePrefSrc(bindIp: IpAddress): (Opt[IpAddress], PrefSrcStatus) =
else:
return (Opt.none(IpAddress), BindAddressIsPrivate)

proc getRoutePrefSrcv6*(bindIp: IpAddress): Opt[IpAddress] =
let bindAddress = initTAddress(bindIp, Port(0))

if bindAddress.isAnyLocal():
let ip = getRouteIpv6()
if ip.isErr():
# No route was found, log error and continue without IP.
error "No routable IP address found, check your network connection",
error = ip.error
Opt.none(IpAddress)
elif ip.get().isGlobalUnicast():
Opt.some(ip.get())
else:
Opt.none(IpAddress)
elif bindAddress.isGlobalUnicast():
Opt.some(bindIp)
else:
Opt.none(IpAddress)

# Try to detect a public IP assigned to this host, before trying NAT traversal.
proc getPublicRoutePrefSrcOrExternalIP*(
natStrategy: NatStrategy, bindIp: IpAddress, quiet = true):
Expand Down
19 changes: 19 additions & 0 deletions eth/net/utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ proc getRouteIpv4*(): Result[IpAddress, cstring] =
return err("Invalid IP address")
ok(ip)

proc getRouteIpv6*(): Result[IpAddress, cstring] =
# Avoiding Exception with initTAddress and can't make it work with static.
# Note: `publicAddress` is only used an "example" IP to find the best route,
# no data is send over the network to this IP!
let
publicAddress = TransportAddress(family: AddressFamily.IPv6,
address_v6: [1'u8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], port: Port(0))
route = getBestRoute(publicAddress)

if route.source.isUnspecified():
err("No best ipv6 route found")
else:
let ip = try: route.source.address()
except ValueError as e:
# This should not occur really.
error "Address conversion error", exception = e.name, msg = e.msg
return err("Invalid IP address")
ok(ip)

func ipv4*(address: array[4, byte]): IpAddress =
IpAddress(family: IPv4, address_v4: address)

Expand Down
65 changes: 58 additions & 7 deletions eth/p2p/discoveryv5/node.nim
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type
id*: NodeId
pubkey*: PublicKey
address*: Opt[Address]
address6*: Opt[Address]
record*: Record
seen*: bool ## Indicates if there was at least one successful
## request-response with this node.
Expand All @@ -41,14 +42,26 @@ func toNodeId*(pk: PublicKey): NodeId =
func fromRecord*(T: type Node, r: Record): T =
## Create a new `Node` from a `Record`.
let tr = TypedRecord.fromRecord(r)
if tr.ip.isSome() and tr.udp.isSome():
let a = Address(ip: ipv4(tr.ip.get()), port: Port(tr.udp.get()))

Node(id: r.publicKey.toNodeId(), pubkey: r.publicKey, record: r,
address: Opt.some(a))
else:
Node(id: r.publicKey.toNodeId(), pubkey: r.publicKey, record: r,
address: Opt.none(Address))
let address =
if tr.ip.isSome() and tr.udp.isSome():
Opt.some(Address(ip: ipv4(tr.ip.get()), port: Port(tr.udp.get())))
else:
Opt.none(Address)

let address6 =
if tr.ip6.isSome():
if tr.udp.isSome():
Opt.some(Address(ip: ipv6(tr.ip6.get()), port: Port(tr.udp.get())))
elif tr.udp6.isSome():
Opt.some(Address(ip: ipv6(tr.ip6.get()), port: Port(tr.udp6.get())))
else:
Opt.none(Address)
else:
Opt.none(Address)

Node(id: r.publicKey.toNodeId(), pubkey: r.publicKey, record: r,
address: address, address6: address6)

func newNode*(r: Record): Result[Node, cstring] {.deprecated: "Use TypedRecord.fromRecord instead".} =
## Create a new `Node` from a `Record`.
Expand All @@ -74,6 +87,44 @@ func update*(n: Node, pk: PrivateKey, ip: Opt[IpAddress],

ok()

func preferredAddress*(remote: Node, local: Node): Opt[Address] =
## Returns the preferred address of the remote node.
## If both IPv6 and IPv4 addresses are valid, IPv6 is preferred over IPv4 address.

func isValid(localAddr: Address, remoteAddr: Address): bool =
let
localTA = initTAddress(localAddr.ip, localAddr.port)
remoteTA = initTAddress(remoteAddr.ip, remoteAddr.port)

# If remote is public, always valid
if remoteTA.isGlobalUnicast():
return true

# If remote is not public, both must be loopback or private
(localTA.isLoopback() and remoteTA.isLoopback()) or
(localTA.isSiteLocal() and remoteTA.isSiteLocal())

template tryAddress(localOpt, remoteOpt: Opt[Address]): Opt[Address] =
if localOpt.isSome() and remoteOpt.isSome():
if isValid(localOpt.get(), remoteOpt.get()):
remoteOpt
else:
Opt.none(Address)
else:
Opt.none(Address)

# Try IPv6 first (preferred), then IPv4
let v6Result = tryAddress(local.address6, remote.address6)
if v6Result.isSome():
return v6Result

tryAddress(local.address, remote.address)

func hasAddress*(n: Node, a: Address): bool =
## Returns true if the given address matches either the IPv4 or IPv6 address of the node.
(n.address.isSome() and n.address.get() == a) or
(n.address6.isSome() and n.address6.get() == a)

func hash*(n: Node): hashes.Hash = hash(n.pubkey.toRaw)

func `==`*(a, b: Node): bool =
Expand Down
26 changes: 18 additions & 8 deletions eth/p2p/discoveryv5/nodes_verification.nim
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ func validIp(sender, address: IpAddress): bool =
false

proc verifyNodesRecords(
enrs: openArray[Record], src: Node, nodesLimit: int,
enrs: openArray[Record], src: Node, local: Node, nodesLimit: int,
distances: Opt[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.
## The src Node must always have an address set as this call is to be used
## when verifying nodes returned from a FindNodes request.
## The local Node is used to ensure we can actually communicate with the
## returned nodes (i.e., we have a matching address family).
logScope:
sender = src.record.toURI

Expand Down Expand Up @@ -66,9 +70,15 @@ proc verifyNodesRecords(
# 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(src.address.get().ip, n.address.get().ip):
trace "Invalid ip-address", record = n.record.toURI, node = n
if preferredAddress(n, src).isNone():
trace "Invalid ip-address from src perspective", record = n.record.toURI, node = n
continue
# Check if local node can actually communicate with this node
# (i.e., has a compatible address family)
# TODO: This is not sufficient, as the local node can also not have an ENR IP set but still contact
# nodes over IPv4/IPv6
if preferredAddress(n, local).isNone():
trace "No compatible address family with local node", record = n.record.toURI, node = n
continue
# Check if returned node has one of the requested distances.
if distances.isSome():
Expand All @@ -84,10 +94,10 @@ proc verifyNodesRecords(
result.add(n)

proc verifyNodesRecords*(
enrs: openArray[Record], src: Node, nodesLimit: int): seq[Node] =
verifyNodesRecords(enrs, src, nodesLimit, Opt.none(seq[uint16]))
enrs: openArray[Record], src: Node, local: Node, nodesLimit: int): seq[Node] =
verifyNodesRecords(enrs, src, local, nodesLimit, Opt.none(seq[uint16]))

proc verifyNodesRecords*(
enrs: openArray[Record], src: Node, nodesLimit: int,
enrs: openArray[Record], src: Node, local: Node, nodesLimit: int,
distances: seq[uint16]): seq[Node] =
verifyNodesRecords(enrs, src, nodesLimit, Opt.some(distances))
verifyNodesRecords(enrs, src, local, nodesLimit, Opt.some(distances))
Loading
Loading