diff --git a/storage/nat.nim b/storage/nat.nim index ad06fba3..eccb193a 100644 --- a/storage/nat.nim +++ b/storage/nat.nim @@ -18,6 +18,7 @@ import pkg/chronos import pkg/chronicles import pkg/libp2p import pkg/libp2p/protocols/connectivity/autonatv2/service +import pkg/libp2p/services/autorelayservice import ./utils import ./utils/natutils @@ -66,12 +67,18 @@ type PrefSrcStatus = enum type NatMapper* = ref object of RootObj method mapNatAddresses*( - m: NatMapper, addrs: seq[MultiAddress], discoveryPort: Port + m: NatMapper, addrs: seq[MultiAddress] ): tuple[libp2p, discovery: seq[MultiAddress]] {.base, gcsafe, raises: [].} = raiseAssert "mapNatAddresses not implemented" +method getReachableAddresses*( + m: NatMapper, addrs: seq[MultiAddress] +): tuple[libp2p, discovery: seq[MultiAddress]] {.base, gcsafe, raises: [].} = + raiseAssert "getReachableAddresses not implemented" + type DefaultNatMapper* = ref object of NatMapper natConfig*: NatConfig + discoveryPort*: Port ## Also does threadvar initialisation. ## Must be called before redirectPorts() in each thread. @@ -451,29 +458,70 @@ proc nattedAddress*( (newAddrs, discoveryAddrs) method mapNatAddresses*( - m: DefaultNatMapper, addrs: seq[MultiAddress], discoveryPort: Port + m: DefaultNatMapper, addrs: seq[MultiAddress] ): tuple[libp2p, discovery: seq[MultiAddress]] {.gcsafe, raises: [].} = - nattedAddress(m.natConfig, addrs, discoveryPort) + nattedAddress(m.natConfig, addrs, m.discoveryPort) + +method getReachableAddresses*( + m: DefaultNatMapper, addrs: seq[MultiAddress] +): tuple[libp2p, discovery: seq[MultiAddress]] {.gcsafe, raises: [].} = + let ip = + if m.natConfig.hasExtIp: + some(m.natConfig.extIp) + else: + let (routeIp, _) = getRoutePrefSrc(static parseIpAddress("0.0.0.0")) + routeIp + if ip.isNone: + return (@[], @[]) + let announceAddrs = + addrs.mapIt(it.remapAddr(ip = ip, port = none(Port))).deduplicate() + (announceAddrs, @[getMultiAddrWithIPAndUDPPort(ip.get, m.discoveryPort)]) + +proc hasPublicIp*(addrs: seq[MultiAddress]): bool = + for addr in addrs: + let (ip, _) = getAddressAndPort(addr) + if ip.isSome and isGlobalUnicast(ip.get): + return true proc handleNatStatus*( networkReachability: NetworkReachability, - confidence: Opt[float], mapper: NatMapper, - listenAddrs: seq[MultiAddress], - discoveryPort: Port, discovery: Discovery, + switch: Switch, + autoRelayService: AutoRelayService, ) {.async: (raises: [CancelledError]).} = - debug "AutoNAT status", reachability = networkReachability, confidence - case networkReachability - of Reachable: - # TODO: switch DHT to server mode, stop relay if running - discard - of NotReachable: - let (announceAddrs, discoveryAddrs) = - mapper.mapNatAddresses(listenAddrs, discoveryPort) - discovery.updateAnnounceRecord(announceAddrs) - discovery.updateDhtRecord(announceAddrs & discoveryAddrs) of Unknown: # Nothing to do here, not enough confidence score result discard + of Reachable: + # For UPnP, it the mapping was a success, + # the autorelay service has been stopped + # and the address was already announced + if autoRelayService.isRunning: + if not await autoRelayService.stop(switch): + debug "AutoRelayService stop method returned false" + + let (announceAddrs, discoveryAddrs) = + mapper.getReachableAddresses(switch.peerInfo.addrs) + discovery.updateAnnounceRecord(announceAddrs) + discovery.updateDhtRecord(announceAddrs & discoveryAddrs) + # TODO: switch DHT to server mode + of NotReachable: + let (announceAddrs, discoveryAddrs) = mapper.mapNatAddresses(switch.peerInfo.addrs) + + # With a UPnP / NatPmP successful mapping, + # we suppose that having a public IP make it Reachable. + # If not, the state will be updated in the next Autonat iteration. + # TODO: Do we need to manually call dialMe to make sure we are Reachable ? + if hasPublicIp(announceAddrs): + discovery.updateAnnounceRecord(announceAddrs) + discovery.updateDhtRecord(announceAddrs & discoveryAddrs) + + if autoRelayService.isRunning: + if not await autoRelayService.stop(switch): + debug "AutoRelayService stop method returned false" + else: + if not autoRelayService.isRunning: + if not await autoRelayService.setup(switch): + debug "AutoRelayService setup method returned false" diff --git a/storage/storage.nim b/storage/storage.nim index c760e7dd..ff9d97b3 100644 --- a/storage/storage.nim +++ b/storage/storage.nim @@ -18,6 +18,8 @@ import pkg/taskpools import pkg/presto import pkg/libp2p import pkg/libp2p/protocols/connectivity/autonatv2/[service, client] +import pkg/libp2p/protocols/connectivity/relay/client as relayClientModule +import pkg/libp2p/services/autorelayservice import pkg/confutils import pkg/confutils/defs import pkg/stew/io2 @@ -54,6 +56,7 @@ type maintenance: BlockMaintainer taskpool: Taskpool autonatService*: AutonatV2Service + autoRelayService: AutoRelayService isStarted: bool StoragePrivateKey* = libp2p.PrivateKey # alias @@ -195,6 +198,8 @@ proc new*( ## create StorageServer including setting up datastore, repostore, etc let listenMultiAddr = getMultiAddrWithIpAndTcpPort(config.listenIp, config.listenPort) + let relayClient = relayClientModule.RelayClient.new() + let autonatClient = AutonatV2Client.new(random.Rng.instance()) let autonatService = AutonatV2Service.new( rng = random.Rng.instance(), @@ -220,6 +225,7 @@ proc new*( .withSignedPeerRecord(true) .withTcpTransport({ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay}) .withAutonatV2Server() + .withCircuitRelay(relayClient) .withServices(@[Service(autonatService)]) .build() @@ -327,6 +333,16 @@ proc new*( taskPool = taskPool, ) + autoRelayService = AutoRelayService.new( + maxNumRelays = config.natMaxRelays, + client = relayClient, + onReservation = proc(addresses: seq[MultiAddress]) {.gcsafe, raises: [].} = + debug "Relay reservation updated", addresses + discovery.updateAnnounceRecord(addresses) + discovery.updateDhtRecord(addresses), + rng = random.Rng.instance(), + ) + var restServer: RestServerRef = nil if config.apiBindAddress.isSome: @@ -344,14 +360,15 @@ proc new*( switch.mount(network) switch.mount(manifestProto) - let natMapper = DefaultNatMapper(natConfig: config.nat) + let natMapper = + DefaultNatMapper(natConfig: config.nat, discoveryPort: config.discoveryPort) autonatService.setStatusAndConfidenceHandler( proc( networkReachability: NetworkReachability, confidence: Opt[float] ) {.async: (raises: [CancelledError]).} = + debug "AutoNAT status", reachability = networkReachability, confidence await handleNatStatus( - networkReachability, confidence, natMapper, switch.peerInfo.addrs, - config.discoveryPort, discovery, + networkReachability, natMapper, discovery, switch, autoRelayService ) ) @@ -364,4 +381,5 @@ proc new*( taskPool: taskPool, logFile: logFile, autonatService: autonatService, + autoRelayService: autoRelayService, ) diff --git a/tests/storage/testnat.nim b/tests/storage/testnat.nim index 0acb0725..3f772ee2 100644 --- a/tests/storage/testnat.nim +++ b/tests/storage/testnat.nim @@ -1,10 +1,15 @@ -import std/[unittest, net] +import std/net import pkg/chronos -import pkg/libp2p import pkg/libp2p/[multiaddress, multihash, multicodec] -import pkg/libp2p/protocols/connectivity/autonatv2/service +import pkg/libp2p/protocols/connectivity/autonatv2/service except setup +import pkg/libp2p/protocols/connectivity/autonatv2/types +import pkg/libp2p/protocols/connectivity/relay/client as relayClientModule +import pkg/libp2p/services/autorelayservice except setup + import pkg/results +import ./helpers +import ../asynctest import ../../storage/nat import ../../storage/discovery import ../../storage/rng @@ -15,7 +20,12 @@ type MockNatMapper = ref object of NatMapper mapped: tuple[libp2p, discovery: seq[MultiAddress]] method mapNatAddresses*( - m: MockNatMapper, addrs: seq[MultiAddress], discoveryPort: Port + m: MockNatMapper, addrs: seq[MultiAddress] +): tuple[libp2p, discovery: seq[MultiAddress]] {.raises: [].} = + m.mapped + +method getReachableAddresses*( + m: MockNatMapper, addrs: seq[MultiAddress] ): tuple[libp2p, discovery: seq[MultiAddress]] {.raises: [].} = m.mapped @@ -44,7 +54,6 @@ suite "NAT Address Tests": MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"), MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"), ] - #ipv6Addr = MultiAddress.init("/ip6/::1/tcp/5000").expect("valid multiaddr") addrs = @[localAddr, anyAddr, publicAddr] @@ -78,17 +87,94 @@ suite "setupAddress": check tcpPort == some(Port(5000)) check udpPort == some(Port(5001)) -suite "handleNatStatus": - let key = PrivateKey.random(Rng.instance[]).get() +suite "getReachableAddresses": + test "returns remapped addresses when extIp is configured": + let + natConfig = NatConfig(hasExtIp: true, extIp: parseIpAddress("1.2.3.4")) + mapper = DefaultNatMapper(natConfig: natConfig, discoveryPort: Port(8090)) + listenAddr = MultiAddress.init("/ip4/0.0.0.0/tcp/5000").expect("valid") - test "NotReachable updates announce addresses": - let disc = Discovery.new(key, announceAddrs = @[]) + let (libp2pAddrs, discAddrs) = mapper.getReachableAddresses(@[listenAddr]) + + check libp2pAddrs == @[MultiAddress.init("/ip4/1.2.3.4/tcp/5000").expect("valid")] + check discAddrs == @[MultiAddress.init("/ip4/1.2.3.4/udp/8090").expect("valid")] + +suite "hasPublicIp": + test "hasPublicIp returns true when the address is public": + let ma = MultiAddress.init("/ip4/8.8.8.8/tcp/8080").expect("valid") + check hasPublicIp(@[ma]) + + test "hasPublicIp returns false when the address is private": + let ma = MultiAddress.init("/ip4/192.168.1.1/tcp/8080").expect("valid") + check not hasPublicIp(@[ma]) + + test "hasPublicIp returns false when the address is empty": + check not hasPublicIp(@[]) + +asyncchecksuite "handleNatStatus": + var sw: Switch + var key: PrivateKey + var disc: Discovery + let autoRelay = + AutoRelayService.new(1, relayClientModule.RelayClient.new(), nil, Rng.instance()) + + setup: + key = PrivateKey.random(Rng.instance[]).get() + disc = Discovery.new(key, announceAddrs = @[]) + sw = newStandardSwitch() + await sw.start() + + teardown: + await sw.stop() + + if autoRelay.isRunning: + discard await autoRelay.stop(sw) + + test "handleNatStatus announces address when the node is not Reachable and the UPnP succeed with public ip": let announceAddr = MultiAddress.init("/ip4/1.2.3.4/tcp/8080").expect("valid") let discAddr = MultiAddress.init("/ip4/1.2.3.4/udp/8090").expect("valid") let mapper = MockNatMapper(mapped: (@[announceAddr], @[discAddr])) - waitFor handleNatStatus( - NotReachable, Opt.none(float), mapper, @[], Port(8090), disc - ) + await handleNatStatus(NotReachable, mapper, disc, sw, autoRelay) check disc.announceAddrs == @[announceAddr] + check not autoRelay.isRunning + + # test "handleNatStatus does not announce address when the node is not Reachable and the UPnP succeed with private ip": + # let privateAddr = MultiAddress.init("/ip4/192.168.1.1/tcp/8080").expect("valid") + # let mapper = MockNatMapper(mapped: (@[privateAddr], @[])) + + # await handleNatStatus( + # NotReachable, mapper, disc, sw, autoRelay + # ) + + # check disc.announceAddrs == @[] + # check not autoRelay.isRunning + + test "handleNatStatus starts autoRelay when node is not Reachable and UPnP failed": + let mapper = MockNatMapper(mapped: (@[], @[])) + + await handleNatStatus(NotReachable, mapper, disc, sw, autoRelay) + + check autoRelay.isRunning + # The addresses will be announced in the onReservation callback + # after a node accepted a Relay reservation. + + test "handleNatStatus does not announce address when node is Reachable and relay is not running": + let mapper = MockNatMapper(mapped: (@[], @[])) + + await handleNatStatus(Reachable, mapper, disc, sw, autoRelay) + + check disc.announceAddrs == newSeq[MultiAddress]() + check not autoRelay.isRunning + + test "handleNatStatus stops relay and announces address when node is Reachable and relay is running": + let announceAddr = MultiAddress.init("/ip4/1.2.3.4/tcp/8080").expect("valid") + let discAddr = MultiAddress.init("/ip4/1.2.3.4/udp/8090").expect("valid") + let mapper = MockNatMapper(mapped: (@[announceAddr], @[discAddr])) + + discard await autorelayservice.setup(autoRelay, sw) + await handleNatStatus(Reachable, mapper, disc, sw, autoRelay) + + check not autoRelay.isRunning + check disc.announceAddrs == @[announceAddr]