From 348e61364e824938e11393d8e01d4bb17463f885 Mon Sep 17 00:00:00 2001 From: munna0908 Date: Wed, 18 Dec 2024 15:00:11 +0530 Subject: [PATCH] add support for uPnP and PMP nat traversal --- codex.nim | 7 - codex/codex.nim | 29 +-- codex/discovery.nim | 11 +- codex/nat.nim | 419 ++++++++++++++++++++++++++++++++++++++ codex/utils.nim | 30 +++ codex/utils/addrutils.nim | 57 ++++++ codex/utils/natutils.nim | 68 +++++++ 7 files changed, 584 insertions(+), 37 deletions(-) create mode 100644 codex/nat.nim create mode 100644 codex/utils/natutils.nim diff --git a/codex.nim b/codex.nim index 79e0ef68..477041c3 100644 --- a/codex.nim +++ b/codex.nim @@ -55,13 +55,6 @@ when isMainModule: config.setupLogging() config.setupMetrics() - if config.nat == ValidIpAddress.init(IPv4_any()): - error "`--nat` cannot be set to the any (`0.0.0.0`) address" - quit QuitFailure - - if config.nat == ValidIpAddress.init("127.0.0.1"): - warn "`--nat` is set to loopback, your node wont properly announce over the DHT" - if not(checkAndCreateDataDir((config.dataDir).string)): # We are unable to access/create data folder or data folder's # permissions are insecure. diff --git a/codex/codex.nim b/codex/codex.nim index 441bdf88..a3e1ef99 100644 --- a/codex/codex.nim +++ b/codex/codex.nim @@ -44,6 +44,7 @@ import ./utils/addrutils import ./namespaces import ./codextypes import ./logutils +import ./nat logScope: topics = "codex node" @@ -158,30 +159,13 @@ proc start*(s: CodexServer) {.async.} = await s.codexNode.switch.start() - let - # TODO: Can't define these as constants, pity - natIpPart = MultiAddress.init("/ip4/" & $s.config.nat & "/") - .expect("Should create multiaddress") - anyAddrIp = MultiAddress.init("/ip4/0.0.0.0/") - .expect("Should create multiaddress") - loopBackAddrIp = MultiAddress.init("/ip4/127.0.0.1/") - .expect("Should create multiaddress") - - # announce addresses should be set to bound addresses, - # but the IP should be mapped to the provided nat ip - announceAddrs = s.codexNode.switch.peerInfo.addrs.mapIt: - block: - let - listenIPPart = it[multiCodec("ip4")].expect("Should get IP") - - if listenIPPart == anyAddrIp or - (listenIPPart == loopBackAddrIp and natIpPart != loopBackAddrIp): - it.remapAddr(s.config.nat.some) - else: - it + let (announceAddrs,discoveryAddrs)= natedAddress( + s.config.nat, + s.config.listenAddrs, + s.config.discoveryPort) s.codexNode.discovery.updateAnnounceRecord(announceAddrs) - s.codexNode.discovery.updateDhtRecord(s.config.nat, s.config.discoveryPort) + s.codexNode.discovery.updateDhtRecord(discoveryAddrs) await s.bootstrapInteractions() await s.codexNode.start() @@ -243,7 +227,6 @@ proc new*( discovery = Discovery.new( switch.peerInfo.privateKey, announceAddrs = config.listenAddrs, - bindIp = config.discoveryIp, bindPort = config.discoveryPort, bootstrapNodes = config.bootstrapNodes, store = discoveryStore) diff --git a/codex/discovery.nim b/codex/discovery.nim index 47ac950d..d2ea1147 100644 --- a/codex/discovery.nim +++ b/codex/discovery.nim @@ -146,17 +146,14 @@ proc updateAnnounceRecord*(d: Discovery, addrs: openArray[MultiAddress]) = d.protocol.updateRecord(d.providerRecord) .expect("Should update SPR") -proc updateDhtRecord*(d: Discovery, ip: ValidIpAddress, port: Port) = +proc updateDhtRecord*(d: Discovery, addrs: openArray[MultiAddress]) = ## Update providers record ## - trace "Updating Dht record", ip, port = $port + trace "Updating Dht record", addrs = addrs d.dhtRecord = SignedPeerRecord.init( - d.key, PeerRecord.init(d.peerId, @[ - MultiAddress.init( - ip, - IpTransportProtocol.udpProtocol, - port)])).expect("Should construct signed record").some + d.key, PeerRecord.init(d.peerId, @addrs)) + .expect("Should construct signed record").some if not d.protocol.isNil: d.protocol.updateRecord(d.dhtRecord) diff --git a/codex/nat.nim b/codex/nat.nim new file mode 100644 index 00000000..317f3546 --- /dev/null +++ b/codex/nat.nim @@ -0,0 +1,419 @@ +# Copyright (c) 2019-2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [].} + +import + std/[options, os, strutils, times, net],stew/shims/net as stewNet, + stew/[objects,results], nat_traversal/[miniupnpc, natpmp], + json_serialization/std/net + +import pkg/chronos +import pkg/chronicles +import pkg/libp2p + +import ./utils +import ./utils/natutils +import ./utils/addrutils + +const + UPNP_TIMEOUT = 200 # ms + PORT_MAPPING_INTERVAL = 20 * 60 # seconds + NATPMP_LIFETIME = 60 * 60 # in seconds, must be longer than PORT_MAPPING_INTERVAL + +var + upnp {.threadvar.}: Miniupnp + npmp {.threadvar.}: NatPmp + strategy = NatStrategy.NatNone + internalTcpPort: Port + externalTcpPort: Port + internalUdpPort: Port + externalUdpPort: Port + +logScope: + topics = "nat" + +type + PrefSrcStatus = enum + NoRoutingInfo + PrefSrcIsPublic + PrefSrcIsPrivate + BindAddressIsPublic + BindAddressIsPrivate + +## Also does threadvar initialisation. +## Must be called before redirectPorts() in each thread. +proc getExternalIP*(natStrategy: NatStrategy, quiet = false): Option[IpAddress] = + var externalIP: IpAddress + + if natStrategy == NatStrategy.NatAny or natStrategy == NatStrategy.NatUpnp: + if upnp == nil: + upnp = newMiniupnp() + + upnp.discoverDelay = UPNP_TIMEOUT + let dres = upnp.discover() + if dres.isErr: + debug "UPnP", msg = dres.error + else: + var + msg: cstring + canContinue = true + case upnp.selectIGD(): + of IGDNotFound: + msg = "Internet Gateway Device not found. Giving up." + canContinue = false + of IGDFound: + msg = "Internet Gateway Device found." + of IGDNotConnected: + msg = "Internet Gateway Device found but it's not connected. Trying anyway." + of NotAnIGD: + msg = "Some device found, but it's not recognised as an Internet Gateway Device. Trying anyway." + if not quiet: + debug "UPnP", msg + if canContinue: + let ires = upnp.externalIPAddress() + if ires.isErr: + debug "UPnP", msg = ires.error + else: + # if we got this far, UPnP is working and we don't need to try NAT-PMP + try: + externalIP = parseIpAddress(ires.value) + strategy = NatStrategy.NatUpnp + return some(externalIP) + except ValueError as e: + error "parseIpAddress() exception", err = e.msg + return + + if natStrategy == NatStrategy.NatAny or natStrategy == NatStrategy.NatPmp: + if npmp == nil: + npmp = newNatPmp() + let nres = npmp.init() + if nres.isErr: + debug "NAT-PMP", msg = nres.error + else: + let nires = npmp.externalIPAddress() + if nires.isErr: + debug "NAT-PMP", msg = nires.error + else: + try: + externalIP = parseIpAddress($(nires.value)) + strategy = NatPmp + return some(externalIP) + except ValueError as e: + error "parseIpAddress() exception", err = e.msg + return + +# This queries the routing table to get the "preferred source" attribute and +# checks if it's a public IP. If so, then it's our public IP. +# +# Further more, we check if the bind address (user provided, or a "0.0.0.0" +# default) is a public IP. That's a long shot, because code paths involving a +# user-provided bind address are not supposed to get here. +proc getRoutePrefSrc( + bindIp: ValidIpAddress): (Option[ValidIpAddress], PrefSrcStatus) = + let bindAddress = initTAddress(bindIp, Port(0)) + + if bindAddress.isAnyLocal(): + let ip = getRouteIpv4() + 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 + return (none(ValidIpAddress), NoRoutingInfo) + elif ip.get().isGlobalUnicast(): + return (some(ip.get()), PrefSrcIsPublic) + else: + return (none(ValidIpAddress), PrefSrcIsPrivate) + elif bindAddress.isGlobalUnicast(): + return (some(ValidIpAddress.init(bindIp)), BindAddressIsPublic) + else: + return (none(ValidIpAddress), BindAddressIsPrivate) + +# Try to detect a public IP assigned to this host, before trying NAT traversal. +proc getPublicRoutePrefSrcOrExternalIP*(natStrategy: NatStrategy, bindIp: ValidIpAddress, quiet = true): Option[ValidIpAddress] = + let (prefSrcIp, prefSrcStatus) = getRoutePrefSrc(bindIp) + + case prefSrcStatus: + of NoRoutingInfo, PrefSrcIsPublic, BindAddressIsPublic: + return prefSrcIp + of PrefSrcIsPrivate, BindAddressIsPrivate: + let extIp = getExternalIP(natStrategy, quiet) + if extIp.isSome: + return some(ValidIpAddress.init(extIp.get)) + +proc doPortMapping(tcpPort, udpPort: Port, description: string): Option[(Port, Port)] {.gcsafe.} = + var + extTcpPort: Port + extUdpPort: Port + + if strategy == NatStrategy.NatUpnp: + for t in [(tcpPort, UPNPProtocol.TCP), (udpPort, UPNPProtocol.UDP)]: + let + (port, protocol) = t + pmres = upnp.addPortMapping(externalPort = $port, + protocol = protocol, + internalHost = upnp.lanAddr, + internalPort = $port, + desc = description, + leaseDuration = 0) + if pmres.isErr: + error "UPnP port mapping", msg = pmres.error, port + return + else: + # let's check it + let cres = upnp.getSpecificPortMapping(externalPort = $port, + protocol = protocol) + if cres.isErr: + warn "UPnP port mapping check failed. Assuming the check itself is broken and the port mapping was done.", msg = cres.error + + info "UPnP: added port mapping", externalPort = port, internalPort = port, protocol = protocol + case protocol: + of UPNPProtocol.TCP: + extTcpPort = port + of UPNPProtocol.UDP: + extUdpPort = port + elif strategy == NatStrategy.NatPmp: + for t in [(tcpPort, NatPmpProtocol.TCP), (udpPort, NatPmpProtocol.UDP)]: + let + (port, protocol) = t + pmres = npmp.addPortMapping(eport = port.cushort, + iport = port.cushort, + protocol = protocol, + lifetime = NATPMP_LIFETIME) + if pmres.isErr: + error "NAT-PMP port mapping", msg = pmres.error, port + return + else: + let extPort = Port(pmres.value) + info "NAT-PMP: added port mapping", externalPort = extPort, internalPort = port, protocol = protocol + case protocol: + of NatPmpProtocol.TCP: + extTcpPort = extPort + of NatPmpProtocol.UDP: + extUdpPort = extPort + return some((extTcpPort, extUdpPort)) + +type PortMappingArgs = tuple[tcpPort, udpPort: Port, description: string] +var + natThread: Thread[PortMappingArgs] + natCloseChan: Channel[bool] + +proc repeatPortMapping(args: PortMappingArgs) {.thread, raises: [ValueError].} = + ignoreSignalsInThread() + let + (tcpPort, udpPort, description) = args + interval = initDuration(seconds = PORT_MAPPING_INTERVAL) + sleepDuration = 1_000 # in ms, also the maximum delay after pressing Ctrl-C + + var lastUpdate = now() + + # We can't use copies of Miniupnp and NatPmp objects in this thread, because they share + # C pointers with other instances that have already been garbage collected, so + # we use threadvars instead and initialise them again with getExternalIP(), + # even though we don't need the external IP's value. + let ipres = getExternalIP(strategy, quiet = true) + if ipres.isSome: + while true: + # we're being silly here with this channel polling because we can't + # select on Nim channels like on Go ones + let (dataAvailable, _) = try: natCloseChan.tryRecv() + except Exception: (false, false) + if dataAvailable: + return + else: + let currTime = now() + if currTime >= (lastUpdate + interval): + discard doPortMapping(tcpPort, udpPort, description) + lastUpdate = currTime + sleep(sleepDuration) + +proc stopNatThread() {.noconv.} = + # stop the thread + + try: + natCloseChan.send(true) + natThread.joinThread() + natCloseChan.close() + except Exception as exc: + warn "Failed to stop NAT port mapping renewal thread", exc = exc.msg + + # delete our port mappings + + # FIXME: if the initial port mapping failed because it already existed for the + # required external port, we should not delete it. It might have been set up + # by another program. + + # In Windows, a new thread is created for the signal handler, so we need to + # initialise our threadvars again. + let ipres = getExternalIP(strategy, quiet = true) + if ipres.isSome: + if strategy == NatStrategy.NatUpnp: + for t in [(externalTcpPort, internalTcpPort, UPNPProtocol.TCP), (externalUdpPort, internalUdpPort, UPNPProtocol.UDP)]: + let + (eport, iport, protocol) = t + pmres = upnp.deletePortMapping(externalPort = $eport, + protocol = protocol) + if pmres.isErr: + error "UPnP port mapping deletion", msg = pmres.error + else: + debug "UPnP: deleted port mapping", externalPort = eport, internalPort = iport, protocol = protocol + elif strategy == NatStrategy.NatPmp: + for t in [(externalTcpPort, internalTcpPort, NatPmpProtocol.TCP), (externalUdpPort, internalUdpPort, NatPmpProtocol.UDP)]: + let + (eport, iport, protocol) = t + pmres = npmp.deletePortMapping(eport = eport.cushort, + iport = iport.cushort, + protocol = protocol) + if pmres.isErr: + error "NAT-PMP port mapping deletion", msg = pmres.error + else: + debug "NAT-PMP: deleted port mapping", externalPort = eport, internalPort = iport, protocol = protocol + +proc redirectPorts*(tcpPort, udpPort: Port, description: string): Option[(Port, Port)] = + result = doPortMapping(tcpPort, udpPort, description) + if result.isSome: + (externalTcpPort, externalUdpPort) = result.get() + # needed by NAT-PMP on port mapping deletion + internalTcpPort = tcpPort + internalUdpPort = udpPort + # Port mapping works. Let's launch a thread that repeats it, in case the + # NAT-PMP lease expires or the router is rebooted and forgets all about + # these mappings. + natCloseChan.open() + try: + natThread.createThread(repeatPortMapping, (externalTcpPort, externalUdpPort, description)) + # atexit() in disguise + addQuitProc(stopNatThread) + except Exception as exc: + warn "Failed to create NAT port mapping renewal thread", exc = exc.msg + +proc setupNat*(natStrategy: NatStrategy, tcpPort, udpPort: Port, + clientId: string): + tuple[ip: Option[ValidIpAddress], tcpPort, udpPort: Option[Port]] = + ## Setup NAT port mapping and get external IP address. + ## If any of this fails, we don't return any IP address but do return the + ## original ports as best effort. + ## TODO: Allow for tcp or udp port mapping to be optional. + let extIp = getExternalIP(natStrategy) + if extIp.isSome: + let ip = ValidIpAddress.init(extIp.get) + let extPorts = ({.gcsafe.}: + redirectPorts(tcpPort = tcpPort, + udpPort = udpPort, + description = clientId)) + if extPorts.isSome: + let (extTcpPort, extUdpPort) = extPorts.get() + (ip: some(ip), tcpPort: some(extTcpPort), udpPort: some(extUdpPort)) + else: + warn "UPnP/NAT-PMP available but port forwarding failed" + (ip: none(ValidIpAddress), tcpPort: some(tcpPort), udpPort: some(udpPort)) + else: + warn "UPnP/NAT-PMP not available" + (ip: none(ValidIpAddress), tcpPort: some(tcpPort), udpPort: some(udpPort)) + +type + NatConfig* = object + case hasExtIp*: bool + of true: extIp*: ValidIpAddress + of false: nat*: NatStrategy + +func parseCmdArg*(T: type NatConfig, p: string): T {.raises: [ValueError].} = + case p.toLowerAscii: + of "any": + NatConfig(hasExtIp: false, nat: NatStrategy.NatAny) + of "none": + NatConfig(hasExtIp: false, nat: NatStrategy.NatNone) + of "upnp": + NatConfig(hasExtIp: false, nat: NatStrategy.NatUpnp) + of "pmp": + NatConfig(hasExtIp: false, nat: NatStrategy.NatPmp) + else: + if p.startsWith("extip:"): + try: + let ip = ValidIpAddress.init(p[6..^1]) + NatConfig(hasExtIp: true, extIp: ip) + except ValueError: + let error = "Not a valid IP address: " & p[6..^1] + raise newException(ValueError, error) + else: + let error = "Not a valid NAT option: " & p + raise newException(ValueError, error) + +proc completeCmdArg*(T: type NatConfig; val: string): seq[string] = + return @[] + + +proc setupAddress*(natConfig: NatConfig, bindIp: ValidIpAddress, + tcpPort, udpPort: Port, clientId: string): + tuple[ip: Option[ValidIpAddress], tcpPort, udpPort: Option[Port]] + {.gcsafe.} = + ## Set-up of the external address via any of the ways as configured in + ## `NatConfig`. In case all fails an error is logged and the bind ports are + ## selected also as external ports, as best effort and in hope that the + ## external IP can be figured out by other means at a later stage. + ## TODO: Allow for tcp or udp bind ports to be optional. + + if natConfig.hasExtIp: + # any required port redirection must be done by hand + return (some(natConfig.extIp), some(tcpPort), some(udpPort)) + + case natConfig.nat: + of NatStrategy.NatAny: + let (prefSrcIp, prefSrcStatus) = getRoutePrefSrc(bindIp) + + case prefSrcStatus: + of NoRoutingInfo, PrefSrcIsPublic, BindAddressIsPublic: + return (prefSrcIp, some(tcpPort), some(udpPort)) + of PrefSrcIsPrivate, BindAddressIsPrivate: + return setupNat(natConfig.nat, tcpPort, udpPort, clientId) + of NatStrategy.NatNone: + let (prefSrcIp, prefSrcStatus) = getRoutePrefSrc(bindIp) + + case prefSrcStatus: + of NoRoutingInfo, PrefSrcIsPublic, BindAddressIsPublic: + return (prefSrcIp, some(tcpPort), some(udpPort)) + of PrefSrcIsPrivate: + error "No public IP address found. Should not use --nat:none option" + return (none(ValidIpAddress), some(tcpPort), some(udpPort)) + of BindAddressIsPrivate: + error "Bind IP is not a public IP address. Should not use --nat:none option" + return (none(ValidIpAddress), some(tcpPort), some(udpPort)) + of NatStrategy.NatUpnp, NatStrategy.NatPmp: + return setupNat(natConfig.nat, tcpPort, udpPort, clientId) + +proc natedAddress*(natConfig: NatConfig, addrs: seq[MultiAddress], udpPort: Port): tuple[libp2p, discovery: seq[MultiAddress]] = + ## Takes a NAT configuration, sequence of multiaddresses and UDP port and returns: + ## - Modified multiaddresses with NAT-mapped addresses for libp2p + ## - Discovery addresses with NAT-mapped UDP ports + + var discoveryAddrs = newSeq[MultiAddress](0) + let + newAddrs = addrs.mapIt: + block: + # Extract IP address and port from the multiaddress + let (ipPart, port) = getAddressAndPort(it) + if ipPart.isSome and port.isSome: + # Try to setup NAT mapping for the address + let (newIP, tcp, udp) = setupAddress(natConfig, ipPart.get, port.get, udpPort, "codex") + if newIP.isSome: + # NAT mapping successful - add discovery address with mapped UDP port + discoveryAddrs.add(getMultiAddrWithIPAndUDPPort(newIP.get, udp.get)) + # Remap original address with NAT IP and TCP port + it.remapAddr(ip = newIP, port = tcp) + else: + # NAT mapping failed - use original address + echo "Failed to get external IP, using original address", it + discoveryAddrs.add(getMultiAddrWithIPAndUDPPort(ipPart.get, udpPort)) + it + else: + # Invalid multiaddress format - return as is + it + (newAddrs, discoveryAddrs) + + + \ No newline at end of file diff --git a/codex/utils.nim b/codex/utils.nim index 17dd924f..617f30cb 100644 --- a/codex/utils.nim +++ b/codex/utils.nim @@ -20,6 +20,8 @@ import ./utils/asynciter export asyncheapqueue, fileutils, asynciter, chronos +when defined(posix): + import os, posix func divUp*[T: SomeInteger](a, b : T): T = ## Division with result rounded up (rather than truncated as in 'div') @@ -94,3 +96,31 @@ when not declared(parseDuration): # Odd code formatting to minimize diff v. main result = start #..is no unit to the end of `s`. var sizeF = number * scale + 0.5 # Saturate to int64.high when too big size = seconds(int(sizeF)) + +# Block all/most signals in the current thread, so we don't interfere with regular signal +# handling elsewhere. +proc ignoreSignalsInThread*() = + when defined(posix): + var signalMask, oldSignalMask: Sigset + + # sigprocmask() doesn't work on macOS, for multithreaded programs + if sigfillset(signalMask) != 0: + echo osErrorMsg(osLastError()) + quit(QuitFailure) + when defined(boehmgc): + # Turns out Boehm GC needs some signals to deal with threads: + # https://www.hboehm.info/gc/debugging.html + const + SIGPWR = 30 + SIGXCPU = 24 + SIGSEGV = 11 + SIGBUS = 7 + if sigdelset(signalMask, SIGPWR) != 0 or + sigdelset(signalMask, SIGXCPU) != 0 or + sigdelset(signalMask, SIGSEGV) != 0 or + sigdelset(signalMask, SIGBUS) != 0: + echo osErrorMsg(osLastError()) + quit(QuitFailure) + if pthread_sigmask(SIG_BLOCK, signalMask, oldSignalMask) != 0: + echo osErrorMsg(osLastError()) + quit(QuitFailure) \ No newline at end of file diff --git a/codex/utils/addrutils.nim b/codex/utils/addrutils.nim index f044581a..3ceda666 100644 --- a/codex/utils/addrutils.nim +++ b/codex/utils/addrutils.nim @@ -15,6 +15,7 @@ import std/options import pkg/libp2p import pkg/stew/shims/net +import pkg/stew/endians2 func remapAddr*( address: MultiAddress, @@ -39,3 +40,59 @@ func remapAddr*( MultiAddress.init(parts.join("/")) .expect("Should construct multiaddress") + +proc getMultiAddrWithIPAndUDPPort*(ip: ValidIpAddress, port: Port): MultiAddress = + ## Creates a MultiAddress with the specified IP address and UDP port + ## + ## Parameters: + ## - ip: A valid IP address (IPv4 or IPv6) + ## - port: The UDP port number + ## + ## Returns: + ## A MultiAddress in the format "/ip4/
/udp/" or "/ip6/
/udp/" + + let ipFamily = ip.family + if ipFamily == IpAddressFamily.IPv4: + # Construct IPv4 multiaddress + return MultiAddress.init("/ip4/" & $ip & "/udp/" & $port).expect("valid multiaddr") + else: + # Construct IPv6 multiaddress + return MultiAddress.init("/ip6/" & $ip & "/udp/" & $port).expect("valid multiaddr") + +proc getAddressAndPort*(ma: MultiAddress): tuple[ip: Option[ValidIpAddress], port: Option[Port]] = + try: + # Try IPv4 first + let ipv4Result = ma[multiCodec("ip4")] + let ip = if ipv4Result.isOk: + let ipBytes = ipv4Result.get() + .protoArgument() + .expect("Invalid IPv4 format") + let ipArray = [ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3]] + some(ipv4(ipArray)) + else: + # Try IPv6 if IPv4 not found + let ipv6Result = ma[multiCodec("ip6")] + if ipv6Result.isOk: + let ipBytes = ipv6Result.get() + .protoArgument() + .expect("Invalid IPv6 format") + var ipArray: array[16, byte] + for i in 0..15: + ipArray[i] = ipBytes[i] + some(ipv6(ipArray)) + else: + none(ValidIpAddress) + + # Get TCP Port + let portResult = ma[multiCodec("tcp")] + let port = if portResult.isOk: + let portBytes = portResult.get() + .protoArgument() + .expect("Invalid port format") + some(Port(fromBytesBE(uint16, portBytes))) + else: + none(Port) + + result = (ip: ip, port: port) + except Exception: + result = (ip: none(ValidIpAddress), port: none(Port)) \ No newline at end of file diff --git a/codex/utils/natutils.nim b/codex/utils/natutils.nim new file mode 100644 index 00000000..25f083bd --- /dev/null +++ b/codex/utils/natutils.nim @@ -0,0 +1,68 @@ +{.push raises: [].} + +import + std/[tables, hashes], + stew/results, stew/shims/net as stewNet, chronos, chronicles + +import pkg/libp2p + +type + NatStrategy* = enum + NatAny + NatUpnp + NatPmp + NatNone + +type + IpLimits* = object + limit*: uint + ips: Table[ValidIpAddress, uint] + +func hash*(ip: ValidIpAddress): Hash = + case ip.family + of IpAddressFamily.IPv6: hash(ip.address_v6) + of IpAddressFamily.IPv4: hash(ip.address_v4) + +func inc*(ipLimits: var IpLimits, ip: ValidIpAddress): bool = + let val = ipLimits.ips.getOrDefault(ip, 0) + if val < ipLimits.limit: + ipLimits.ips[ip] = val + 1 + true + else: + false + +func dec*(ipLimits: var IpLimits, ip: ValidIpAddress) = + let val = ipLimits.ips.getOrDefault(ip, 0) + if val == 1: + ipLimits.ips.del(ip) + elif val > 1: + ipLimits.ips[ip] = val - 1 + +func isGlobalUnicast*(address: TransportAddress): bool = + if address.isGlobal() and address.isUnicast(): + true + else: + false + +func isGlobalUnicast*(address: IpAddress): bool = + let a = initTAddress(address, Port(0)) + a.isGlobalUnicast() + +proc getRouteIpv4*(): Result[ValidIpAddress, 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.IPv4, + address_v4: [1'u8, 1, 1, 1], port: Port(0)) + route = getBestRoute(publicAddress) + + if route.source.isUnspecified(): + err("No best ipv4 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(ValidIpAddress.init(ip)) \ No newline at end of file