diff --git a/codex/nat/port_mapping.nim b/codex/nat/port_mapping.nim index f0222af6..2409da20 100644 --- a/codex/nat/port_mapping.nim +++ b/codex/nat/port_mapping.nim @@ -10,7 +10,6 @@ import std/[options, os, strutils, times, net, atomics] -import pkg/stew/objects import pkg/nat_traversal/[miniupnpc, natpmp] import pkg/json_serialization/std/net import pkg/results @@ -40,6 +39,9 @@ type PortMappingStrategy* = enum type MappingPort* = ref object of RootObj value*: Port +proc `$`(p: MappingPort): string = + $(p.value) + type TcpPort* = ref object of MappingPort type UdpPort* = ref object of MappingPort @@ -49,7 +51,7 @@ proc newTcpMappingPort*(value: Port): TcpPort = proc newUdpMappingPort*(value: Port): UdpPort = UdpPort(value: value) -type PortMapping = tuple[internalPort: MappingPort, externalPort: Option[MappingPort]] +type PortMapping* = tuple[internalPort: MappingPort, externalPort: Option[MappingPort]] type RenewelThreadArgs = tuple[strategy: PortMappingStrategy, portMapping: seq[PortMapping]] @@ -150,7 +152,7 @@ proc upnpPortMapping( if pmres.isErr: error "UPnP port mapping", msg = pmres.error - return failure(pmres.error) + return failure($pmres.error) # let's check it let cres = upnp.getSpecificPortMapping( @@ -175,22 +177,23 @@ proc npmpPortMapping( internalPort = internalPort.value protocol = protocol - without extPort =? - npmp.addPortMapping( - eport = externalPort.value, - iport = internalPort.value, - protocol = protocol, - lifetime = Pmp_LIFETIME, - ), err: - error "NAT-PMP port mapping error", msg = err.msg - return failure(err.msg) + let extPortRes = npmp.addPortMapping( + eport = externalPort.value.cushort, + iport = internalPort.value.cushort, + protocol = protocol, + lifetime = Pmp_LIFETIME, + ) + + if extPortRes.isErr: + error "NAT-PMP port mapping error", msg = extPortRes.error() + return failure(extPortRes.error()) info "NAT-PMP: added port mapping" if internalPort is TcpPort: - return success(newTcpMappingPort(extPort)) + return success(MappingPort(newTcpMappingPort(Port(extPortRes.value)))) else: - return success(newUdpMappingPort(extPort)) + return success(MappingPort(newUdpMappingPort(Port(extPortRes.value)))) ## Create port mapping that will try to utilize the same port number ## of the internal port for the external port mapping. @@ -217,118 +220,6 @@ proc doPortMapping( return failure("No active startegy") -## Gets external IP provided by the port mapping protocols -## Port mapping needs to be succesfully started first using `startPortMapping()` -proc getExternalIP*(): ?IpAddress = - if upnp == nil and npmp == nil: - warn "No available port-mapping protocol" - return IpAddress.none - - if upnp != nil: - let ires = upnp.externalIPAddress - if ires.isOk(): - info "Got externa IP address: " & ires.value, ip = ires.value - return parseIpAddress(ires.value).some - else: - debug "Getting external IP address using UPnP failed", - msg = ires.error, protocol = "upnp" - - if npmp != nil: - let nires = npmp.externalIPAddress() - if nires.isErr: - debug "Getting external IP address using NAT-PMP failed", msg = nires.error - else: - try: - info "Got externa IP address: " & $(nires.value), - ip =$ (nires.value), protocol = "npmp" - return parseIpAddress($(nires.value)).some - except ValueError as e: - error "parseIpAddress() exception", err = e.msg - - return IpAddress.none - -proc startPortMapping*( - strategy: PortMappingStrategy, internalPorts: seq[MappingPort] -): ?!seq[PortMapping] = - if strategy == PortMappingStrategy.None: - return failure("No port mapping strategy requested") - - if internalPorts.len == 0: - return failure("No internal ports to be mapped were supplied") - - strategy = initProtocols(strategy) - if strategy == PortMappingStrategy.None: - return failure("No available port mapping protocols on the network") - - portMapping = newSeqOfCap[PortMappings](internalPorts.len) - - for port in internalPorts: - without mappedPort =? doPortMapping(port), err: - warn "Failed to map port", port = port.value, msg = err.msg - portMapping.add((internalPort: port, externalPort: MappingPort.none)) - - portMapping.add((internalPort: port, externalPort: mappedPort.some)) - - startRenewalThread(strategy) - - return success(externalPorts) - -proc stopPortMapping*() = - if upnp == nil or npmp == nil: - debug "Port mapping is not running, nothing to stop" - return - - info "Stopping port mapping renewal threads" - try: - portMappingExiting.store(true) - renewalThread.join() - except CatchableError as exc: - warn "Failed to stop port mapping renewal thread", exc = exc.msg - - for mapping in portMapping: - if upnp != nil: - let protocol = - if (internalPort is TcpPort): UPNPProtocol.TCP else: UPNPProtocol.UDP - - if err =? - upnp.deletePortMapping( - externalPort = $(mapping.externalPort.value), protocol = protocol - ).errorOption: - error "UPnP port mapping deletion error", msg = err.msg - else: - debug "UPnP: deleted port mapping", - externalPort = mapping.externalPort, - internalPort = mapping.internalPort, - protocol = protocol - - if npnp != nil: - let protocol = - if (internalPort is TcpPort): NatPmpProtocol.TCP else: NatPmpProtocol.UDP - - if err =? - npmp.deletePortMapping( - eport = mapping.externalPort.value, - iport = mapping.internalPort.value, - protocol = protocol, - ).errorOption: - error "NAT-PMP port mapping deletion error", msg = err.msg - else: - debug "NAT-PMP: deleted port mapping", - externalPort = mapping.externalPort, - internalPort = mapping.internalPort, - protocol = protocol - -proc startRenewalThread( - strategy: PortMappingStrategy, - internalPorts: seq[MappingPort], - externalPorts: seq[?MappingPort], -) = - try: - renewalThread = Thread[RenewelThreadArgs]() - renewalThread.createThread(renewPortMapping, (strategy, portMapping)) - except CatchableError as exc: - warn "Failed to create NAT port mapping renewal thread", exc = exc.msg - proc renewPortMapping(args: RenewelThreadArgs) {.thread, raises: [ValueError].} = ignoreSignalsInThread() let @@ -361,3 +252,121 @@ proc renewPortMapping(args: RenewelThreadArgs) {.thread, raises: [ValueError].} lastUpdate = now() sleep(sleepDuration) + +proc startRenewalThread(strategy: PortMappingStrategy) = + try: + renewalThread = Thread[RenewelThreadArgs]() + renewalThread.createThread(renewPortMapping, (strategy, mappings)) + except CatchableError as exc: + warn "Failed to create NAT port mapping renewal thread", exc = exc.msg + +## Gets external IP provided by the port mapping protocols +## Port mapping needs to be succesfully started first using `startPortMapping()` +proc getExternalIP*(): ?IpAddress = + if upnp == nil and npmp == nil: + warn "No available port-mapping protocol" + return IpAddress.none + + if upnp != nil: + let ires = upnp.externalIPAddress + if ires.isOk(): + info "Got externa IP address", ip = ires.value + try: + return parseIpAddress(ires.value).some + except ValueError as e: + error "Failed to parse IP address", err = e.msg + else: + debug "Getting external IP address using UPnP failed", + msg = ires.error, protocol = "upnp" + + if npmp != nil: + let nires = npmp.externalIPAddress() + if nires.isErr: + debug "Getting external IP address using NAT-PMP failed", msg = nires.error + else: + try: + info "Got externa IP address", ip = $(nires.value), protocol = "npmp" + return parseIpAddress($(nires.value)).some + except ValueError as e: + error "Failed to parse IP address", err = e.msg + + return IpAddress.none + +proc startPortMapping*( + strategy: var PortMappingStrategy, internalPorts: seq[MappingPort] +): ?!seq[PortMapping] = + if strategy == PortMappingStrategy.None: + return failure("No port mapping strategy requested") + + if internalPorts.len == 0: + return failure("No internal ports to be mapped were supplied") + + strategy = initProtocols(strategy) + if strategy == PortMappingStrategy.None: + return failure("No available port mapping protocols on the network") + + if mappings.len > 0: + return failure("Port mapping was already started! Stop first before re-starting.") + + mappings = newSeqOfCap[PortMapping](internalPorts.len) + + for port in internalPorts: + without mappedPort =? doPortMapping(port), err: + warn "Failed to map port", port = port, msg = err.msg + mappings.add((internalPort: port, externalPort: MappingPort.none)) + + mappings.add((internalPort: port, externalPort: mappedPort.some)) + + startRenewalThread(strategy) + + return success(mappings) + +proc stopPortMapping*() = + if upnp == nil or npmp == nil: + debug "Port mapping is not running, nothing to stop" + return + + info "Stopping port mapping renewal threads" + try: + portMappingExiting.store(true) + renewalThread.joinThread() + except CatchableError as exc: + warn "Failed to stop port mapping renewal thread", exc = exc.msg + + for mapping in mappings: + if mapping.externalPort.isNone: + continue + + if upnp != nil: + let protocol = + if (mapping.internalPort is TcpPort): UPNPProtocol.TCP else: UPNPProtocol.UDP + + if err =? + upnp.deletePortMapping( + externalPort = $((!mapping.externalPort).value), protocol = protocol + ).errorOption: + error "UPnP port mapping deletion error", msg = err + else: + debug "UPnP: deleted port mapping", + externalPort = !mapping.externalPort, + internalPort = mapping.internalPort, + protocol = protocol + + if npmp != nil: + let protocol = + if (mapping.internalPort is TcpPort): NatPmpProtocol.TCP else: NatPmpProtocol.UDP + + if err =? + npmp.deletePortMapping( + eport = (!mapping.externalPort).value.cushort, + iport = mapping.internalPort.value.cushort, + protocol = protocol, + ).errorOption: + error "NAT-PMP port mapping deletion error", msg = err + else: + debug "NAT-PMP: deleted port mapping", + externalPort = !mapping.externalPort, + internalPort = mapping.internalPort, + protocol = protocol + + mappings = @[] diff --git a/codex/nat/reachabilitymanager.nim b/codex/nat/reachabilitymanager.nim index b110b45e..864a2d75 100644 --- a/codex/nat/reachabilitymanager.nim +++ b/codex/nat/reachabilitymanager.nim @@ -10,6 +10,7 @@ import pkg/libp2p/protocols/connectivity/autonat/service import ../rng as random import ./port_mapping +import ./utils const AutonatCheckInterval = Opt.some(chronos.seconds(30)) @@ -34,11 +35,53 @@ proc new*( ): T = return T(portMappingStrategy: portMappingStrategy) +proc startPortMapping(self: ReachabilityManager): bool = + if not self.started: + warn "ReachabilityManager is not started, yet we are trying to map ports already!" + return false + + try: + let announceRecords = (!self.getAnnounceRecords)() + let discoveryRecords = (!self.getDiscoveryRecords)() + let portsToBeMapped = + (announceRecords & discoveryRecords).mapIt(getAddressAndPort(it)).mapIt(it.port) + + without mappedPorts =? startPortMapping(self.portMappingStrategy, portsToBeMapped), + err: + warn "Could not start port mapping", msg = err.msg + return false + + if mappedPorts.any( + proc(x: PortMapping): bool = + isNone(x.externalPort) + ): + warn "Some ports were not mapped - not using port mapping then" + return false + + info "Started port mapping" + + let announceMappedRecords = zip( + announceRecords, mappedPorts[0 .. announceRecords.len - 1] + ) + .mapIt(getMultiAddr(getAddressAndPort(it[0]).ip, !it[1].externalPort)) + (!self.updateAnnounceRecords)(announceMappedRecords) + + let discoveryMappedRecords = zip( + discoveryRecords, mappedPorts[announceRecords.len .. ^1] + ) + .mapIt(getMultiAddr(getAddressAndPort(it[0]).ip, !it[1].externalPort)) + (!self.updateDiscoveryRecords)(discoveryMappedRecords) + + return true + except ValueError as exc: + error "Error while starting port mapping", msg = exc.msg + return false + proc getReachabilityHandler(manager: ReachabilityManager): StatusAndConfidenceHandler = let statusAndConfidenceHandler = proc( networkReachability: NetworkReachability, confidenceOpt: Opt[float] - ): Future[void] {.gcsafe, async: (raises: [CancelledError]).} = - if not started: + ): Future[void] {.async: (raises: [CancelledError]).} = + if not manager.started: warn "ReachabilityManager was not started, but we are already getting reachability updates! Ignoring..." return @@ -56,7 +99,7 @@ proc getReachabilityHandler(manager: ReachabilityManager): StatusAndConfidenceHa manager.networkReachability = networkReachability - if networkReachability == NetworkReachability.Unreachable: + if networkReachability == NetworkReachability.NotReachable: # Lets first start to expose port using port mapping protocols like NAT-PMP or UPnP if manager.startPortMapping(): return # We exposed ports so we should be good! @@ -65,45 +108,6 @@ proc getReachabilityHandler(manager: ReachabilityManager): StatusAndConfidenceHa return statusAndConfidenceHandler -proc startPortMapping(self: ReachabilityManager): bool = - try: - let announceRecords = self.getAnnounceRecords() - let discoveryRecords = self.getDiscoveryRecords() - let portsToBeMapped = - (announceRecords & discoveryRecords).mapIt(getAddressAndPort(it)).mapIt(it.port) - - without mappedPorts =? startPortMapping( - manager.portMappingStrategy, portsToBeMapped - ), err: - warn "Could not start port mapping", msg = err - return false - - if mappedPorts.any( - proc(x: ?MappingPort): bool = - isNone(x) - ): - warn "Some ports were not mapped - not using port mapping then" - return false - - info "Started port mapping" - - let announceMappedRecords = zip( - announceRecords, mappedPorts[0 .. announceRecords.len - 1] - ) - .mapIt(getMultiAddr(getAddressAndPort(it[0]).ip, it[1].value)) - self.updateAnnounceRecords(announceMappedRecords) - - let discoveryMappedRecords = zip( - discoveryRecords, mappedPorts[announceRecords.len, ^1] - ) - .mapIt(getMultiAddr(getAddressAndPort(it[0]).ip, it[1].value)) - self.updateDiscoveryRecords(discoveryMappedRecords) - - return true - except ValueError as exc: - error "Error while starting port mapping", msg = exc.msg - return false - proc start*( self: ReachabilityManager, switch: Switch, bootNodes: seq[SignedPeerRecord] ): Future[void] {.async: (raises: [CancelledError]).} = diff --git a/codex/nat/utils.nim b/codex/nat/utils.nim index ae856d31..9436ec16 100644 --- a/codex/nat/utils.nim +++ b/codex/nat/utils.nim @@ -7,13 +7,11 @@ ## This file may not be copied, modified, or distributed except according to ## those terms. -import std/strutils import std/options import std/net import pkg/libp2p -import pkg/questionable -import pkg/questionable/results +import pkg/stew/endians2 import ./port_mapping @@ -62,7 +60,7 @@ proc getAddressAndPort*( proc getMultiAddr*(ip: IpAddress, port: MappingPort): MultiAddress = let ipFamily = if ip.family == IpAddressFamily.IPv4: "/ip4/" else: "/ip6/" - let portType = if (internalPort is TcpPort): "/tcp/" else: "/udp/" + let portType = if (port is TcpPort): "/tcp/" else: "/udp/" return MultiAddress.init(ipFamily & $ip & portType & $(port.value)).expect( "valid multiaddr" )