diff --git a/eth/enr/enr.nim b/eth/enr/enr.nim index 34c1dd9b..c9696590 100644 --- a/eth/enr/enr.nim +++ b/eth/enr/enr.nim @@ -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") @@ -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 @@ -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 diff --git a/eth/net/nat.nim b/eth/net/nat.nim index eb7648f1..8cdac649 100644 --- a/eth/net/nat.nim +++ b/eth/net/nat.nim @@ -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): diff --git a/eth/net/utils.nim b/eth/net/utils.nim index e4f995d5..057aebf6 100644 --- a/eth/net/utils.nim +++ b/eth/net/utils.nim @@ -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) diff --git a/eth/p2p/discoveryv5/node.nim b/eth/p2p/discoveryv5/node.nim index 832cfa24..aac76d36 100644 --- a/eth/p2p/discoveryv5/node.nim +++ b/eth/p2p/discoveryv5/node.nim @@ -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. @@ -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`. @@ -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 = diff --git a/eth/p2p/discoveryv5/nodes_verification.nim b/eth/p2p/discoveryv5/nodes_verification.nim index 3210511a..f96df140 100644 --- a/eth/p2p/discoveryv5/nodes_verification.nim +++ b/eth/p2p/discoveryv5/nodes_verification.nim @@ -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 @@ -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(): @@ -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)) diff --git a/eth/p2p/discoveryv5/protocol.nim b/eth/p2p/discoveryv5/protocol.nim index bd538608..6c3fc823 100644 --- a/eth/p2p/discoveryv5/protocol.nim +++ b/eth/p2p/discoveryv5/protocol.nim @@ -299,8 +299,9 @@ proc send*(d: Protocol, a: Address, data: seq[byte]) = asyncSpawn sendTo(d, a, data) proc send(d: Protocol, n: Node, data: seq[byte]) = - doAssert(n.address.isSome()) - d.send(n.address.get(), data) + let address = preferredAddress(n, d.localNode) + doAssert(address.isSome()) + d.send(address.get(), data) proc sendNodes(d: Protocol, toId: NodeId, toAddr: Address, reqId: RequestId, nodes: openArray[Node]) = @@ -494,9 +495,10 @@ proc receive*(d: Protocol, a: Address, packet: openArray[byte]) = var pr: PendingRequest if d.pendingRequests.take(packet.whoareyou.requestNonce, pr): let toNode = pr.node + let addressOpt = preferredAddress(toNode, d.localNode) # This is a node we previously contacted and thus must have an address. - doAssert(toNode.address.isSome()) - let address = toNode.address.get() + doAssert(addressOpt.isSome()) + let address = addressOpt.get() let data = encodeHandshakePacket(d.rng[], d.codec, toNode.id, address, pr.message, packet.whoareyou, toNode.pubkey) @@ -524,7 +526,7 @@ proc receive*(d: Protocol, a: Address, packet: openArray[byte]) = # The ENR could contain bogus IPs and although they would get removed # on the next revalidation, one could spam these as the handshake # message occurs on (first) incoming messages. - if node.address.isSome() and a == node.address.get(): + if node.hasAddress(a): if d.addNode(node): trace "Added new node to routing table after handshake", node @@ -610,9 +612,14 @@ proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId): proc sendMessage*[T: SomeMessage](d: Protocol, toNode: Node, m: T): RequestId = - doAssert(toNode.address.isSome()) + let addressOpt = preferredAddress(toNode, d.localNode) + doAssert(addressOpt.isSome()) + # if addressOpt.isNone(): + # warn "No preferred address found for node, cannot send message", + # nodeId = toNode.id + let - address = toNode.address.get() + address = addressOpt.get() reqId = RequestId.init(d.rng[]) message = encodeMessage(m, reqId) @@ -665,7 +672,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, findNodeResultLimit, distances) + let res = verifyNodesRecords(nodes.get(), toNode, d.localNode, findNodeResultLimit, distances) d.routingTable.setJustSeen(toNode) return ok(res.filterIt(not d.isBanned(it.id))) else: @@ -1144,31 +1151,33 @@ proc newProtocol*( privKey: PrivateKey, enrIp: Opt[IpAddress], enrTcpPort, enrUdpPort: Opt[Port], - localEnrFields: openArray[(string, seq[byte])] = [], + enrIp6: Opt[IpAddress], + enrTcp6Port, enrUdp6Port: Opt[Port], + localEnrFields: openArray[FieldPair] = [], bootstrapRecords: openArray[Record] = [], previousRecord = Opt.none(enr.Record), bindPort: Port, bindIp: Opt[IpAddress], enrAutoUpdate = false, + banNodes = false, config = defaultDiscoveryConfig, rng = newRng()): Protocol = - let - customEnrFields = mapIt(localEnrFields, toFieldPair(it[0], it[1])) - record = - if previousRecord.isSome(): - var res = previousRecord.get() - # TODO: this is faulty in case the intent is to remove a field with - # opt.none - res.update(privKey, enrIp, enrTcpPort, enrUdpPort, - customEnrFields).expect("Record within size limits and correct key") - res - else: - enr.Record.init(1, privKey, enrIp, enrTcpPort, enrUdpPort, - customEnrFields).expect("Record within size limits") + let record = + if previousRecord.isSome(): + var res = previousRecord.get() + # TODO: this is faulty in case the intent is to remove a field with + # opt.none + res.updateDS(privKey, enrIp, enrTcpPort, enrUdpPort, enrIp6, enrTcp6Port, + enrUdp6Port, + localEnrFields).expect("Record within size limits and correct key") + res + else: + enr.Record.initDS(1, privKey, enrIp, enrTcpPort, enrUdpPort, enrIp6, enrTcp6Port, enrUdp6Port, + localEnrFields).expect("Record within size limits") info "Discovery ENR initialized", enrAutoUpdate, seqNum = record.seqNum, - ip = enrIp, tcpPort = enrTcpPort, udpPort = enrUdpPort, - customEnrFields, uri = toURI(record) + ip = enrIp, ip6 = enrIp6, tcpPort = enrTcpPort, udpPort = enrUdpPort, + localEnrFields, uri = toURI(record) if enrIp.isNone(): if enrAutoUpdate: @@ -1194,10 +1203,34 @@ proc newProtocol*( enrAutoUpdate: enrAutoUpdate, routingTable: RoutingTable.init( node, config.bitsPerHop, config.tableIpLimits, rng), + banNodes: banNodes, handshakeTimeout: config.handshakeTimeout, responseTimeout: config.responseTimeout, rng: rng) +proc newProtocol*( + privKey: PrivateKey, + enrIp: Opt[IpAddress], + enrTcpPort, enrUdpPort: Opt[Port], + enrIp6: Opt[IpAddress], + enrTcp6Port, enrUdp6Port: Opt[Port], + localEnrFields: openArray[(string, seq[byte])] = [], + bootstrapRecords: openArray[Record] = [], + previousRecord = Opt.none(enr.Record), + bindPort: Port, + bindIp: Opt[IpAddress], + enrAutoUpdate = false, + banNodes = false, + config = defaultDiscoveryConfig, + rng = newRng(), +): Protocol = + let customEnrFields = mapIt(localEnrFields, toFieldPair(it[0], it[1])) + newProtocol( + privKey, enrIp, enrTcpPort, enrUdpPort, enrIp6, enrTcp6Port, enrUdp6Port, + customEnrFields, bootstrapRecords, + previousRecord, bindPort, bindIp, enrAutoUpdate, banNodes, config, rng, + ) + proc `$`*(a: OptAddress): string = if a.ip.isNone(): "*:" & $a.port diff --git a/eth/p2p/discoveryv5/routing_table.nim b/eth/p2p/discoveryv5/routing_table.nim index 342293b8..5adffcbf 100644 --- a/eth/p2p/discoveryv5/routing_table.nim +++ b/eth/p2p/discoveryv5/routing_table.nim @@ -174,7 +174,7 @@ func ipLimitInc(r: var RoutingTable, b: KBucket, n: Node): bool = ## the specified `Node` its ip. ## When one of the ip limits is reached return false, else increment them and ## return true. - let ip = n.address.get().ip # Node from table should always have an address + let ip = n.preferredAddress(r.localNode).get().ip # Node from table should always have an address # Check ip limit for bucket if not b.ipLimits.inc(ip): return false @@ -188,7 +188,7 @@ func ipLimitInc(r: var RoutingTable, b: KBucket, n: Node): bool = func ipLimitDec(r: var RoutingTable, b: KBucket, n: Node) = ## Decrement the ip limits of the routing table and the bucket for the ## specified `Node` its ip. - let ip = n.address.get().ip # Node from table should always have an address + let ip = n.preferredAddress(r.localNode).get().ip # Node from table should always have an address b.ipLimits.dec(ip) r.ipLimits.dec(ip) @@ -253,7 +253,7 @@ proc remove(k: KBucket, n: Node): bool = else: false -func split(k: KBucket): tuple[lower, upper: KBucket] = +func split(r: var RoutingTable, k: KBucket): tuple[lower, upper: KBucket] = ## Split the kbucket `k` at the median id. let splitid = k.midpoint result.lower = KBucket.new(k.istart, splitid, k.ipLimits.limit) @@ -264,13 +264,13 @@ func split(k: KBucket): tuple[lower, upper: KBucket] = # Ip limits got reset because of the KBucket.new, so there is the need to # increment again for each added node. It should however never fail as the # previous bucket had the same limits. - doAssert(bucket.ipLimits.inc(node.address.get().ip), + doAssert(bucket.ipLimits.inc(node.preferredAddress(r.localNode).get().ip), "IpLimit increment should work as all buckets have the same limits") for node in k.replacementCache: let bucket = if node.id <= splitid: result.lower else: result.upper bucket.replacementCache.add(node) - doAssert(bucket.ipLimits.inc(node.address.get().ip), + doAssert(bucket.ipLimits.inc(node.preferredAddress(r.localNode).get().ip), "IpLimit increment should work as all buckets have the same limits") func inRange(k: KBucket, n: Node): bool = @@ -328,7 +328,7 @@ func init*(T: type RoutingTable, localNode: Node, bitsPerHop = DefaultBitsPerHop func splitBucket(r: var RoutingTable, index: int) = let bucket = r.buckets[index] - let (a, b) = bucket.split() + let (a, b) = r.split(bucket) r.buckets[index] = a r.buckets.insert(b, index + 1) @@ -350,7 +350,7 @@ func addReplacement(r: var RoutingTable, k: KBucket, n: Node): NodeStatus = if k.replacementCache[nodeIdx].record.seqNum <= n.record.seqNum: # In case the record sequence number is higher or the same, the new node # gets moved to the tail. - if k.replacementCache[nodeIdx].address.get().ip != n.address.get().ip: + if k.replacementCache[nodeIdx].preferredAddress(r.localNode).get().ip != n.preferredAddress(r.localNode).get().ip: if not ipLimitInc(r, k, n): return IpLimitReached ipLimitDec(r, k, k.replacementCache[nodeIdx]) @@ -387,7 +387,7 @@ proc addNode*(r: var RoutingTable, n: Node): NodeStatus = # Don't allow nodes without an address field in the ENR to be added. # This could also be reworked by having another Node type that always has an # address. - if n.address.isNone(): + if n.preferredAddress(r.localNode).isNone(): return NoAddress if n == r.localNode: @@ -404,7 +404,7 @@ proc addNode*(r: var RoutingTable, n: Node): NodeStatus = if nodeIdx != -1: if bucket.nodes[nodeIdx].record.seqNum < n.record.seqNum: # In case of a newer record, it gets replaced. - if bucket.nodes[nodeIdx].address.get().ip != n.address.get().ip: + if bucket.nodes[nodeIdx].preferredAddress(r.localNode).get().ip != n.preferredAddress(r.localNode).get().ip: if not ipLimitInc(r, bucket, n): return IpLimitReached ipLimitDec(r, bucket, bucket.nodes[nodeIdx]) diff --git a/tools/dcli.nim b/tools/dcli.nim index de0fcd26..5250170e 100644 --- a/tools/dcli.nim +++ b/tools/dcli.nim @@ -21,9 +21,7 @@ import ../eth/p2p/discoveryv5/protocol as discv5_protocol const - defaultListenAddress* = (static parseIpAddress("0.0.0.0")) defaultAdminListenAddress* = (static parseIpAddress("127.0.0.1")) - defaultListenAddressDesc = $defaultListenAddress defaultAdminListenAddressDesc = $defaultAdminListenAddress type @@ -46,10 +44,9 @@ type name: "udp-port" .}: uint16 listenAddress* {. - defaultValue: defaultListenAddress - defaultValueDesc: $defaultListenAddressDesc desc: "Listening address for the Discovery v5 traffic" - name: "listen-address" }: IpAddress + defaultValueDesc: "*" + name: "listen-address" .}: Option[IpAddress] persistingFile* {. defaultValue: "peerstore.csv", @@ -68,6 +65,12 @@ type defaultValue: NatConfig(hasExtIp: false, nat: NatAny) name: "nat" .}: NatConfig + enrIpv6Address* {. + hidden, + desc: "Specifies the IPv6 address to be advertised in the node’s ENR", + name: "debug-enr-ipv6-address" + .}: Option[IpAddress] + enrAutoUpdate* {. defaultValue: false desc: "Discovery can automatically update its ENR with the IP address " & @@ -352,21 +355,62 @@ proc discover( proc setupNode(config: DiscoveryConf): discv5_protocol.Protocol {.raises: [CatchableError].} = let - bindIp = config.listenAddress + listenAddress = + if config.listenAddress.isSome(): + config.listenAddress.get() + else: + getAutoAddress(Port(0)).toIpAddress() udpPort = Port(config.udpPort) - (extIp, extPorts) = setupAddress( - config.nat, - config.listenAddress, - @[(port: udpPort, protocol: PortProtocol.UDP)], - "dcli", - ) - extUdpPort = extPorts[0].toPort() - - let d = newProtocol(config.nodeKey, - extIp, Opt.none(Port), extUdpPort, - bootstrapRecords = config.bootstrapNodes, - bindIp = bindIp, bindPort = udpPort, - enrAutoUpdate = config.enrAutoUpdate) + (extIp4, extIp6, extUdpPort) = + if listenAddress == AnyAddress6.toIpAddress(): + let + extIp6 = + if config.enrIpv6Address.isSome(): + Opt.some(config.enrIpv6Address.get()) + else: + getRoutePrefSrcv6(listenAddress) + + (extIp4, extPorts) = setupAddress( + config.nat, + listenAddress, + @[(port: udpPort, protocol: PortProtocol.UDP)], + "dcli", + ) + extUdpPort = extPorts[0].toPort() + + (extIp4, extIp6, extUdpPort) + elif listenAddress.family == IpAddressFamily.IPv6: + let extIp6 = + if config.enrIpv6Address.isSome(): + Opt.some(config.enrIpv6Address.get()) + else: + getRoutePrefSrcv6(listenAddress) + (Opt.none(IpAddress), extIp6, Opt.some(udpPort)) + else: # listenAddress.family == IpAddressFamily.IPv4 + let + (extIp4, extPorts) = setupAddress( + config.nat, + listenAddress, + @[(port: udpPort, protocol: PortProtocol.UDP)], + "dcli", + ) + extUdpPort = extPorts[0].toPort() + + (extIp4, Opt.none(IpAddress), extUdpPort) + + let d = newProtocol( + config.nodeKey, + extIp4, + Opt.none(Port), + extUdpPort, + extIp6, + Opt.none(Port), + extUdpPort, + bootstrapRecords = config.bootstrapNodes, + bindIp = Opt.some(listenAddress), + bindPort = udpPort, + enrAutoUpdate = config.enrAutoUpdate, + ) d.open() if config.metricsEnabled: