Refactor UPnP and PMP

This commit is contained in:
Arnaud 2026-05-08 11:16:45 +04:00
parent 0112c9502b
commit ae261e3210
No known key found for this signature in database
GPG Key ID: A6C7C781817146FA
7 changed files with 462 additions and 327 deletions

View File

@ -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

View File

@ -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)

View File

@ -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,
)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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))