diff --git a/build.nims b/build.nims index 2f46a6ab..1eb7acbb 100644 --- a/build.nims +++ b/build.nims @@ -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() diff --git a/openapi.yaml b/openapi.yaml index c310efa2..9bb3ab03 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -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" diff --git a/storage/conf.nim b/storage/conf.nim index 618d3d4f..b1ea573c 100644 --- a/storage/conf.nim +++ b/storage/conf.nim @@ -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, diff --git a/storage/nat.nim b/storage/nat.nim index 0189480f..cbda208e 100644 --- a/storage/nat.nim +++ b/storage/nat.nim @@ -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. diff --git a/storage/rest/api.nim b/storage/rest/api.nim index c251d406..597ff321 100644 --- a/storage/rest/api.nim +++ b/storage/rest/api.nim @@ -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 diff --git a/storage/storage.nim b/storage/storage.nim index c933bc27..a7c1b2c6 100644 --- a/storage/storage.nim +++ b/storage/storage.nim @@ -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), diff --git a/tests/integration/1_minute/testnat.nim b/tests/integration/1_minute/testnat.nim deleted file mode 100644 index f7981030..00000000 --- a/tests/integration/1_minute/testnat.nim +++ /dev/null @@ -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() diff --git a/tests/integration/5_minutes/testnatdownload.nim b/tests/integration/5_minutes/testnatdownload.nim deleted file mode 100644 index 89299760..00000000 --- a/tests/integration/5_minutes/testnatdownload.nim +++ /dev/null @@ -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 diff --git a/tests/integration/5_minutes/testrestapivalidation.nim b/tests/integration/5_minutes/testrestapivalidation.nim index ab0a1b99..20a3ad40 100644 --- a/tests/integration/5_minutes/testrestapivalidation.nim +++ b/tests/integration/5_minutes/testrestapivalidation.nim @@ -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 diff --git a/tests/integration/storageclient.nim b/tests/integration/storageclient.nim index dc852533..50b03376 100644 --- a/tests/integration/storageclient.nim +++ b/tests/integration/storageclient.nim @@ -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() diff --git a/tests/integration/storageconfig.nim b/tests/integration/storageconfig.nim index 32d5b536..2f64ef79 100644 --- a/tests/integration/storageconfig.nim +++ b/tests/integration/storageconfig.nim @@ -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].} = diff --git a/tests/nat/testnatpcp.nim b/tests/nat/testnatpcp.nim deleted file mode 100644 index 25f5902b..00000000 --- a/tests/nat/testnatpcp.nim +++ /dev/null @@ -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 diff --git a/tests/nat/testnatupnp.nim b/tests/nat/testnatupnp.nim deleted file mode 100644 index 9f7c29bf..00000000 --- a/tests/nat/testnatupnp.nim +++ /dev/null @@ -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 diff --git a/storage/utils/natsimulation.nim b/tests/storage/natsimulation.nim similarity index 92% rename from storage/utils/natsimulation.nim rename to tests/storage/natsimulation.nim index 514684e7..2ae0b77d 100644 --- a/storage/utils/natsimulation.nim +++ b/tests/storage/natsimulation.nim @@ -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" diff --git a/tests/storage/testnatdetection.nim b/tests/storage/testnatdetection.nim new file mode 100644 index 00000000..3cc01f34 --- /dev/null +++ b/tests/storage/testnatdetection.nim @@ -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() diff --git a/tests/storage/testnat.nim b/tests/storage/testnatreaction.nim similarity index 69% rename from tests/storage/testnat.nim rename to tests/storage/testnatreaction.nim index 4f2c81df..1f0d17ef 100644 --- a/tests/storage/testnat.nim +++ b/tests/storage/testnatreaction.nim @@ -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()) diff --git a/tests/storage/testnatsimulation.nim b/tests/storage/testnatsimulation.nim index f2079c8a..00f6652d 100644 --- a/tests/storage/testnatsimulation.nim +++ b/tests/storage/testnatsimulation.nim @@ -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 diff --git a/vendor/nim-boringssl b/vendor/nim-boringssl index f8111056..e77caaba 160000 --- a/vendor/nim-boringssl +++ b/vendor/nim-boringssl @@ -1 +1 @@ -Subproject commit f8111056182cf6abd9e35de77a919e873ef94652 +Subproject commit e77caabae78fbc9aa5b78a0a521181b077c82571 diff --git a/vendor/nim-lsquic b/vendor/nim-lsquic index 00e4b7df..2f01046b 160000 --- a/vendor/nim-lsquic +++ b/vendor/nim-lsquic @@ -1 +1 @@ -Subproject commit 00e4b7dfaa197cd120267aa897b33b0914166b45 +Subproject commit 2f01046bf1d513de8b5f8296c3d8bec819ab0cb9 diff --git a/vendor/nim-protobuf-serialization b/vendor/nim-protobuf-serialization index f45476a3..d9aa950b 160000 --- a/vendor/nim-protobuf-serialization +++ b/vendor/nim-protobuf-serialization @@ -1 +1 @@ -Subproject commit f45476a3c1f4e7bff73845e6450d686be040ddeb +Subproject commit d9aa950b9d9e8bfc8a201740042b5e8ea5880875