mirror of https://github.com/status-im/nim-eth.git
external IP retrieval and port mapping functionality
This commit is contained in:
parent
29407afeb6
commit
3db5f4c5dd
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
Loading…
Reference in New Issue