Refactor tests

This commit is contained in:
Arnaud 2026-06-12 14:42:26 +04:00
parent b33031cdef
commit 37fd43221f
No known key found for this signature in database
GPG Key ID: A6C7C781817146FA
20 changed files with 326 additions and 615 deletions

View File

@ -72,9 +72,7 @@ task testStorage, "Build & run Logos Storage tests":
task testIntegration, "Run integration tests":
buildBinary "storage",
outName = "storage",
params =
"-d:chronicles_runtime_filtering -d:chronicles_log_level=TRACE " &
"-d:storage_enable_nat_simulation=true"
params = "-d:chronicles_runtime_filtering -d:chronicles_log_level=TRACE"
test "testIntegration"
# use params to enable logging from the integration test executable
# test "testIntegration", params = "-d:chronicles_sinks=textlines[notimestamps,stdout],textlines[dynamic] " &
@ -94,6 +92,13 @@ task testNatPcpMapping, "Run PCP NAT integration test (requires miniupnpd contai
putEnv("STORAGE_INTEGRATION_TEST_INCLUDES", "nat/testnatpcp.nim")
test "testIntegration", outName = "testIntegrationNatPcp"
task testNatNotReachable,
"Run NAT not-reachable scenario (needs the image + podman-compose)":
test "integration/nat/not-reachable/testnotreachable", outName = "testNatNotReachable"
task testNatReachable, "Run NAT reachable scenario (needs the image + podman-compose)":
test "integration/nat/reachable/testreachable", outName = "testNatReachable"
task build, "build Logos Storage binary":
storageTask()

View File

@ -564,29 +564,6 @@ paths:
"500":
description: Well it was bad-bad
"/debug/nat/filtering":
post:
summary: "Set NAT simulation filtering behavior at runtime"
description: "Only available on nodes started with --nat-simulation. Used for testing NAT transitions."
tags: [Debug]
operationId: setNatFiltering
parameters:
- in: query
name: filtering
required: true
schema:
type: string
enum: [endpoint-independent, address-dependent, address-and-port-dependent, double-nat]
responses:
"200":
description: Filtering behavior updated successfully
"400":
description: Missing or invalid filtering value, or NAT simulation not active
"500":
description: Internal error
"/debug/info":
get:
summary: "Gets node information"

View File

@ -73,7 +73,6 @@ proc defaultDataDir*(): string =
const
storage_enable_api_debug_peers* {.booldefine.} = false
storage_enable_log_counter* {.booldefine.} = false
storage_enable_nat_simulation* {.booldefine.} = false
DefaultThreadCount* = ThreadCount(0)
@ -368,14 +367,6 @@ type
name: "nat-port-mapping-recheck-period"
.}: int
natSimulation* {.
desc:
"Simulate NAT filtering behavior for testing: endpoint-independent, address-dependent, address-and-port-dependent",
defaultValue: string.none,
name: "nat-simulation",
hidden
.}: Option[string]
autonatServer* {.
desc: "Enable AutoNAT server to help other nodes check their reachability",
defaultValue: false,

View File

@ -128,6 +128,9 @@ proc stop*(m: NatPortMapper) =
proc isPortMapped*(m: NatPortMapper, port: Port): bool =
m.activeTcpPort.isSome and m.activeTcpPort.get == port
method hasActiveMapping*(m: NatPortMapper): bool {.base, gcsafe.} =
m.tcpMappingId.isSome and m.udpMappingId.isSome
proc announcePeerInfoAddrs*(discovery: Discovery, peerInfo: PeerInfo, udpPort: Port) =
## Announces peerInfo.addrs to the DHT, excluding relay circuit addresses:
## they are announced via onReservation and must not enter the DHT routing
@ -192,11 +195,11 @@ method handleNatStatus*(
if dialBackAddr.isNone:
warn "Got empty dialback address in AutoNat when node is NotReachable"
if m.tcpMappingId.isSome and m.udpMappingId.isSome:
if m.hasActiveMapping():
m.close()
discovery.announceDirectAddrs(@[], udpPort = discoveryPort)
elif m.tcpMappingId.isSome and m.udpMappingId.isSome:
elif m.hasActiveMapping():
warn "Not Reachable with active port mapping. The port mapping will be deleted and relay will start."
# The mapping was created the the node is still not reachable.

View File

@ -40,7 +40,6 @@ import ../stores/repostore
import ../blockexchange
import ../units
import ../utils/options
import ../utils/natsimulation
import ../nat
import ./coders
@ -565,7 +564,6 @@ proc initDebugApi(
autonat: Option[AutonatV2Service],
autoRelay: Option[AutoRelayService],
natMapper: Option[NatPortMapper],
natRouter: Option[NatRouter],
router: var RestRouter,
) =
let allowedOrigin = router.allowedOrigin
@ -630,28 +628,6 @@ proc initDebugApi(
trace "Excepting processing request", exc = exc.msg
return RestApiResponse.error(Http500, headers = headers)
when storage_enable_nat_simulation:
router.api(MethodPost, "/api/storage/v1/debug/nat/filtering") do(
filtering: Option[string]
) -> RestApiResponse:
var headers = buildCorsHeaders("POST", allowedOrigin)
without natSimulation =? natRouter:
return RestApiResponse.error(
Http400, "NAT simulation not active on this node", headers = headers
)
without res =? filtering and filtering =? res:
return
RestApiResponse.error(Http400, "Missing filtering value", headers = headers)
let behavior = FilteringBehavior.fromString(filtering).valueOr:
return
RestApiResponse.error(Http400, "Invalid filtering value", headers = headers)
natSimulation.setFiltering(behavior)
return RestApiResponse.response("", headers = headers)
when storage_enable_api_debug_peers:
router.api(MethodGet, "/api/storage/v1/debug/peer/{peerId}") do(
peerId: PeerId
@ -679,13 +655,12 @@ proc initRestApi*(
autonat: Option[AutonatV2Service],
autoRelay: Option[AutoRelayService],
natMapper: Option[NatPortMapper],
natRouter: Option[NatRouter],
corsAllowedOrigin: ?string,
): RestRouter =
var router = RestRouter.init(validate, corsAllowedOrigin)
initDataApi(node, repoStore, router)
initNodeApi(node, conf, router)
initDebugApi(node, conf, autonat, autoRelay, natMapper, natRouter, router)
initDebugApi(node, conf, autonat, autoRelay, natMapper, router)
return router

View File

@ -22,6 +22,7 @@ import pkg/libp2p/protocols/connectivity/autonatv2/[service, client]
import pkg/libp2p/protocols/connectivity/relay/client as relayClientModule
import pkg/libp2p/protocols/connectivity/relay/relay as relayModule
import pkg/libp2p/services/autorelayservice
import pkg/libp2p/transports/tcptransport
import pkg/confutils
import pkg/confutils/defs
import pkg/stew/io2
@ -43,11 +44,15 @@ import ./storagetypes
import ./logutils
import ./nat
import ./utils/natutils
import ./utils/natsimulation
logScope:
topics = "storage node"
const StorageTransportFlags = {ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay}
proc tcpTransportBuilder(config: TransportConfig): Transport {.gcsafe, raises: [].} =
TcpTransport.new(StorageTransportFlags, config.upgr)
type
StorageServer* = ref object
config: StorageConf
@ -237,8 +242,10 @@ proc new*(
config: StorageConf,
privateKey: StoragePrivateKey,
logFile: Option[IoHandle] = IoHandle.none,
transportBuilder: TransportBuilder = tcpTransportBuilder,
): StorageServer =
## create StorageServer including setting up datastore, repostore, etc
## create StorageServer including setting up datastore, repostore, etc.
## ``transportBuilder`` defaults to TCP; tests inject a simulated NAT transport.
if err =? config.validateAutonatConfig().errorOption:
raise newException(StorageError, err.msg)
@ -320,32 +327,7 @@ proc new*(
# addresses.
switchBuilder = switchBuilder.withAddressPolicy(dialableAddressPolicy)
var natRouter: Option[NatRouter]
let switch =
when storage_enable_nat_simulation:
if config.natSimulation.isSome:
# Provide a NAT simulation useful for testing NAT Traversal
let filtering = FilteringBehavior.fromString(config.natSimulation.get).valueOr(
AddressAndPortDependent
)
let router = NatRouter.new(filtering)
natRouter = some(router)
switchBuilder
.withNatTransport(router, {ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay})
.build()
else:
switchBuilder
.withTcpTransport({ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay})
.build()
else:
if config.natSimulation.isSome:
raise newException(
StorageError,
"--nat-simulation requires a build with -d:storage_enable_nat_simulation=true",
)
switchBuilder
.withTcpTransport({ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay})
.build()
let switch = switchBuilder.withTransport(transportBuilder).build()
var taskPool: Taskpool
@ -500,10 +482,6 @@ proc new*(
)
)
# natRouter is some only when using nat simulation
if natRouter.isSome:
natRouter.get.natMapper = natMapper
peerInfoObserver =
some(setupPeerInfoObserver(switch, autonatService.get, discovery, natMapper.get))
@ -529,7 +507,7 @@ proc new*(
restServer = RestServerRef
.new(
storageNode.initRestApi(
config, repoStore, autonatService, autoRelayService, natMapper, natRouter,
config, repoStore, autonatService, autoRelayService, natMapper,
config.apiCorsAllowedOrigin,
),
initTAddress(config.apiBindAddress.get(), config.apiPort),

View File

@ -1,132 +0,0 @@
import std/options
import pkg/chronos
import pkg/questionable/results
import ../multinodes
import ../storageclient
import ../storageconfig
import ../nathelper
export nathelper
const DetectionTimeout = 15_000
# Reminder: multinodesuite setup the first node as bootstrap node
multinodesuite "AutoNAT detection":
let natConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
test "node is reachable when using bootstrap node on same network", natConfig:
let node2 = clients()[1]
await node2.client.checkReachable()
let endpointIndependentConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "endpoint-independent")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
# EIF = Endpoint Independent Filtering
test "node with simulated EIF nat is detected as reachable", endpointIndependentConfig:
let node2 = clients()[1]
await node2.client.checkReachable()
let autonatConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "address-and-port-dependent")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
# APDF = Address and Port-Dependent Filtering
test "node with simulated APDF nat is detected as not reachable and starts relay",
autonatConfig:
let node2 = clients()[1]
await node2.client.checkNotReachable()
let transitionConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "address-and-port-dependent")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
# APDF = Address and Port-Dependent Filtering
# EIF = Endpoint Independent Filtering
test "node with simulated APDF nat recovers to reachable and stops relay when nat switches to EIF nat",
transitionConfig:
let node2 = clients()[1]
await node2.client.checkNotReachable()
check (await node2.client.setNatFiltering("endpoint-independent")).isOk
await node2.client.checkReachable()
let natToSimConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "endpoint-independent")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
# APDF = Address and Port-Dependent Filtering
test "reachable node becomes not reachable and starts relay when nat switches to APDF nat",
natToSimConfig:
let node2 = clients()[1]
await node2.client.checkReachable()
check (await node2.client.setNatFiltering("address-and-port-dependent")).isOk
await node2.client.checkNotReachable()
let doubleNatConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "double-nat")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
test "node behind double NAT is detected as not reachable and starts relay",
doubleNatConfig:
let node2 = clients()[1]
await node2.client.checkNotReachable()
let multiNatConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 3)
.withRelay(0)
.withNatSimulation(idx = 1, "address-and-port-dependent")
.withNatSimulation(idx = 2, "address-and-port-dependent")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
# APDF = Address and Port-Dependent Filtering
test "two nodes with simulated APDF nat starts relay through the same relay node",
multiNatConfig:
let node2 = clients()[1]
let node3 = clients()[2]
await node2.client.checkNotReachable()
await node3.client.checkNotReachable()

View File

@ -1,74 +0,0 @@
import std/[json, sequtils]
import pkg/chronos
import pkg/questionable/results
import ../multinodes
import ../storageclient
import ../storageconfig
import ../nathelper
const
RelayTimeout = 30_000
PollInterval = 1_000
multinodesuite "NAT download":
let natDownloadConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 3)
.withRelay(idx = 0)
.withNatSimulation(idx = 2, "address-and-port-dependent")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
# APDF = Address and Port-Dependent Filtering
test "node 3 with simulated APDF downloads content from reachable seed node 2",
natDownloadConfig:
let seed = clients()[1]
let natNode = clients()[2]
let content = "content for nat download test"
let cid = (await seed.client.upload(content)).get
check eventuallySafe(
(await natNode.client.download(cid)).isOk,
timeout = RelayTimeout,
pollInterval = PollInterval,
)
check (await natNode.client.download(cid)).get == content
# APDF = Address and Port-Dependent Filtering
test "reachable node 2 downloads content from node 3 with simulated APDF via relay",
natDownloadConfig:
let seed = clients()[1]
let natNode = clients()[2]
check eventuallySafe(
(await natNode.client.natRelayRunning()).get(),
timeout = RelayTimeout,
pollInterval = PollInterval,
)
# relayRunning only means the service started: the reservation itself
# takes a few more seconds, so we have to poll.
proc advertisesCircuitAddr(): Future[bool] {.async.} =
let info = (await natNode.client.info()).get
let addrs = info["addrs"].getElems.mapIt(it.getStr)
return addrs.anyIt("p2p-circuit" in it)
check eventuallySafe(
await advertisesCircuitAddr(), timeout = RelayTimeout, pollInterval = PollInterval
)
let content = "content seeded from nat node"
let cid = (await natNode.client.upload(content)).get
check eventuallySafe(
(await seed.client.download(cid)).isOk,
timeout = RelayTimeout,
pollInterval = PollInterval,
)
check (await seed.client.download(cid)).get == content

View File

@ -44,25 +44,3 @@ multinodesuite "Rest API validation":
check:
response.status == 400
(await response.body) == "Incorrect Cid"
test "nat/filtering returns 400 when nat simulation not active", config:
let response = await client.post(
client.buildUrl("/debug/nat/filtering?filtering=endpoint-independent")
)
check response.status == 400
let natSimConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 1)
.withNatSimulation(idx = 0, "address-and-port-dependent").some
)
test "nat/filtering returns 400 for invalid filtering value", natSimConfig:
let response = await client.post(
client.buildUrl("/debug/nat/filtering?filtering=not-a-valid-value")
)
check response.status == 400
test "nat/filtering returns 400 when filtering param is missing", natSimConfig:
let response = await client.post(client.buildUrl("/debug/nat/filtering"))
check response.status == 400

View File

@ -283,12 +283,3 @@ proc natPortMapping*(
return info.get()["nat"]["portMapping"].getStr().success
except KeyError as e:
return failure e.msg
proc setNatFiltering*(
client: StorageClient, filtering: string
): Future[?!void] {.async: (raises: [CancelledError, HttpError]).} =
let response =
await client.post(client.baseurl & "/debug/nat/filtering?filtering=" & filtering)
if response.status != 200:
return failure "Failed to set NAT filtering: " & $response.status
return success()

View File

@ -346,15 +346,6 @@ proc isBootstrapNode*(config: StorageConfig): bool {.raises: [].} =
return false
proc withNatSimulation*(
self: StorageConfigs, idx: int, filtering: string
): StorageConfigs {.raises: [StorageConfigError].} =
self.checkBounds idx
var startConfig = self
startConfig.configs[idx].addCliOption("--nat-simulation", filtering)
return startConfig
proc withAutonatServer*(
self: StorageConfigs, idx: int
): StorageConfigs {.raises: [StorageConfigError].} =

View File

@ -1,93 +0,0 @@
import std/[json, strutils, sequtils]
import pkg/chronos
import pkg/questionable/results
import ../integration/multinodes
import ../integration/storageclient
import ../integration/storageconfig
import ../integration/nathelper
multinodesuite "AutoNAT PCP port mapping":
let pcpConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "address-and-port-dependent")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
test "node behind NAT maps ports via PCP and exposes mapping in debug info", pcpConfig:
let node2 = clients()[1]
await node2.client.checkNotReachable(relayRunning = false)
check eventuallySafe(
block:
let res = await node2.client.natPortMapping()
res.isOk and res.get == "pcp",
timeout = RelayTimeout,
pollInterval = PollInterval,
)
await node2.client.checkReachable()
await node2.stop()
let relayFallbackConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "double-nat")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
# Increase the max queue to trigger the AutoNat 2 times
.withNatMaxQueueSize(2).some
)
test "node behind double NAT falls back to relay after PCP mapping does not help",
relayFallbackConfig:
let node2 = clients()[1]
await node2.client.checkNotReachable(relayRunning = false)
check eventuallySafe(
block:
let res = await node2.client.natPortMapping()
res.isOk and res.get == "pcp",
timeout = RelayTimeout,
pollInterval = PollInterval,
)
# Wait for next Autonat iteration
await sleepAsync(6.seconds)
await node2.client.checkNotReachable()
test "reachable node downloads content uploaded by node behind NAT after PCP mapping",
pcpConfig:
let node1 = clients()[0]
let node2 = clients()[1]
check eventuallySafe(
block:
let res = await node2.client.natPortMapping()
res.isOk and res.get == "pcp",
timeout = RelayTimeout,
pollInterval = PollInterval,
)
let content = "content uploaded by nat node"
let cid = (await node2.client.upload(content)).get
check eventuallySafe(
(await node1.client.download(cid)).isOk,
timeout = RelayTimeout,
pollInterval = PollInterval,
)
check (await node1.client.download(cid)).get == content

View File

@ -1,94 +0,0 @@
import std/[json, strutils, sequtils]
import pkg/chronos
import pkg/questionable/results
import ../integration/multinodes
import ../integration/storageclient
import ../integration/storageconfig
import ../integration/nathelper
multinodesuite "AutoNAT UPnP port mapping":
let upnpConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "address-and-port-dependent")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
.withNatMaxQueueSize(1).some
)
test "node behind NAT maps ports via UPnP and exposes mapping in debug info",
upnpConfig:
let node2 = clients()[1]
await node2.client.checkNotReachable(relayRunning = false)
check eventuallySafe(
block:
let res = await node2.client.natPortMapping()
res.isOk and res.get == "upnp",
timeout = RelayTimeout,
pollInterval = PollInterval,
)
await node2.client.checkReachable()
await node2.stop()
let relayFallbackConfig = NodeConfigs(
clients: StorageConfigs
.init(nodes = 2)
.withRelay(0)
.withNatSimulation(idx = 1, "double-nat")
.withNatNumPeersToAsk(1)
.withNatMinConfidence(0.5)
.withNatScheduleInterval(NatScheduleInterval)
# Increase the max queue to trigger the AutoNat 2 times
.withNatMaxQueueSize(2).some
)
test "node behind double NAT falls back to relay after UPnP mapping does not help",
relayFallbackConfig:
let node2 = clients()[1]
await node2.client.checkNotReachable(relayRunning = false)
check eventuallySafe(
block:
let res = await node2.client.natPortMapping()
res.isOk and res.get == "upnp",
timeout = RelayTimeout,
pollInterval = PollInterval,
)
# Wait for next Autonat iteration
await sleepAsync(6.seconds)
await node2.client.checkNotReachable()
test "reachable node downloads content uploaded by node behind NAT after UPnP mapping",
upnpConfig:
let node1 = clients()[0]
let node2 = clients()[1]
check eventuallySafe(
block:
let res = await node2.client.natPortMapping()
res.isOk and res.get == "upnp",
timeout = RelayTimeout,
pollInterval = PollInterval,
)
let content = "content uploaded by nat node"
let cid = (await node2.client.upload(content)).get
check eventuallySafe(
(await node1.client.download(cid)).isOk,
timeout = RelayTimeout,
pollInterval = PollInterval,
)
check (await node1.client.download(cid)).get == content

View File

@ -1,11 +1,8 @@
# NAT simulation for integration testing.
#
# Testing NAT traversal in CI requires controlling inbound/outbound filtering
# rules, which is not possible with real network interfaces. This module wraps
# the TCP transport to enforce configurable filtering behaviors (endpoint-
# independent, address-dependent, address-and-port-dependent, double NAT) at
# the connection level, so the full AutoNAT detection and relay
# stack can be exercised without actual NAT hardware.
# It simulates the filtering behaviors (endpoint-independent, address-dependent,
# address-and-port-dependent, double NAT) at the connection level, so the full
# AutoNAT detection and relay stack can be exercised without actual NAT hardware.
{.push raises: [].}
@ -18,7 +15,7 @@ import pkg/libp2p/transports/tcptransport
import pkg/libp2p/transports/transport
import pkg/libp2p/wire
import ../nat
import ../../storage/nat
logScope:
topics = "nat simulation"

View File

@ -0,0 +1,241 @@
## NAT detection unit tests: real AutoNAT v2 detecting
## through the NAT simulation, feeding storage's handleNatStatus, which drives
## the relay and client mode.
##
## The MockNatPortMapper simulates a failing port mapping. This is not the aspect
## of the NAT detection that is being tested: we want to test the NAT detection
## logic itself, not the port mapping logic.
import std/options
import pkg/chronos
import pkg/libp2p except setup
import pkg/libp2p/protocols/connectivity/autonatv2/service except setup
import pkg/libp2p/protocols/connectivity/autonatv2/client except setup
import pkg/libp2p/protocols/connectivity/autonatv2/types as autonatv2Types
import pkg/libp2p/protocols/connectivity/relay/client as relayClientModule
import pkg/libp2p/services/autorelayservice except setup
import pkg/libp2p/observedaddrmanager
import ./helpers
import ./natsimulation
import ../asynctest
import ../../storage/utils/natutils
import ../../storage/nat
import ../../storage/discovery
import ../../storage/rng
const
flags = {ServerFlags.ReuseAddr}
listenAddr = "/ip4/127.0.0.1/tcp/0"
discoveryPort = Port(8090)
# ms — AutoNAT probe + confidence + reaction
detectTimeout = 20000
type MockNatPortMapper = ref object of NatPortMapper
method mapNatPorts*(
m: MockNatPortMapper
): Future[Option[(Port, Port, MappingProtocol)]] {.
async: (raises: [CancelledError]), gcsafe
.} =
none((Port, Port, MappingProtocol))
# Captures the candidate addresses the service sends and answers Reachable, so
# the service flips to reachable and runs its address mapper — without dialing.
type MockAutonatV2Client = ref object of AutonatV2Client
reqAddrs: seq[MultiAddress]
method sendDialRequest*(
self: MockAutonatV2Client, pid: PeerId, testAddrs: seq[MultiAddress]
): Future[AutonatV2Response] {.
async: (raises: [AutonatV2Error, CancelledError, DialFailedError, LPStreamError])
.} =
self.reqAddrs = testAddrs
AutonatV2Response(reachability: Reachable)
proc serverSwitch(): Switch =
SwitchBuilder
.new()
.withRng(Rng.instance())
.withPrivateKey(PrivateKey.random(Rng.instance()).get())
.withAddresses(@[MultiAddress.init(listenAddr).get()])
.withTcpTransport(flags)
.withNoise()
.withYamux()
.withAutonatV2Server()
.build()
asyncchecksuite "NAT detection - simulated NAT":
var
natNode: Switch
autonat: AutonatV2Service
relay: AutoRelayService
disc: Discovery
server: Switch
proc setupTopology(router: NatRouter) {.async.} =
let relayClient = relayClientModule.RelayClient.new()
natNode = SwitchBuilder
.new()
.withRng(Rng.instance())
.withPrivateKey(PrivateKey.random(Rng.instance()).get())
.withAddresses(@[MultiAddress.init(listenAddr).get()])
.withNatTransport(router, flags)
.withNoise()
.withYamux()
.withCircuitRelay(relayClient)
.build()
relay = AutoRelayService.new(1, relayClient, nil, Rng.instance())
autorelayservice.setup(relay, natNode)
disc = Discovery.new(PrivateKey.random(Rng.instance()).get(), announceAddrs = @[])
# nodes start in client mode until Reachable
disc.protocol.clientMode = true
# Setup real AutoNAT v2 client using nat simulation
let autonatClient = AutonatV2Client.new(natNode.rng)
client.setup(autonatClient, natNode)
natNode.mount(autonatClient)
# Setup AutoNAT v2 service with maxQueueSize=1 and minConfidence=0.5,
# so a single dial-back answer (confidence 1.0) is needed.
let config = AutonatV2ServiceConfig.new(
scheduleInterval = Opt.some(1.seconds),
askNewConnectedPeers = true,
numPeersToAsk = 1,
maxQueueSize = 1,
minConfidence = 0.5,
)
autonat = AutonatV2Service.new(natNode.rng, autonatClient, config)
service.setup(autonat, natNode)
autonat.setStatusAndConfidenceHandler(
proc(
reachability: NetworkReachability,
confidence: Opt[float],
addrs: Opt[MultiAddress],
) {.async: (raises: [CancelledError]).} =
# One call to our handleNatStatus handler
await MockNatPortMapper().handleNatStatus(
reachability, addrs, discoveryPort, disc, natNode, relay
)
)
# Create and start one Autonat server (maxQueueSize=1 and minConfidence=0.5)
server = serverSwitch()
await server.start()
# Start the NAT node and connect to the Autonat server (bootstrap node in our network).
# Then start the Autonat service on the NAT node.
await natNode.start()
await natNode.connect(server.peerInfo.peerId, server.peerInfo.addrs)
await autonat.start(natNode)
teardown:
await autonat.stop(natNode)
if relay.isRunning:
await relay.stop(natNode)
await natNode.stop()
await server.stop()
test "node behind EIF nat ends up reachable: no relay, not in client mode":
await setupTopology(NatRouter.new(EndpointIndependent))
check eventually(
not relay.isRunning and not disc.protocol.clientMode, timeout = detectTimeout
)
test "node behind APDF nat ends up not reachable: relay running, client mode":
await setupTopology(NatRouter.new(AddressAndPortDependent))
check eventually(
relay.isRunning and disc.protocol.clientMode, timeout = detectTimeout
)
test "node behind double NAT ends up not reachable: relay running, client mode":
await setupTopology(NatRouter.new(DoubleNat))
check eventually(
relay.isRunning and disc.protocol.clientMode, timeout = detectTimeout
)
test "node recovers (relay stops) when nat switches from APDF to EIF":
let router = NatRouter.new(AddressAndPortDependent)
await setupTopology(router)
check eventually(
relay.isRunning and disc.protocol.clientMode, timeout = detectTimeout
)
router.setFiltering(EndpointIndependent)
check eventually(
not relay.isRunning and not disc.protocol.clientMode, timeout = detectTimeout
)
test "node degrades (relay starts) when nat switches from EIF to APDF":
let router = NatRouter.new(EndpointIndependent)
await setupTopology(router)
check eventually(
not relay.isRunning and not disc.protocol.clientMode, timeout = detectTimeout
)
router.setFiltering(AddressAndPortDependent)
check eventually(
relay.isRunning and disc.protocol.clientMode, timeout = detectTimeout
)
asyncchecksuite "NAT detection - dial request candidates":
# This detects is useful to detect behaviour changes in libp2p
# that may break our autonat dial request candidate handling.
#
# By default, Autonat V2 dials the addresses passed to peerInfo.addrs and
# uses the first attempt to dial as the primary candidate.
#
# With enableDialableCandidates, Autonat V2 also dials the guessDialableAddress
# as first candidate and use the most observed address as the fallback.
var sw: Switch
setup:
sw = newStandardSwitch()
await sw.start()
teardown:
await sw.stop()
test "autonat handles the observed dialable address":
let mockClient = MockAutonatV2Client()
let autonat = AutonatV2Service.new(
Rng.instance(),
mockClient,
AutonatV2ServiceConfig.new(
enableDialableCandidates = true, maxQueueSize = 1, minConfidence = 0.5
),
)
service.setup(autonat, sw)
await autonat.start(sw) # registers the address mapper on peerInfo
# observations before the manager trusts an addr in libp2p; the observed
# port (4001) differs from our listen port on purpose (see dialable below)
let quorum = 3
let observed = MultiAddress.init("/ip4/8.8.8.8/tcp/4001").expect("valid")
for _ in 0 ..< quorum:
discard sw.peerStore.identify.observedAddrManager.addObservation(observed)
let sw2 = newStandardSwitch()
await sw2.start()
await sw.connect(sw2.peerInfo.peerId, sw2.peerInfo.addrs)
# The dialable candidate keeps our real listen port and swaps in the
# observed IP. It must be reached via AutoNAT and peerInfo, so it can only
# come from guessDialableAddr.
let tcpPart = sw.peerInfo.listenAddrs[0][1].expect("valid")
let dialable =
concat(MultiAddress.init("/ip4/8.8.8.8").expect("valid"), tcpPart).expect("valid")
# Phase 1: it is submitted as a dial candidate, not 127.0.0.1 from
# newStandardSwitch.
check eventually(dialable in mockClient.reqAddrs)
# Phase 2: the address mapper promotes it into peerInfo.
check eventually(dialable in sw.peerInfo.addrs)
await autonat.stop(sw)
await sw2.stop()

View File

@ -3,11 +3,7 @@ import pkg/chronos
import pkg/libp2p/[multiaddress, multihash, multicodec]
import pkg/libp2p/protocols/connectivity/autonat/types
import pkg/libp2p/protocols/connectivity/autonatv2/service except setup
import pkg/libp2p/protocols/connectivity/autonatv2/client except setup
import pkg/libp2p/protocols/connectivity/autonatv2/types as autonatv2Types
import pkg/libp2p/protocols/connectivity/relay/client as relayClientModule
import pkg/libp2p/protocols/connectivity/dcutr/core as dcutrCore
import pkg/libp2p/multistream
import pkg/libp2p/services/autorelayservice except setup
import pkg/results
@ -21,6 +17,7 @@ import ../../storage/utils
type MockNatPortMapper = ref object of NatPortMapper
mappedPorts: Option[(Port, Port, MappingProtocol)]
activeMapping: bool
method mapNatPorts*(
m: MockNatPortMapper
@ -29,18 +26,10 @@ method mapNatPorts*(
.} =
m.mappedPorts
type MockAutonatV2Client = ref object of AutonatV2Client
reqAddrs: seq[MultiAddress]
method hasActiveMapping*(m: MockNatPortMapper): bool =
m.activeMapping
method sendDialRequest*(
self: MockAutonatV2Client, pid: PeerId, testAddrs: seq[MultiAddress]
): Future[AutonatV2Response] {.
async: (raises: [AutonatV2Error, CancelledError, DialFailedError, LPStreamError])
.} =
self.reqAddrs = testAddrs
AutonatV2Response(reachability: Unknown)
asyncchecksuite "NAT - handleNatStatus":
asyncchecksuite "NAT reaction - port mapping":
var sw: Switch
var key: PrivateKey
var disc: Discovery
@ -78,7 +67,7 @@ asyncchecksuite "NAT - handleNatStatus":
check not autoRelay.isRunning
check disc.protocol.clientMode
test "handleNatStatus starts autoRelay when NotReachable and no dialBackAddr":
test "handleNatStatus starts autoRelay when NotReachable and no dialBackAddr but no mapped ports":
let mapper = MockNatPortMapper(mappedPorts: none((Port, Port, MappingProtocol)))
autorelayservice.setup(autoRelay, sw)
@ -102,7 +91,32 @@ asyncchecksuite "NAT - handleNatStatus":
check disc.announceAddrs == newSeq[MultiAddress]()
check disc.protocol.clientMode
test "handleNatStatus stops relay and exits client mode when Reachable":
test "handleNatStatus tears down an active mapping and starts relay when NotReachable with dialBackAddr":
let dialBack = MultiAddress.init("/ip4/1.2.3.4/tcp/8080").expect("valid")
let mapper = MockNatPortMapper(activeMapping: true)
autorelayservice.setup(autoRelay, sw)
await mapper.handleNatStatus(
NotReachable, Opt.some(dialBack), discoveryPort, disc, sw, autoRelay
)
check autoRelay.isRunning
check disc.announceAddrs == newSeq[MultiAddress]()
check disc.protocol.clientMode
test "handleNatStatus tears down an active mapping and starts relay when NotReachable without dialBackAddr":
let mapper = MockNatPortMapper(activeMapping: true)
autorelayservice.setup(autoRelay, sw)
await mapper.handleNatStatus(
NotReachable, Opt.none(MultiAddress), discoveryPort, disc, sw, autoRelay
)
check autoRelay.isRunning
check disc.announceAddrs == newSeq[MultiAddress]()
check disc.protocol.clientMode
test "handleNatStatus stops relay and exits client mode when mapping is created and node is Reachable":
let mapper = MockNatPortMapper(mappedPorts: none((Port, Port, MappingProtocol)))
disc.protocol.clientMode = true
@ -129,6 +143,22 @@ asyncchecksuite "NAT - handleNatStatus":
check not autoRelay.isRunning
check disc.announceAddrs == newSeq[MultiAddress]()
asyncchecksuite "NAT reaction - address announcing":
var sw: Switch
var key: PrivateKey
var disc: Discovery
setup:
key = PrivateKey.random(Rng.instance()).get()
disc = Discovery.new(key, announceAddrs = @[])
sw = newStandardSwitch()
await sw.start()
teardown:
await sw.stop()
let discoveryPort = Port(8090)
test "announcePeerInfoAddrs excludes relay circuit addresses":
let circuitAddr = MultiAddress
.init("/ip4/1.2.3.4/tcp/4040/p2p/" & $sw.peerInfo.peerId & "/p2p-circuit")
@ -192,56 +222,3 @@ asyncchecksuite "NAT - handleNatStatus":
await sw.peerInfo.update()
check disc.announceAddrs == newSeq[MultiAddress]()
test "autonat dial request includes the observed addresses as candidates":
# The dial request includes the addresses observed by other peers, so a NATed node submits
# a dialable candidate even though its listen addrs are private.
let client = MockAutonatV2Client()
let autonat = AutonatV2Service.new(
Rng.instance(),
client,
AutonatV2ServiceConfig.new(enableDialableCandidates = true),
)
service.setup(autonat, sw)
await autonat.start(sw)
let observed = MultiAddress.init("/ip4/8.8.8.8/tcp/4001").expect("valid")
for _ in 0 ..< 3: # minCount: 3 observations before the manager trusts an addr
discard sw.peerStore.identify.observedAddrManager.addObservation(observed)
let sw2 = newStandardSwitch()
await sw2.start()
await sw.connect(sw2.peerInfo.peerId, sw2.peerInfo.addrs)
check eventually(observed in client.reqAddrs)
await autonat.stop(sw)
await sw2.stop()
asyncchecksuite "NAT - Hole punching":
test "setupHolePunching mounts the dcutr protocol on the switch":
let sw = newStandardSwitch()
discard setupHolePunching(sw)
check sw.ms.handlers.anyIt(dcutrCore.DcutrCodec in it.protos)
test "holePunchIfRelayed returns early when the peer has no connections":
let sw1 = newStandardSwitch()
let sw2 = newStandardSwitch()
await allFutures(sw1.start(), sw2.start())
await holePunchIfRelayed(sw1, sw2.peerInfo.peerId)
await allFutures(sw1.stop(), sw2.stop())
test "holePunchIfRelayed returns early when a direct connection already exists":
let sw1 = newStandardSwitch()
let sw2 = newStandardSwitch()
await allFutures(sw1.start(), sw2.start())
await sw1.connect(sw2.peerInfo.peerId, sw2.peerInfo.addrs)
check sw1.isConnected(sw2.peerInfo.peerId)
await holePunchIfRelayed(sw1, sw2.peerInfo.peerId)
check sw1.isConnected(sw2.peerInfo.peerId)
await allFutures(sw1.stop(), sw2.stop())

View File

@ -6,7 +6,7 @@ import ./helpers
import ../asynctest
import ../../storage/rng
import ../../storage/nat
import ../../storage/utils/natsimulation
import ./natsimulation
const flags = {ServerFlags.ReuseAddr}
const listenAddr = "/ip4/127.0.0.1/tcp/0"
@ -44,7 +44,7 @@ proc newNatSwitch(router: NatRouter, rng: Rng): Switch =
.withYamux()
.build()
asyncchecksuite "NatTransport - Endpoint-Independent Filtering":
asyncchecksuite "Nat transport - Endpoint-Independent Filtering":
var bootstrap, natNode: Switch
setup:
@ -62,7 +62,7 @@ asyncchecksuite "NatTransport - Endpoint-Independent Filtering":
await bootstrap.connect(natNode.peerInfo.peerId, natNode.peerInfo.addrs)
check bootstrap.isConnected(natNode.peerInfo.peerId)
asyncchecksuite "NatTransport - Address-Dependent Filtering":
asyncchecksuite "Nat transport - Address-Dependent Filtering":
var bootstrap, thirdNode, natNode: Switch
setup:
@ -94,7 +94,7 @@ asyncchecksuite "NatTransport - Address-Dependent Filtering":
test "bootstrap cannot connect to nat node without a pre-existing connection":
check await cannotConnect(bootstrap, natNode)
asyncchecksuite "NatTransport - Address-and-Port-Dependent Filtering":
asyncchecksuite "Nat transport - Address-and-Port-Dependent Filtering":
var bootstrap, thirdNode, natNode: Switch
setup:
@ -125,7 +125,7 @@ asyncchecksuite "NatTransport - Address-and-Port-Dependent Filtering":
await natNode.connect(bootstrap.peerInfo.peerId, bootstrap.peerInfo.addrs)
check await cannotConnect(thirdNode, natNode)
asyncchecksuite "NatTransport - Double NAT":
asyncchecksuite "Nat transport - Double NAT":
var bootstrap, natNode: Switch
var router: NatRouter
@ -148,7 +148,7 @@ asyncchecksuite "NatTransport - Double NAT":
check await cannotConnect(bootstrap, natNode)
asyncchecksuite "NatTransport - Port Mapping":
asyncchecksuite "Nat transport - Port Mapping":
var bootstrap, natNode: Switch
var router: NatRouter

@ -1 +1 @@
Subproject commit f8111056182cf6abd9e35de77a919e873ef94652
Subproject commit e77caabae78fbc9aa5b78a0a521181b077c82571

2
vendor/nim-lsquic vendored

@ -1 +1 @@
Subproject commit 00e4b7dfaa197cd120267aa897b33b0914166b45
Subproject commit 2f01046bf1d513de8b5f8296c3d8bec819ab0cb9

@ -1 +1 @@
Subproject commit f45476a3c1f4e7bff73845e6450d686be040ddeb
Subproject commit d9aa950b9d9e8bfc8a201740042b5e8ea5880875