mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-05-11 22:09:32 +00:00
Refactor UPnP and PMP
This commit is contained in:
parent
0112c9502b
commit
ae261e3210
333
storage/nat.nim
333
storage/nat.nim
@ -8,10 +8,8 @@
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
std/[options, os, times, atomics, exitprocs],
|
||||
nat_traversal/[miniupnpc, natpmp],
|
||||
results
|
||||
import std/[options, net]
|
||||
import results
|
||||
|
||||
import pkg/chronos
|
||||
import pkg/chronicles
|
||||
@ -23,304 +21,52 @@ import ./utils/natutils
|
||||
import ./utils/addrutils
|
||||
import ./discovery
|
||||
|
||||
const
|
||||
UPNP_TIMEOUT = 200 # ms
|
||||
PORT_MAPPING_INTERVAL = 20 * 60 # seconds
|
||||
NATPMP_LIFETIME = 60 * 60 # in seconds, must be longer than PORT_MAPPING_INTERVAL
|
||||
|
||||
type PortMappings* = object
|
||||
internalTcpPort: Port
|
||||
externalTcpPort: Port
|
||||
internalUdpPort: Port
|
||||
externalUdpPort: Port
|
||||
description: string
|
||||
|
||||
type PortMappingArgs =
|
||||
tuple[strategy: NatStrategy, tcpPort, udpPort: Port, description: string]
|
||||
logScope:
|
||||
topics = "nat"
|
||||
|
||||
type NatConfig* = object
|
||||
case hasExtIp*: bool
|
||||
of true: extIp*: IpAddress
|
||||
of false: nat*: NatStrategy
|
||||
|
||||
var
|
||||
upnp {.threadvar.}: Miniupnp
|
||||
npmp {.threadvar.}: NatPmp
|
||||
strategy = NatStrategy.NatAuto
|
||||
natClosed: Atomic[bool]
|
||||
activeMappings: seq[PortMappings]
|
||||
natThreads: seq[Thread[PortMappingArgs]] = @[]
|
||||
|
||||
logScope:
|
||||
topics = "nat"
|
||||
|
||||
type NatMapper* = ref object of RootObj
|
||||
|
||||
method mapNatPorts*(m: NatMapper): Option[(Port, Port)] {.base, gcsafe, raises: [].} =
|
||||
none((Port, Port))
|
||||
|
||||
type DefaultNatMapper* = ref object of NatMapper
|
||||
natConfig*: NatConfig
|
||||
tcpPort*: Port
|
||||
discoveryPort*: Port
|
||||
hasUpnpMapping: bool
|
||||
|
||||
## Initialises the UPnP or NAT-PMP threadvar and sets the `strategy` threadvar.
|
||||
## Must be called before redirectPorts() in each thread.
|
||||
proc initNatDevice(natStrategy: NatStrategy, quiet = false): bool =
|
||||
if natStrategy == NatStrategy.NatAuto 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."
|
||||
of IGDIpNotRoutable:
|
||||
msg =
|
||||
"Internet Gateway Device found and is connected, but with a reserved or non-routable IP. Trying anyway."
|
||||
if not quiet:
|
||||
debug "UPnP", msg
|
||||
if canContinue:
|
||||
strategy = NatStrategy.NatUpnp
|
||||
return true
|
||||
|
||||
if natStrategy == NatStrategy.NatAuto or natStrategy == NatStrategy.NatPmp:
|
||||
if npmp == nil:
|
||||
npmp = newNatPmp()
|
||||
let nres = npmp.init()
|
||||
if nres.isErr:
|
||||
debug "NAT-PMP", msg = nres.error
|
||||
else:
|
||||
strategy = NatStrategy.NatPmp
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
proc doPortMapping(
|
||||
strategy: NatStrategy, 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))
|
||||
|
||||
proc repeatPortMapping(args: PortMappingArgs) {.thread, raises: [ValueError].} =
|
||||
ignoreSignalsInThread()
|
||||
let
|
||||
(strategy, 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 here.
|
||||
if initNatDevice(strategy, quiet = true):
|
||||
while natClosed.load() == false:
|
||||
let currTime = now()
|
||||
if currTime >= (lastUpdate + interval):
|
||||
discard doPortMapping(strategy, tcpPort, udpPort, description)
|
||||
lastUpdate = currTime
|
||||
|
||||
sleep(sleepDuration)
|
||||
|
||||
proc stopNatThreads() {.noconv.} =
|
||||
# stop the thread
|
||||
debug "Stopping NAT port mapping renewal threads"
|
||||
try:
|
||||
natClosed.store(true)
|
||||
joinThreads(natThreads)
|
||||
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.
|
||||
|
||||
if initNatDevice(strategy, quiet = true):
|
||||
if strategy == NatStrategy.NatUpnp:
|
||||
for entry in activeMappings:
|
||||
for t in [
|
||||
(entry.externalTcpPort, entry.internalTcpPort, UPNPProtocol.TCP),
|
||||
(entry.externalUdpPort, entry.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 entry in activeMappings:
|
||||
for t in [
|
||||
(entry.externalTcpPort, entry.internalTcpPort, NatPmpProtocol.TCP),
|
||||
(entry.externalUdpPort, entry.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*(
|
||||
strategy: NatStrategy, tcpPort, udpPort: Port, description: string
|
||||
): Option[(Port, Port)] =
|
||||
result = doPortMapping(strategy, tcpPort, udpPort, description)
|
||||
if result.isSome:
|
||||
let (externalTcpPort, externalUdpPort) = result.get()
|
||||
# needed by NAT-PMP on port mapping deletion
|
||||
# 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.
|
||||
activeMappings.add(
|
||||
PortMappings(
|
||||
internalTcpPort: tcpPort,
|
||||
externalTcpPort: externalTcpPort,
|
||||
internalUdpPort: udpPort,
|
||||
externalUdpPort: externalUdpPort,
|
||||
description: description,
|
||||
)
|
||||
)
|
||||
try:
|
||||
natThreads.add(Thread[PortMappingArgs]())
|
||||
natThreads[^1].createThread(
|
||||
repeatPortMapping, (strategy, externalTcpPort, externalUdpPort, description)
|
||||
)
|
||||
# atexit() in disguise
|
||||
if natThreads.len == 1:
|
||||
# we should register the thread termination function only once
|
||||
addExitProc(stopNatThreads)
|
||||
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
|
||||
): Option[(Port, Port)] =
|
||||
## Setup NAT port mapping.
|
||||
## Returns the external (tcpPort, udpPort) if port mapping succeeded, none otherwise.
|
||||
## TODO: Allow for tcp or udp port mapping to be optional.
|
||||
if not initNatDevice(natStrategy):
|
||||
warn "UPnP/NAT-PMP not available"
|
||||
method mapNatPorts*(m: NatMapper): Option[(Port, Port)] {.base, gcsafe, raises: [].} =
|
||||
if m.natConfig.hasExtIp:
|
||||
return none((Port, Port))
|
||||
|
||||
let extPorts = (
|
||||
{.gcsafe.}:
|
||||
redirectPorts(
|
||||
strategy, tcpPort = tcpPort, udpPort = udpPort, description = clientId
|
||||
)
|
||||
)
|
||||
if extPorts.isNone:
|
||||
warn "UPnP/NAT-PMP available but port forwarding failed"
|
||||
extPorts
|
||||
# Devices are recreated on each call: discover() costs ~200ms but only fires
|
||||
# when AutoNAT reports NotReachable, which is exactly when we want a fresh scan.
|
||||
let upnpRes = UpnpDevice.init()
|
||||
if upnpRes.isOk:
|
||||
let ports = upnpRes.value.mapPorts(m.tcpPort, m.discoveryPort)
|
||||
if ports.isSome:
|
||||
m.hasUpnpMapping = true
|
||||
return ports
|
||||
|
||||
proc findReachableNodes*(bootstrapNodes: seq[SignedPeerRecord]): seq[SignedPeerRecord] =
|
||||
## Returns the list of nodes known to be directly reachable.
|
||||
## Currently returns bootstrap nodes. In the future, any network participant
|
||||
## confirmed reachable by AutoNAT could be included.
|
||||
bootstrapNodes
|
||||
let pmpRes = PmpDevice.init()
|
||||
if pmpRes.isOk:
|
||||
let ports = pmpRes.value.mapPorts(m.tcpPort, m.discoveryPort)
|
||||
if ports.isSome:
|
||||
return ports
|
||||
|
||||
proc nattedPorts*(natConfig: NatConfig, tcpPort, udpPort: Port): Option[(Port, Port)] =
|
||||
if natConfig.hasExtIp:
|
||||
return none((Port, Port))
|
||||
let clientId = "storage"
|
||||
return setupNat(natConfig.nat, tcpPort, udpPort, clientId)
|
||||
none((Port, Port))
|
||||
|
||||
method mapNatPorts*(m: DefaultNatMapper): Option[(Port, Port)] {.gcsafe, raises: [].} =
|
||||
nattedPorts(m.natConfig, m.tcpPort, m.discoveryPort)
|
||||
|
||||
proc handleNatStatus*(
|
||||
method handleNatStatus*(
|
||||
m: NatMapper,
|
||||
networkReachability: NetworkReachability,
|
||||
dialBackAddr: Opt[MultiAddress],
|
||||
discoveryPort: Port,
|
||||
mapper: NatMapper,
|
||||
discovery: Discovery,
|
||||
switch: Switch,
|
||||
autoRelayService: AutoRelayService,
|
||||
) {.async: (raises: [CancelledError]).} =
|
||||
) {.async: (raises: [CancelledError]), base, gcsafe.} =
|
||||
case networkReachability
|
||||
of Unknown:
|
||||
# Nothing to do here, not enough confidence score result
|
||||
discard
|
||||
of Reachable:
|
||||
if dialBackAddr.isNone:
|
||||
@ -337,9 +83,9 @@ proc handleNatStatus*(
|
||||
var hasPortMapping = false
|
||||
|
||||
if dialBackAddr.isNone:
|
||||
warn "Got empty dialback address in AutoNat when node is Reachable"
|
||||
warn "Got empty dialback address in AutoNat when node is NotReachable"
|
||||
else:
|
||||
let maybePorts = mapper.mapNatPorts()
|
||||
let maybePorts = m.mapNatPorts()
|
||||
|
||||
if maybePorts.isSome:
|
||||
let (tcpPort, udpPort) = maybePorts.get()
|
||||
@ -352,9 +98,34 @@ proc handleNatStatus*(
|
||||
debug "AutoRelayService stop method returned false"
|
||||
|
||||
discovery.updateRecords(@[announceAddress], udpPort)
|
||||
|
||||
hasPortMapping = true
|
||||
|
||||
if not hasPortMapping and not autoRelayService.isRunning:
|
||||
if not await autoRelayService.setup(switch):
|
||||
debug "AutoRelayService setup method returned false"
|
||||
|
||||
proc close*(m: NatMapper, device = UpnpDevice()) =
|
||||
# UPnP mappings are permanent (leaseDuration=0) and must be deleted explicitly.
|
||||
# NAT-PMP mappings expire automatically after NATPMP_LIFETIME seconds.
|
||||
if not m.hasUpnpMapping:
|
||||
return
|
||||
|
||||
# deletePortMapping requires the IGD control URL set during init
|
||||
let deviceRes = device.init()
|
||||
if deviceRes.isErr:
|
||||
warn "UPnP reinit failed during cleanup, port mappings may remain",
|
||||
msg = deviceRes.error
|
||||
return
|
||||
|
||||
for (port, proto) in [
|
||||
(m.tcpPort, NatIpProtocol.Tcp), (m.discoveryPort, NatIpProtocol.Udp)
|
||||
]:
|
||||
let res = deviceRes.value.deletePortMapping(port, proto)
|
||||
if res.isErr:
|
||||
error "UPnP port mapping deletion failed", port, proto, msg = res.error
|
||||
|
||||
proc findReachableNodes*(bootstrapNodes: seq[SignedPeerRecord]): seq[SignedPeerRecord] =
|
||||
## Returns the list of nodes known to be directly reachable.
|
||||
## Currently returns bootstrap nodes. In the future, any network participant
|
||||
## confirmed reachable by AutoNAT could be included.
|
||||
bootstrapNodes
|
||||
|
||||
@ -561,7 +561,7 @@ proc initNodeApi(node: StorageNodeRef, conf: StorageConf, router: var RestRouter
|
||||
proc initDebugApi(
|
||||
node: StorageNodeRef,
|
||||
conf: StorageConf,
|
||||
autonat: AutonatV2Service,
|
||||
autonat: Option[AutonatV2Service],
|
||||
router: var RestRouter,
|
||||
) =
|
||||
let allowedOrigin = router.allowedOrigin
|
||||
@ -583,7 +583,13 @@ proc initDebugApi(
|
||||
"announceAddresses": node.discovery.announceAddrs,
|
||||
"table": table,
|
||||
"storage": {"version": $storageVersion, "revision": $storageRevision},
|
||||
"nat": {"reachability": $autonat.networkReachability},
|
||||
"nat": {
|
||||
"reachability":
|
||||
if autonat.isSome:
|
||||
$autonat.get.networkReachability
|
||||
else:
|
||||
"unknown"
|
||||
},
|
||||
}
|
||||
|
||||
# return pretty json for human readability
|
||||
@ -644,7 +650,7 @@ proc initRestApi*(
|
||||
node: StorageNodeRef,
|
||||
conf: StorageConf,
|
||||
repoStore: RepoStore,
|
||||
autonat: AutonatV2Service,
|
||||
autonat: Option[AutonatV2Service],
|
||||
corsAllowedOrigin: ?string,
|
||||
): RestRouter =
|
||||
var router = RestRouter.init(validate, corsAllowedOrigin)
|
||||
|
||||
@ -53,8 +53,9 @@ type
|
||||
repoStore: RepoStore
|
||||
maintenance: BlockMaintainer
|
||||
taskpool: Taskpool
|
||||
autonatService*: AutonatV2Service
|
||||
autonatService*: Option[AutonatV2Service]
|
||||
autoRelayService: AutoRelayService
|
||||
natMapper: NatMapper
|
||||
isStarted: bool
|
||||
|
||||
StoragePrivateKey* = libp2p.PrivateKey # alias
|
||||
@ -123,6 +124,8 @@ proc stop*(s: StorageServer) {.async.} =
|
||||
|
||||
notice "Stopping Storage node"
|
||||
|
||||
s.natMapper.close()
|
||||
|
||||
var futures = @[
|
||||
s.storageNode.switch.stop(),
|
||||
s.storageNode.stop(),
|
||||
@ -190,17 +193,23 @@ proc new*(
|
||||
let relayClient = relayClientModule.RelayClient.new(canHop = config.relay)
|
||||
|
||||
let autonatClient = AutonatV2Client.new(random.Rng.instance())
|
||||
let autonatService = AutonatV2Service.new(
|
||||
rng = random.Rng.instance(),
|
||||
client = autonatClient,
|
||||
config = AutonatV2ServiceConfig.new(
|
||||
scheduleInterval = Opt.some(config.natScheduleInterval),
|
||||
askNewConnectedPeers = true,
|
||||
numPeersToAsk = config.natNumPeersToAsk,
|
||||
maxQueueSize = config.natMaxQueueSize,
|
||||
minConfidence = config.natMinConfidence,
|
||||
),
|
||||
)
|
||||
let autonatService =
|
||||
if config.nat.hasExtIp:
|
||||
none(AutonatV2Service)
|
||||
else:
|
||||
some(
|
||||
AutonatV2Service.new(
|
||||
rng = random.Rng.instance(),
|
||||
client = autonatClient,
|
||||
config = AutonatV2ServiceConfig.new(
|
||||
scheduleInterval = Opt.some(config.natScheduleInterval),
|
||||
askNewConnectedPeers = true,
|
||||
numPeersToAsk = config.natNumPeersToAsk,
|
||||
maxQueueSize = config.natMaxQueueSize,
|
||||
minConfidence = config.natMinConfidence,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
let switch = SwitchBuilder
|
||||
.new()
|
||||
@ -215,7 +224,12 @@ proc new*(
|
||||
.withTcpTransport({ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay})
|
||||
.withAutonatV2Server()
|
||||
.withCircuitRelay(relayClient)
|
||||
.withServices(@[Service(autonatService)])
|
||||
.withServices(
|
||||
if autonatService.isSome:
|
||||
@[Service(autonatService.get)]
|
||||
else:
|
||||
@[]
|
||||
)
|
||||
.build()
|
||||
|
||||
autonatClient.setup(switch)
|
||||
@ -349,23 +363,24 @@ proc new*(
|
||||
switch.mount(network)
|
||||
switch.mount(manifestProto)
|
||||
|
||||
let natMapper = DefaultNatMapper(
|
||||
let natMapper = NatMapper(
|
||||
natConfig: config.nat,
|
||||
tcpPort: config.listenPort,
|
||||
discoveryPort: config.discoveryPort,
|
||||
)
|
||||
autonatService.setStatusAndConfidenceHandler(
|
||||
proc(
|
||||
networkReachability: NetworkReachability,
|
||||
confidence: Opt[float],
|
||||
addrs: Opt[MultiAddress],
|
||||
) {.async: (raises: [CancelledError]).} =
|
||||
debug "AutoNAT status", reachability = networkReachability, confidence
|
||||
await handleNatStatus(
|
||||
networkReachability, addrs, config.discoveryPort, natMapper, discovery, switch,
|
||||
autoRelayService,
|
||||
)
|
||||
)
|
||||
if autonatService.isSome:
|
||||
autonatService.get.setStatusAndConfidenceHandler(
|
||||
proc(
|
||||
networkReachability: NetworkReachability,
|
||||
confidence: Opt[float],
|
||||
addrs: Opt[MultiAddress],
|
||||
) {.async: (raises: [CancelledError]).} =
|
||||
debug "AutoNAT status", reachability = networkReachability, confidence
|
||||
await natMapper.handleNatStatus(
|
||||
networkReachability, addrs, config.discoveryPort, discovery, switch,
|
||||
autoRelayService,
|
||||
)
|
||||
)
|
||||
|
||||
StorageServer(
|
||||
config: config,
|
||||
@ -377,4 +392,5 @@ proc new*(
|
||||
logFile: logFile,
|
||||
autonatService: autonatService,
|
||||
autoRelayService: autoRelayService,
|
||||
natMapper: natMapper,
|
||||
)
|
||||
|
||||
@ -1,6 +1,207 @@
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[options, net]
|
||||
import nat_traversal/[miniupnpc, natpmp]
|
||||
import pkg/chronicles
|
||||
import results
|
||||
|
||||
export miniupnpc, natpmp, results, options, net
|
||||
|
||||
logScope:
|
||||
topics = "nat"
|
||||
|
||||
const UPNP_TIMEOUT* = 200 # ms
|
||||
const NATPMP_LIFETIME* = 60 * 60 # seconds
|
||||
|
||||
type NatStrategy* = enum
|
||||
NatAuto
|
||||
NatUpnp
|
||||
NatPmp
|
||||
|
||||
type NatIpProtocol* = enum
|
||||
Tcp
|
||||
Udp
|
||||
|
||||
# Generic Nat device can be UPnP or PmP
|
||||
type NatDevice* = ref object of RootObj
|
||||
|
||||
type UpnpDevice* = ref object of NatDevice
|
||||
upnp: Miniupnp
|
||||
|
||||
type PmpDevice* = ref object of NatDevice
|
||||
npmp: NatPmp
|
||||
|
||||
# appPortMapping is specific to the type of Nat device
|
||||
method addPortMapping*(
|
||||
d: NatDevice, port: Port, proto: NatIpProtocol
|
||||
): Result[Port, string] {.base, gcsafe.} =
|
||||
return err("not implemented")
|
||||
|
||||
# Creates the mapping the the router and
|
||||
# returns the opened ports.
|
||||
method mapPorts*(
|
||||
d: NatDevice, tcpPort, udpPort: Port
|
||||
): Option[(Port, Port)] {.base, gcsafe.} =
|
||||
var extTcpPort, extUdpPort: Port
|
||||
|
||||
for t in [(tcpPort, NatIpProtocol.Tcp), (udpPort, NatIpProtocol.Udp)]:
|
||||
let (port, proto) = t
|
||||
let pmres = d.addPortMapping(port, proto)
|
||||
|
||||
if pmres.isErr:
|
||||
error "port mapping failed", msg = pmres.error
|
||||
return none((Port, Port))
|
||||
|
||||
case proto
|
||||
of Tcp:
|
||||
extTcpPort = pmres.value
|
||||
of Udp:
|
||||
extUdpPort = pmres.value
|
||||
|
||||
return some((extTcpPort, extUdpPort))
|
||||
|
||||
method getSpecificPortMapping*(
|
||||
d: UpnpDevice, externalPort: string, protocol: UPNPProtocol
|
||||
): Result[PortMappingRes, cstring] {.base, gcsafe.} =
|
||||
if d.upnp == nil:
|
||||
return err(cstring("upnp not initialized"))
|
||||
|
||||
d.upnp.getSpecificPortMapping(externalPort = externalPort, protocol = protocol)
|
||||
|
||||
method discover*(d: UpnpDevice): Result[int, cstring] {.base, gcsafe.} =
|
||||
if d.upnp == nil:
|
||||
return err(cstring("upnp not initialized"))
|
||||
|
||||
return d.upnp.discover()
|
||||
|
||||
method selectIGD*(d: UpnpDevice): SelectIGDResult {.base, gcsafe.} =
|
||||
if d.upnp == nil:
|
||||
return IGDNotFound
|
||||
|
||||
return d.upnp.selectIGD()
|
||||
|
||||
proc init*(T: type UpnpDevice): Result[UpnpDevice, string] {.gcsafe.} =
|
||||
UpnpDevice().init()
|
||||
|
||||
# Init UPnP device and create miniupnp instance.
|
||||
# It call "discover" to retrieve the UPnP devices on the network,
|
||||
# and then "selectIGD" to select a suitable device.
|
||||
proc init*(d: UpnpDevice): Result[UpnpDevice, string] {.gcsafe.} =
|
||||
if d.upnp == nil:
|
||||
d.upnp = newMiniupnp()
|
||||
|
||||
d.upnp.discoverDelay = UPNP_TIMEOUT
|
||||
|
||||
let dres = d.discover()
|
||||
if dres.isErr:
|
||||
debug "UPnP", msg = dres.error
|
||||
return err($dres.error)
|
||||
|
||||
case d.selectIGD()
|
||||
of IGDNotFound:
|
||||
debug "UPnP", msg = "Internet Gateway Device not found. Giving up."
|
||||
return err("IGD not found")
|
||||
of IGDFound:
|
||||
debug "UPnP", msg = "Internet Gateway Device found."
|
||||
of IGDNotConnected:
|
||||
debug "UPnP",
|
||||
msg = "Internet Gateway Device found but it's not connected. Trying anyway."
|
||||
of NotAnIGD:
|
||||
debug "UPnP",
|
||||
msg =
|
||||
"Some device found, but it's not recognised as an Internet Gateway Device. Trying anyway."
|
||||
of IGDIpNotRoutable:
|
||||
debug "UPnP",
|
||||
msg =
|
||||
"Internet Gateway Device found and is connected, but with a reserved or non-routable IP. Trying anyway."
|
||||
|
||||
return ok(d)
|
||||
|
||||
# For UPnP, the external port is the same as the application port.
|
||||
# This should work for most of the case.
|
||||
# We could change this by using addAnyPortMapping for IGD2 compatible routers
|
||||
# if needed.
|
||||
method addPortMapping*(
|
||||
d: UpnpDevice, port: Port, proto: NatIpProtocol
|
||||
): Result[Port, string] {.gcsafe.} =
|
||||
if d.upnp == nil:
|
||||
return err("upnp not initialized")
|
||||
|
||||
let protocol = if proto == NatIpProtocol.Tcp: UPNPProtocol.TCP else: UPNPProtocol.UDP
|
||||
let pmres = d.upnp.addPortMapping(
|
||||
externalPort = $port,
|
||||
protocol = protocol,
|
||||
internalHost = d.upnp.lanAddr,
|
||||
internalPort = $port,
|
||||
desc = "logos-storage",
|
||||
leaseDuration = 0,
|
||||
)
|
||||
if pmres.isErr:
|
||||
return err($pmres.error)
|
||||
|
||||
let cres = d.getSpecificPortMapping(externalPort = $port, protocol = protocol)
|
||||
if cres.isErr:
|
||||
# Eventually, the check could fail on some router even if the router is successful.
|
||||
# So we log a warning but we still want to continue because it is not sure it is a failure.
|
||||
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
|
||||
|
||||
return ok(port)
|
||||
|
||||
method deletePortMapping*(
|
||||
d: UpnpDevice, port: Port, proto: NatIpProtocol
|
||||
): Result[void, string] {.base, gcsafe.} =
|
||||
if d.upnp == nil:
|
||||
return err("upnp not initialized")
|
||||
|
||||
let protocol = if proto == NatIpProtocol.Tcp: UPNPProtocol.TCP else: UPNPProtocol.UDP
|
||||
let res = d.upnp.deletePortMapping(externalPort = $port, protocol = protocol)
|
||||
if res.isErr:
|
||||
return err($res.error)
|
||||
|
||||
debug "UPnP: deleted port mapping", port, proto
|
||||
|
||||
ok()
|
||||
|
||||
proc init*(T: type PmpDevice): Result[PmpDevice, string] {.gcsafe.} =
|
||||
PmpDevice().init()
|
||||
|
||||
# Create a NatPmP instance.
|
||||
proc init*(d: PmpDevice): Result[PmpDevice, string] {.gcsafe.} =
|
||||
if d.npmp == nil:
|
||||
d.npmp = newNatPmp()
|
||||
|
||||
let res = d.npmp.init()
|
||||
if res.isErr:
|
||||
debug "NAT-PMP", msg = res.error
|
||||
return err($res.error)
|
||||
|
||||
return ok(d)
|
||||
|
||||
# Add a port mapping on NAT-PMP device.
|
||||
# The application port might not be the external port.
|
||||
# The latter is returned.
|
||||
method addPortMapping*(
|
||||
d: PmpDevice, port: Port, proto: NatIpProtocol
|
||||
): Result[Port, string] {.gcsafe.} =
|
||||
if d.npmp == nil:
|
||||
return err("npmp not initialized")
|
||||
|
||||
let protocol =
|
||||
if proto == NatIpProtocol.Tcp: NatPmpProtocol.TCP else: NatPmpProtocol.UDP
|
||||
let pmres = d.npmp.addPortMapping(
|
||||
eport = port.cushort,
|
||||
iport = port.cushort,
|
||||
protocol = protocol,
|
||||
lifetime = NATPMP_LIFETIME,
|
||||
)
|
||||
if pmres.isErr:
|
||||
return err(pmres.error)
|
||||
|
||||
let extPort = Port(pmres.value)
|
||||
|
||||
info "NAT-PMP: added port mapping", externalPort = extPort, internalPort = port
|
||||
|
||||
return ok(extPort)
|
||||
|
||||
@ -213,7 +213,9 @@ template multinodesuite*(suiteName: string, body: untyped) =
|
||||
trace "Setting up test", suite = suiteName, test = currentTestName, nodeConfigs
|
||||
if var clients =? nodeConfigs.clients:
|
||||
failAndTeardownOnError "failed to start client nodes":
|
||||
clients = clients.withExtIp()
|
||||
# Only the first node (bootstrap) gets a known extip. Other nodes use
|
||||
# nat=auto so AutoNAT can run and determine their reachability.
|
||||
clients = clients.withExtIp(0)
|
||||
for config in clients.configs:
|
||||
let node = await startClientNode(config)
|
||||
running.add RunningNode(role: Role.Client, node: node)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import std/net
|
||||
import std/[net, importutils]
|
||||
import pkg/chronos
|
||||
import ../../storage/utils/natutils
|
||||
import pkg/libp2p/[multiaddress, multihash, multicodec]
|
||||
import pkg/libp2p/protocols/connectivity/autonatv2/service except setup
|
||||
import pkg/libp2p/protocols/connectivity/autonatv2/types
|
||||
@ -16,12 +17,52 @@ import ../../storage/rng
|
||||
import ../../storage/utils
|
||||
import ../../storage/utils/addrutils
|
||||
|
||||
privateAccess(NatMapper)
|
||||
|
||||
type MockUpnpDevice = ref object of UpnpDevice
|
||||
deletedPorts: seq[(Port, NatIpProtocol)]
|
||||
|
||||
method discover*(d: MockUpnpDevice): Result[int, cstring] {.gcsafe.} =
|
||||
ok(1)
|
||||
|
||||
method selectIGD*(d: MockUpnpDevice): SelectIGDResult {.gcsafe.} =
|
||||
IGDFound
|
||||
|
||||
method deletePortMapping*(
|
||||
d: MockUpnpDevice, port: Port, proto: NatIpProtocol
|
||||
): Result[void, string] {.gcsafe.} =
|
||||
d.deletedPorts.add((port, proto))
|
||||
ok()
|
||||
|
||||
type MockNatMapper = ref object of NatMapper
|
||||
mappedPorts: Option[(Port, Port)]
|
||||
|
||||
method mapNatPorts*(m: MockNatMapper): Option[(Port, Port)] {.raises: [].} =
|
||||
method mapNatPorts*(m: MockNatMapper): Option[(Port, Port)] {.gcsafe, raises: [].} =
|
||||
m.mappedPorts
|
||||
|
||||
suite "NatMapper.close":
|
||||
test "does nothing when no upnp mapping":
|
||||
let mapper = MockNatMapper(
|
||||
natConfig: NatConfig(hasExtIp: false, nat: NatAuto),
|
||||
tcpPort: Port(8080),
|
||||
discoveryPort: Port(8090),
|
||||
)
|
||||
let device = MockUpnpDevice()
|
||||
mapper.close(device)
|
||||
check device.deletedPorts.len == 0
|
||||
|
||||
test "deletes tcp and udp ports when upnp mapping exists":
|
||||
let mapper = MockNatMapper(
|
||||
natConfig: NatConfig(hasExtIp: false, nat: NatAuto),
|
||||
tcpPort: Port(8080),
|
||||
discoveryPort: Port(8090),
|
||||
)
|
||||
mapper.hasUpnpMapping = true
|
||||
let device = MockUpnpDevice()
|
||||
mapper.close(device)
|
||||
check device.deletedPorts ==
|
||||
@[(Port(8080), NatIpProtocol.Tcp), (Port(8090), NatIpProtocol.Udp)]
|
||||
|
||||
suite "remapAddr":
|
||||
test "replaces protocol tcp with udp":
|
||||
let ma = MultiAddress.init("/ip4/1.2.3.4/tcp/5000").expect("valid")
|
||||
@ -38,11 +79,6 @@ suite "remapAddr":
|
||||
let remapped = ma.remapAddr(ip = some(parseIpAddress("8.8.8.8")))
|
||||
check remapped == MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid")
|
||||
|
||||
suite "nattedPorts":
|
||||
test "returns none when extIp is configured (manual setup)":
|
||||
let natConfig = NatConfig(hasExtIp: true, extIp: parseIpAddress("8.8.8.8"))
|
||||
check nattedPorts(natConfig, Port(5000), Port(1234)).isNone
|
||||
|
||||
asyncchecksuite "handleNatStatus":
|
||||
var sw: Switch
|
||||
var key: PrivateKey
|
||||
@ -68,8 +104,8 @@ asyncchecksuite "handleNatStatus":
|
||||
let dialBack = MultiAddress.init("/ip4/1.2.3.4/tcp/8080").expect("valid")
|
||||
let mapper = MockNatMapper(mappedPorts: some((Port(9000), Port(9001))))
|
||||
|
||||
await handleNatStatus(
|
||||
NotReachable, Opt.some(dialBack), discoveryPort, mapper, disc, sw, autoRelay
|
||||
await mapper.handleNatStatus(
|
||||
NotReachable, Opt.some(dialBack), discoveryPort, disc, sw, autoRelay
|
||||
)
|
||||
|
||||
check disc.announceAddrs ==
|
||||
@ -79,17 +115,28 @@ asyncchecksuite "handleNatStatus":
|
||||
test "handleNatStatus starts autoRelay when NotReachable and UPnP failed":
|
||||
let mapper = MockNatMapper(mappedPorts: none((Port, Port)))
|
||||
|
||||
await handleNatStatus(
|
||||
NotReachable, Opt.none(MultiAddress), discoveryPort, mapper, disc, sw, autoRelay
|
||||
await mapper.handleNatStatus(
|
||||
NotReachable, Opt.none(MultiAddress), discoveryPort, disc, sw, autoRelay
|
||||
)
|
||||
|
||||
check autoRelay.isRunning
|
||||
|
||||
test "handleNatStatus starts autoRelay when NotReachable and mapping fails":
|
||||
let dialBack = MultiAddress.init("/ip4/1.2.3.4/tcp/8080").expect("valid")
|
||||
let mapper = MockNatMapper(mappedPorts: none((Port, Port)))
|
||||
|
||||
await mapper.handleNatStatus(
|
||||
NotReachable, Opt.some(dialBack), discoveryPort, disc, sw, autoRelay
|
||||
)
|
||||
|
||||
check autoRelay.isRunning
|
||||
check disc.announceAddrs == newSeq[MultiAddress]()
|
||||
|
||||
test "handleNatStatus does not announce address when Reachable and no dialBackAddr":
|
||||
let mapper = MockNatMapper(mappedPorts: none((Port, Port)))
|
||||
|
||||
await handleNatStatus(
|
||||
Reachable, Opt.none(MultiAddress), discoveryPort, mapper, disc, sw, autoRelay
|
||||
await mapper.handleNatStatus(
|
||||
Reachable, Opt.none(MultiAddress), discoveryPort, disc, sw, autoRelay
|
||||
)
|
||||
|
||||
check disc.announceAddrs == newSeq[MultiAddress]()
|
||||
@ -100,8 +147,8 @@ asyncchecksuite "handleNatStatus":
|
||||
let mapper = MockNatMapper(mappedPorts: none((Port, Port)))
|
||||
|
||||
discard await autorelayservice.setup(autoRelay, sw)
|
||||
await handleNatStatus(
|
||||
Reachable, Opt.some(dialBack), discoveryPort, mapper, disc, sw, autoRelay
|
||||
await mapper.handleNatStatus(
|
||||
Reachable, Opt.some(dialBack), discoveryPort, disc, sw, autoRelay
|
||||
)
|
||||
|
||||
check not autoRelay.isRunning
|
||||
|
||||
@ -1 +1,93 @@
|
||||
discard
|
||||
import std/[options, net]
|
||||
import nat_traversal/[miniupnpc, natpmp]
|
||||
import pkg/results
|
||||
import ../asynctest
|
||||
import ../../storage/utils/natutils
|
||||
|
||||
type MockUpnpDev = ref object of UpnpDevice
|
||||
discoverOk: bool
|
||||
igdResult: SelectIGDResult
|
||||
addPortMappingOk: bool
|
||||
failOnProto: Option[NatIpProtocol]
|
||||
|
||||
type MockPmpDev = ref object of PmpDevice
|
||||
addPortMappingOk: bool
|
||||
mappedPort: Port
|
||||
|
||||
method discover*(d: MockUpnpDev): Result[int, cstring] {.gcsafe.} =
|
||||
if d.discoverOk:
|
||||
ok(1)
|
||||
else:
|
||||
err(cstring("discover failed"))
|
||||
|
||||
method selectIGD*(d: MockUpnpDev): SelectIGDResult {.gcsafe.} =
|
||||
d.igdResult
|
||||
|
||||
method addPortMapping*(
|
||||
d: MockUpnpDev, port: Port, proto: NatIpProtocol
|
||||
): Result[Port, string] {.gcsafe.} =
|
||||
if d.failOnProto == some(proto):
|
||||
err("mapping failed")
|
||||
elif d.addPortMappingOk:
|
||||
ok(port)
|
||||
else:
|
||||
err("mapping failed")
|
||||
|
||||
method getSpecificPortMapping*(
|
||||
d: MockUpnpDev, externalPort: string, protocol: UPNPProtocol
|
||||
): Result[PortMappingRes, cstring] {.gcsafe.} =
|
||||
ok(PortMappingRes())
|
||||
|
||||
method addPortMapping*(
|
||||
d: MockPmpDev, port: Port, proto: NatIpProtocol
|
||||
): Result[Port, string] {.gcsafe.} =
|
||||
if d.addPortMappingOk:
|
||||
ok(d.mappedPort)
|
||||
else:
|
||||
err("mapping failed")
|
||||
|
||||
suite "UpnpDevice.init":
|
||||
test "returns err when discover fails":
|
||||
check MockUpnpDev(discoverOk: false).init().isErr
|
||||
|
||||
test "returns err when IGD not found":
|
||||
check MockUpnpDev(discoverOk: true, igdResult: IGDNotFound).init().isErr
|
||||
|
||||
test "returns ok when IGD found":
|
||||
check MockUpnpDev(discoverOk: true, igdResult: IGDFound).init().isOk
|
||||
|
||||
test "returns ok when IGD not connected":
|
||||
check MockUpnpDev(discoverOk: true, igdResult: IGDNotConnected).init().isOk
|
||||
|
||||
test "returns ok when not an IGD":
|
||||
check MockUpnpDev(discoverOk: true, igdResult: NotAnIGD).init().isOk
|
||||
|
||||
test "returns ok when IP not routable":
|
||||
check MockUpnpDev(discoverOk: true, igdResult: IGDIpNotRoutable).init().isOk
|
||||
|
||||
suite "UpnpDevice.mapPorts":
|
||||
test "returns none when addPortMapping fails":
|
||||
check MockUpnpDev(addPortMappingOk: false).mapPorts(Port(8080), Port(8090)).isNone
|
||||
|
||||
test "returns mapped ports":
|
||||
let res = MockUpnpDev(addPortMappingOk: true).mapPorts(Port(8080), Port(8090))
|
||||
check res.isSome
|
||||
check res.get() == (Port(8080), Port(8090))
|
||||
|
||||
test "returns none when tcp mapping fails":
|
||||
let d = MockUpnpDev(addPortMappingOk: true, failOnProto: some(NatIpProtocol.Tcp))
|
||||
check d.mapPorts(Port(8080), Port(8090)).isNone
|
||||
|
||||
test "returns none when udp mapping fails":
|
||||
let d = MockUpnpDev(addPortMappingOk: true, failOnProto: some(NatIpProtocol.Udp))
|
||||
check d.mapPorts(Port(8080), Port(8090)).isNone
|
||||
|
||||
suite "PmpDevice.mapPorts":
|
||||
test "returns none when mapping fails":
|
||||
check MockPmpDev(addPortMappingOk: false).mapPorts(Port(8080), Port(8090)).isNone
|
||||
|
||||
test "returns assigned external ports":
|
||||
let d = MockPmpDev(addPortMappingOk: true, mappedPort: Port(9000))
|
||||
let res = d.mapPorts(Port(8080), Port(8090))
|
||||
check res.isSome
|
||||
check res.get() == (Port(9000), Port(9000))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user