logos-storage-nim/tests/storage/testnatdetection.nim

315 lines
11 KiB
Nim

## 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
mockMappedTcpPort = Port(40000)
mockMappedUdpPort = Port(40001)
type MockNatPortMapper = ref object of NatPortMapper
method mapNatPorts*(
m: MockNatPortMapper
): Future[Option[(Port, Port, MappingProtocol)]] {.
async: (raises: [CancelledError]), gcsafe
.} =
none((Port, Port, MappingProtocol))
# Simulates a successful PCP mapping
type MockMappingNatPortMapper = ref object of NatPortMapper
method mapNatPorts*(
m: MockMappingNatPortMapper
): Future[Option[(Port, Port, MappingProtocol)]] {.
async: (raises: [CancelledError]), gcsafe
.} =
m.activeTcpPort = some(mockMappedTcpPort)
m.activeUdpPort = some(mockMappedUdpPort)
m.activeMappingProtocol = some(MappingProtocol.PCP)
some((mockMappedTcpPort, mockMappedUdpPort, MappingProtocol.PCP))
# 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().libp2pRng)
.withPrivateKey(PrivateKey.random(Rng.instance().libp2pRng).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().libp2pRng)
.withPrivateKey(PrivateKey.random(Rng.instance().libp2pRng).get())
.withAddresses(@[MultiAddress.init(listenAddr).get()])
.withNatTransport(router, flags)
.withNoise()
.withYamux()
.withCircuitRelay(relayClient)
.build()
relay = AutoRelayService.new(1, relayClient, nil, Rng.instance().libp2pRng)
autorelayservice.setup(relay, natNode)
disc = Discovery.new(
PrivateKey.random(Rng.instance().libp2pRng).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().libp2pRng,
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()
test "after a port mapping, the mapped address is AutoNAT's first dial candidate":
let mapper = MockMappingNatPortMapper()
setupMappedAddrMapper(sw, mapper)
# Reach the observation quorum so guessDialableAddr trusts 8.8.8.8
let observed = MultiAddress.init("/ip4/8.8.8.8/tcp/4001").expect("valid")
let quorum = 3
for _ in 0 ..< quorum:
discard sw.peerStore.identify.observedAddrManager.addObservation(observed)
# Setup AutoRelayService
let relay = AutoRelayService.new(
1, relayClientModule.RelayClient.new(), nil, Rng.instance().libp2pRng
)
autorelayservice.setup(relay, sw)
# Define our handleNatStatus callback
let disc = Discovery.new(
PrivateKey.random(Rng.instance().libp2pRng).get(), announceAddrs = @[]
)
let dialBack = MultiAddress.init("/ip4/8.8.8.8/tcp/8080").expect("valid")
await mapper.handleNatStatus(
NotReachable, Opt.some(dialBack), discoveryPort, disc, sw, relay
)
# Define our AutonatV2Service
let mockClient = MockAutonatV2Client()
let autonat = AutonatV2Service.new(
Rng.instance().libp2pRng,
mockClient,
AutonatV2ServiceConfig.new(
enableDialableCandidates = true, maxQueueSize = 1, minConfidence = 0.5
),
)
service.setup(autonat, sw)
await autonat.start(sw)
# Connect to a second switch to test NAT detection
let sw2 = newStandardSwitch()
await sw2.start()
await sw.connect(sw2.peerInfo.peerId, sw2.peerInfo.addrs)
# The expected mapped address should be the guessDialableAddr (8.8.8.8)
# using the mapping mocked port (40000) because a mapping was created.
let mapped =
MultiAddress.init("/ip4/8.8.8.8/tcp/" & $mockMappedTcpPort).expect("valid")
check eventually(mapped in mockClient.reqAddrs)
# Ensute that it comes first (because AutonatV2 test only the first candidate)
check mockClient.reqAddrs[0] == mapped
await autonat.stop(sw)
await sw2.stop()
if relay.isRunning:
await relay.stop(sw)