diff --git a/apps/wakunode2/app.nim b/apps/wakunode2/app.nim index f103acb17..6d8a84799 100644 --- a/apps/wakunode2/app.nim +++ b/apps/wakunode2/app.nim @@ -8,6 +8,8 @@ import stew/results, chronicles, chronos, + libp2p/wire, + libp2p/multicodec, libp2p/crypto/crypto, libp2p/nameresolving/dnsresolver, libp2p/protocols/pubsub/gossipsub, @@ -117,39 +119,8 @@ proc init*(T: type App, rng: ref HmacDrbgContext, conf: WakuNodeConf): T = quit(QuitFailure) else: netConfigRes.get() - var enrBuilder = EnrBuilder.init(key) + let recordRes = enrConfiguration(conf, netConfig, key) - enrBuilder.withIpAddressAndPorts( - netConfig.enrIp, - netConfig.enrPort, - netConfig.discv5UdpPort - ) - - if netConfig.wakuFlags.isSome(): - enrBuilder.withWakuCapabilities(netConfig.wakuFlags.get()) - - enrBuilder.withMultiaddrs(netConfig.enrMultiaddrs) - - let topics = - if conf.pubsubTopics.len > 0 or conf.contentTopics.len > 0: - let shardsRes = conf.contentTopics.mapIt(getShard(it)) - for res in shardsRes: - if res.isErr(): - error "failed to shard content topic", error=res.error - quit(QuitFailure) - - let shards = shardsRes.mapIt(it.get()) - - conf.pubsubTopics & shards - else: - conf.topics - - let addShardedTopics = enrBuilder.withShardedTopics(topics) - if addShardedTopics.isErr(): - error "failed to add sharded topics to ENR", error=addShardedTopics.error - quit(QuitFailure) - - let recordRes = enrBuilder.build() let record = if recordRes.isErr(): error "failed to create record", error=recordRes.error @@ -329,6 +300,72 @@ proc setupWakuApp*(app: var App): AppResult[void] = ok() +proc getPorts(listenAddrs: seq[MultiAddress]): + AppResult[tuple[tcpPort, websocketPort: Option[Port]]] = + + var tcpPort, websocketPort = none(Port) + + for a in listenAddrs: + if a.isWsAddress(): + if websocketPort.isNone(): + let wsAddress = initTAddress(a).valueOr: + return err("getPorts wsAddr error:" & $error) + websocketPort = some(wsAddress.port) + elif tcpPort.isNone(): + let tcpAddress = initTAddress(a).valueOr: + return err("getPorts tcpAddr error:" & $error) + tcpPort = some(tcpAddress.port) + + return ok((tcpPort: tcpPort, websocketPort: websocketPort)) + +proc updateNetConfig(app: var App): AppResult[void] = + + var conf = app.conf + let (tcpPort, websocketPort) = getPorts(app.node.switch.peerInfo.listenAddrs).valueOr: + return err("Could not retrieve ports " & error) + + if tcpPort.isSome(): + conf.tcpPort = tcpPort.get() + + if websocketPort.isSome(): + conf.websocketPort = websocketPort.get() + + # Rebuild NetConfig with bound port values + let netConf = networkConfiguration(conf, clientId).valueOr: + return err("Could not update NetConfig: " & error) + + app.netConf = netConf + + return ok() + +proc updateEnr(app: var App): AppResult[void] = + + let record = enrConfiguration(app.conf, app.netConf, app.key).valueOr: + return err(error) + + app.record = record + app.node.enr = record + + if app.conf.discv5Discovery: + app.wakuDiscV5 = some(app.setupDiscoveryV5()) + + return ok() + +proc updateApp(app: var App): AppResult[void] = + + if app.conf.tcpPort == Port(0) or app.conf.websocketPort == Port(0): + + updateNetConfig(app).isOkOr: + return err("error calling updateNetConfig: " & $error) + + updateEnr(app).isOkOr: + return err("error calling updateEnr: " & $error) + + app.node.announcedAddresses = app.netConf.announcedAddresses + + printNodeNetworkInfo(app.node) + + return ok() ## Mount protocols @@ -548,7 +585,18 @@ proc startNode(node: WakuNode, conf: WakuNodeConf, return ok() -proc startApp*(app: App): Future[AppResult[void]] {.async.} = +proc startApp*(app: var App): AppResult[void] = + + try: + (waitFor startNode(app.node,app.conf,app.dynamicBootstrapNodes)).isOkOr: + return err(error) + except CatchableError: + return err("exception starting node: " & getCurrentExceptionMsg()) + + # Update app data that is set dynamically on node start + app.updateApp().isOkOr: + return err("Error in updateApp: " & $error) + if app.wakuDiscv5.isSome(): let wakuDiscv5 = app.wakuDiscv5.get() @@ -559,11 +607,8 @@ proc startApp*(app: App): Future[AppResult[void]] {.async.} = asyncSpawn wakuDiscv5.searchLoop(app.node.peerManager) asyncSpawn wakuDiscv5.subscriptionsListener(app.node.topicSubscriptionQueue) - return await startNode( - app.node, - app.conf, - app.dynamicBootstrapNodes - ) + return ok() + ## Monitoring and external interfaces diff --git a/apps/wakunode2/internal_config.nim b/apps/wakunode2/internal_config.nim index 0eec05bb4..39826fdfe 100644 --- a/apps/wakunode2/internal_config.nim +++ b/apps/wakunode2/internal_config.nim @@ -4,15 +4,61 @@ import libp2p/crypto/crypto, libp2p/multiaddress, libp2p/nameresolving/dnsresolver, - std/options, + std/[options, sequtils], stew/results, stew/shims/net import ../../waku/common/utils/nat, ../../waku/node/config, ../../waku/waku_enr/capabilities, + ../../waku/waku_enr, + ../../waku/waku_core, ./external_config +proc enrConfiguration*(conf: WakuNodeConf, netConfig: NetConfig, key: crypto.PrivateKey): + Result[enr.Record, string] = + + var enrBuilder = EnrBuilder.init(key) + + enrBuilder.withIpAddressAndPorts( + netConfig.enrIp, + netConfig.enrPort, + netConfig.discv5UdpPort + ) + + if netConfig.wakuFlags.isSome(): + enrBuilder.withWakuCapabilities(netConfig.wakuFlags.get()) + + enrBuilder.withMultiaddrs(netConfig.enrMultiaddrs) + + let topics = + if conf.pubsubTopics.len > 0 or conf.contentTopics.len > 0: + let shardsRes = conf.contentTopics.mapIt(getShard(it)) + for res in shardsRes: + if res.isErr(): + error "failed to shard content topic", error=res.error + return err($res.error) + + let shards = shardsRes.mapIt(it.get()) + + conf.pubsubTopics & shards + else: + conf.topics + + let addShardedTopics = enrBuilder.withShardedTopics(topics) + if addShardedTopics.isErr(): + error "failed to add sharded topics to ENR", error=addShardedTopics.error + return err($addShardedTopics.error) + + let recordRes = enrBuilder.build() + let record = + if recordRes.isErr(): + error "failed to create record", error=recordRes.error + return err($recordRes.error) + else: recordRes.get() + + return ok(record) + proc validateExtMultiAddrs*(vals: seq[string]): Result[seq[MultiAddress], string] = var multiaddrs: seq[MultiAddress] diff --git a/apps/wakunode2/wakunode2.nim b/apps/wakunode2/wakunode2.nim index 30a281f33..cf05b5d67 100644 --- a/apps/wakunode2/wakunode2.nim +++ b/apps/wakunode2/wakunode2.nim @@ -87,7 +87,7 @@ when isMainModule: debug "5/7 Starting node and mounted protocols" - let res6 = waitFor wakunode2.startApp() + let res6 = wakunode2.startApp() if res6.isErr(): error "5/7 Starting node and protocols failed", error=res6.error quit(QuitFailure) diff --git a/tests/wakunode2/test_app.nim b/tests/wakunode2/test_app.nim index fe2c735c1..f87d264f8 100644 --- a/tests/wakunode2/test_app.nim +++ b/tests/wakunode2/test_app.nim @@ -12,7 +12,9 @@ import import ../testlib/common, ../testlib/wakucore, - ../testlib/wakunode, + ../testlib/wakunode + +include ../../apps/wakunode2/app suite "Wakunode2 - App": @@ -27,7 +29,7 @@ suite "Wakunode2 - App": ## Then check: - version == app.git_version + version == git_version suite "Wakunode2 - App initialization": test "peer persistence setup should be successfully mounted": @@ -53,7 +55,7 @@ suite "Wakunode2 - App initialization": require wakunode2.setupDyamicBootstrapNodes().isOk() require wakunode2.setupWakuApp().isOk() require isOk(waitFor wakunode2.setupAndMountProtocols()) - require isOk(waitFor wakunode2.startApp()) + require isOk(wakunode2.startApp()) require wakunode2.setupMonitoringAndExternalInterfaces().isOk() ## Then @@ -67,3 +69,43 @@ suite "Wakunode2 - App initialization": ## Cleanup waitFor wakunode2.stop() + + test "app properly handles dynamic port configuration": + ## Given + var conf = defaultTestWakuNodeConf() + conf.tcpPort = Port(0) + + ## When + var wakunode2 = App.init(rng(), conf) + require wakunode2.setupPeerPersistence().isOk() + require wakunode2.setupDyamicBootstrapNodes().isOk() + require wakunode2.setupWakuApp().isOk() + require isOk(waitFor wakunode2.setupAndMountProtocols()) + require isOk(wakunode2.startApp()) + require wakunode2.setupMonitoringAndExternalInterfaces().isOk() + + ## Then + let + node = wakunode2.node + typedNodeEnr = node.enr.toTypedRecord() + typedAppEnr = wakunode2.record.toTypedRecord() + + assert typedNodeEnr.isOk(), $typedNodeEnr.error + assert typedAppEnr.isOk(), $typedAppEnr.error + + check: + # App started properly + not node.isNil() + node.wakuArchive.isNil() + node.wakuStore.isNil() + not node.wakuStoreClient.isNil() + not node.rendezvous.isNil() + + # DS structures are updated with dynamic ports + wakunode2.netConf.bindPort != Port(0) + wakunode2.netConf.enrPort.get() != Port(0) + typedNodeEnr.get().tcp.get() != 0 + typedAppEnr.get().tcp.get() != 0 + + ## Cleanup + waitFor wakunode2.stop() diff --git a/waku/node/config.nim b/waku/node/config.nim index 46af0e326..f53d67134 100644 --- a/waku/node/config.nim +++ b/waku/node/config.nim @@ -59,7 +59,7 @@ proc formatListenAddress(inputMultiAdd: MultiAddress): MultiAddress = # If MultiAddress contains "0.0.0.0", replace it for "127.0.0.1" return MultiAddress.init(inputStr.replace("0.0.0.0", "127.0.0.1")).get() -proc isWsAddress(ma: MultiAddress): bool = +proc isWsAddress*(ma: MultiAddress): bool = let isWs = ma.contains(multiCodec("ws")).get() isWss = ma.contains(multiCodec("wss")).get() diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 83207024e..fc512b178 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -1073,6 +1073,26 @@ proc mountRendezvous*(node: WakuNode) {.async, raises: [Defect, LPError].} = node.switch.mount(node.rendezvous) +proc isBindIpWithZeroPort(inputMultiAdd: MultiAddress): bool = + let inputStr = $inputMultiAdd + if inputStr.contains("0.0.0.0/tcp/0") or inputStr.contains("127.0.0.1/tcp/0"): + return true + + return false + +proc printNodeNetworkInfo*(node: WakuNode): void = + let peerInfo = node.switch.peerInfo + var listenStr = "" + + info "PeerInfo", peerId = peerInfo.peerId, addrs = peerInfo.addrs + + for address in node.announcedAddresses: + var fulladdr = "[" & $address & "/p2p/" & $peerInfo.peerId & "]" + listenStr &= fulladdr + + ## XXX: this should be /ip4..., / stripped? + info "Listening on", full = listenStr + info "DNS: discoverable ENR ", enr = node.enr.toUri() proc start*(node: WakuNode) {.async.} = ## Starts a created Waku Node and @@ -1081,16 +1101,15 @@ proc start*(node: WakuNode) {.async.} = waku_version.set(1, labelValues=[git_version]) info "Starting Waku node", version=git_version - let peerInfo = node.switch.peerInfo - info "PeerInfo", peerId = peerInfo.peerId, addrs = peerInfo.addrs - var listenStr = "" + var zeroPortPresent = false for address in node.announcedAddresses: - var fulladdr = "[" & $address & "/p2p/" & $peerInfo.peerId & "]" - listenStr &= fulladdr + if isBindIpWithZeroPort(address): + zeroPortPresent = true - ## XXX: this should be /ip4..., / stripped? - info "Listening on", full = listenStr - info "DNS: discoverable ENR ", enr = node.enr.toUri() + if not zeroPortPresent: + printNodeNetworkInfo(node) + else: + info "Listening port is dynamically allocated, address and ENR generation postponed" # Perform relay-specific startup tasks TODO: this should be rethought if not node.wakuRelay.isNil():