From 3db5f4c5dd47fc6a6b8624572935a813d6e3930d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Wed, 17 Apr 2019 03:35:45 +0200 Subject: [PATCH] external IP retrieval and port mapping functionality --- eth.nimble | 4 +- eth/net/nat.nim | 226 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 eth/net/nat.nim diff --git a/eth.nimble b/eth.nimble index ace1da4..bcd0e75 100644 --- a/eth.nimble +++ b/eth.nimble @@ -13,7 +13,9 @@ requires "nim >= 0.19.0", "rocksdb", "chronos", "chronicles", - "std_shims" + "std_shims", + "result", + "nat_traversal" proc runTest(path: string) = echo "\nRunning: ", path diff --git a/eth/net/nat.nim b/eth/net/nat.nim new file mode 100644 index 0000000..0377387 --- /dev/null +++ b/eth/net/nat.nim @@ -0,0 +1,226 @@ +# Copyright (c) 2019 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. + +import + net, options, os, strutils, times, + result, nat_traversal/[miniupnpc, natpmp], chronicles + +type + NatStrategy* = enum + NatAny + NatUpnp + NatPmp + NatNone + +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 = NatNone + externalIP {.threadvar.}: IPAddress + internalTcpPort: Port + externalTcpPort: Port + internalUdpPort: Port + externalUdpPort: Port + +logScope: + topics = "nat" + +## Also does threadvar initialisation. +## Must be called before redirectPorts() in each thread. +proc getExternalIP*(natStrategy: NatStrategy): Option[IpAddress] = + if natStrategy == NatAny or natStrategy == NatUpnp: + 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." + 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 = NatUpnp + return some(externalIP) + except: + error "parseIpAddress() exception", err = getCurrentExceptionMsg() + return + + if natStrategy == NatAny or natStrategy == NatPmp: + 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: + error "parseIpAddress() exception", err = getCurrentExceptionMsg() + return + +proc doPortMapping(tcpPort, udpPort: Port, description: string): Option[(Port, Port)] {.gcsafe.} = + var + extTcpPort: Port + extUdpPort: Port + + if strategy == 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, + externalIP = $externalIP) + if pmres.isErr: + error "UPnP port mapping", msg = pmres.error + return + else: + # let's check it + let cres = upnp.getSpecificPortMapping(externalPort = $port, + protocol = protocol) + if cres.isErr: + error "UPnP port mapping check", msg = cres.error + return + else: + let extPort = Port(parseUInt(cres.value.externalPort)) + debug "UPnP: added port mapping", externalPort = extPort, internalPort = port, protocol = protocol + case protocol: + of UPNPProtocol.TCP: + extTcpPort = extPort + of UPNPProtocol.UDP: + extUdpPort = extPort + elif strategy == 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 + return + else: + let extPort = Port(pmres.value) + debug "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.} = + 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(). + let ipres = getExternalIP(strategy) + if ipres.isSome: + externalIP = ipres.get() + 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, data) = natCloseChan.tryRecv() + if dataAvailable: + return + else: + let currTime = now() + if currTime >= (lastUpdate + interval): + discard doPortMapping(tcpPort, udpPort, description) + lastUpdate = currTime + sleep(sleepDuration) + +var mainThreadId = getThreadId() + +proc stopNatThread() {.noconv.} = + if getThreadId() == mainThreadId: + # stop the thread + natCloseChan.send(true) + natThread.joinThread() + natCloseChan.close() + # delete our port mappings + if strategy == 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 == 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() + natThread.createThread(repeatPortMapping, (externalTcpPort, externalUdpPort, description)) + # atexit() in disguise + addQuitProc(stopNatThread) +