From 5603f766d1774267d5be81ee1acaa0b28f23cba7 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Thu, 23 Apr 2026 00:13:54 -0300 Subject: [PATCH] feat: service ports default to 0 (auto-assign) * tcp/rest/websocket/metrics/discv5 default port changed to 0 * all CLI port types are now uint16 (instead of Port) * any port set to 0 on conf results in a random port bound * write back bound values to both WakuConf and WakuNode.ports * REST GET /info reports actually bound ports for enabled services * conf builders now err if any port config is unset * setupDiscoveryV5 returns Result and errors out on port 0 * rename setupAndStartDiscv5WithAutoPort to setupAndStartDiscv5 * updateWaku ENR rebuild now runs after discv5 startup * remove Port(0) conf from tests (0 is default) * add port = 0 to conf builder tests (conf builder has no default) --- tests/api/test_api_health.nim | 4 - tests/api/test_api_receive.nim | 2 - tests/api/test_api_send.nim | 2 - tests/api/test_api_subscription.nim | 2 - tests/api/test_node_conf.nim | 2 + tests/factory/test_node_factory.nim | 155 +++++++++++++++++- tests/factory/test_waku_conf.nim | 80 ++++++++- tests/waku_discv5/test_waku_discv5.nim | 6 +- tests/wakunode_rest/test_rest_debug.nim | 32 ++++ tools/confutils/cli_args.nim | 15 +- waku/common/auto_port.nim | 13 ++ waku/discovery/waku_discv5.nim | 56 ++++++- .../conf_builder/discv5_conf_builder.nim | 9 +- .../metrics_server_conf_builder.nim | 5 +- .../conf_builder/waku_conf_builder.nim | 21 ++- waku/factory/waku.nim | 77 +++++---- waku/node/waku_metrics.nim | 52 ++++-- waku/node/waku_node.nim | 15 +- waku/rest_api/endpoint/debug/types.nim | 45 ++++- 19 files changed, 498 insertions(+), 95 deletions(-) create mode 100644 waku/common/auto_port.nim diff --git a/tests/api/test_api_health.nim b/tests/api/test_api_health.nim index f3dd340af..320e0e119 100644 --- a/tests/api/test_api_health.nim +++ b/tests/api/test_api_health.nim @@ -94,8 +94,6 @@ suite "LM API health checking": raiseAssert error conf.mode = Core conf.listenAddress = parseIpAddress("0.0.0.0") - conf.tcpPort = Port(0) - conf.discv5UdpPort = Port(0) conf.clusterId = 3'u16 conf.numShardsInNetwork = 1 conf.rest = false @@ -271,8 +269,6 @@ suite "LM API health checking": raiseAssert error edgeConf.mode = Edge edgeConf.listenAddress = parseIpAddress("0.0.0.0") - edgeConf.tcpPort = Port(0) - edgeConf.discv5UdpPort = Port(0) edgeConf.clusterId = 3'u16 edgeConf.maxMessageSize = "150 KiB" edgeConf.rest = false diff --git a/tests/api/test_api_receive.nim b/tests/api/test_api_receive.nim index 52f8713f9..1f61fd0a3 100644 --- a/tests/api/test_api_receive.nim +++ b/tests/api/test_api_receive.nim @@ -65,8 +65,6 @@ proc createApiNodeConf(numShards: uint16 = 1): WakuNodeConf = raiseAssert error conf.mode = cli_args.WakuMode.Core conf.listenAddress = parseIpAddress("0.0.0.0") - conf.tcpPort = Port(0) - conf.discv5UdpPort = Port(0) conf.clusterId = 3'u16 conf.numShardsInNetwork = numShards conf.reliabilityEnabled = true diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim index 28f0ca2ff..bf9d7013e 100644 --- a/tests/api/test_api_send.nim +++ b/tests/api/test_api_send.nim @@ -122,8 +122,6 @@ proc createApiNodeConf(mode: cli_args.WakuMode = cli_args.WakuMode.Core): WakuNo raiseAssert error conf.mode = mode conf.listenAddress = parseIpAddress("0.0.0.0") - conf.tcpPort = Port(0) - conf.discv5UdpPort = Port(0) conf.clusterId = 3'u16 conf.numShardsInNetwork = 1 conf.reliabilityEnabled = true diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim index e0ceb9226..189054bae 100644 --- a/tests/api/test_api_subscription.nim +++ b/tests/api/test_api_subscription.nim @@ -73,8 +73,6 @@ proc createApiNodeConf( raiseAssert error conf.mode = mode conf.listenAddress = parseIpAddress("0.0.0.0") - conf.tcpPort = Port(0) - conf.discv5UdpPort = Port(0) conf.clusterId = 3'u16 conf.numShardsInNetwork = numShards conf.reliabilityEnabled = true diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index b19739393..e171c5207 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -376,6 +376,7 @@ suite "WakuConfBuilder - store retention policies": test "Multiple retention policies": ## Given var b = WakuConfBuilder.init() + b.withP2pTcpPort(0'u16) b.storeServiceConf.withEnabled(true) b.storeServiceConf.withDbUrl("sqlite://test.db") b.storeServiceConf.withRetentionPolicies( @@ -420,6 +421,7 @@ suite "WakuConfBuilder - store retention policies": test "Store disabled - no retention policy applied": ## Given var b = WakuConfBuilder.init() + b.withP2pTcpPort(0'u16) # storeServiceConf not enabled ## When diff --git a/tests/factory/test_node_factory.nim b/tests/factory/test_node_factory.nim index f30e079b5..7758e16dd 100644 --- a/tests/factory/test_node_factory.nim +++ b/tests/factory/test_node_factory.nim @@ -1,13 +1,18 @@ {.used.} -import testutils/unittests, chronos, libp2p/protocols/connectivity/relay/relay +import + std/[net, options, strutils], + testutils/unittests, + chronos, + chronos/transports/[stream, datagram, common], + metrics/chronos_httpserver, + libp2p/[crypto/crypto, multiaddress, protocols/connectivity/relay/relay] import - ../testlib/wakunode, - waku/waku_node, - waku/factory/node_factory, - waku/factory/conf_builder/conf_builder, - waku/factory/conf_builder/web_socket_conf_builder + tests/testlib/[wakunode, wakucore], + waku/[waku_node, common/auto_port, discovery/waku_discv5, node/waku_metrics], + waku/factory/ + [node_factory, conf_builder/conf_builder, conf_builder/web_socket_conf_builder] suite "Node Factory": asynctest "Set up a node based on default configurations": @@ -68,5 +73,143 @@ asynctest "Start a node based on default test configuration": check: node.started == true + # Default conf has p2pTcpPort=0, so the OS must have assigned a real port. + var hasNonZeroTcp = false + for a in node.switch.peerInfo.listenAddrs: + let s = $a + if ("/tcp/" in s) and not ("/tcp/0" in s): + hasNonZeroTcp = true + check hasNonZeroTcp + ## Cleanup await node.stop() + +suite "Auto-port retry": + asynctest "metrics binds on free TCP port, fails on taken": + let takenPort = Port(55100) + let freePort = Port(55101) + let taken = createStreamServer(initTAddress("127.0.0.1", takenPort)) + + proc buildMetricsConf(port: Port): MetricsServerConf = + var b = MetricsServerConfBuilder.init() + b.withEnabled(true) + b.withHttpPort(port) + b.build().value.get() + + try: + let failRes = await startMetricsServerAndLogging(buildMetricsConf(takenPort), 0'u16) + check failRes.isErr + + let okRes = await startMetricsServerAndLogging(buildMetricsConf(freePort), 0'u16) + check okRes.isOk + if okRes.isOk: + await okRes.get().server.close() + finally: + taken.stop() + await taken.closeWait() + + asynctest "discv5 binds on free UDP port, fails on taken": + let takenPort = Port(55200) + let freePort = Port(55201) + + proc dummyCb( + transp: DatagramTransport, raddr: TransportAddress + ): Future[void] {.async: (raises: []).} = + discard + + let takenUdp = + newDatagramTransport(dummyCb, local = initTAddress("0.0.0.0", takenPort)) + + let nodeKey = generateSecp256k1Key() + let node = newTestWakuNode(nodeKey, parseIpAddress("0.0.0.0"), Port(0)) + await node.start() + + proc buildDiscv5Conf(port: Port): Discv5Conf = + var b = Discv5ConfBuilder.init() + b.withEnabled(true) + b.withUdpPort(port) + b.build().value.get() + + try: + let failRes = await setupAndStartDiscv5( + node.enr, + node.peerManager, + node.topicSubscriptionQueue, + buildDiscv5Conf(takenPort), + @[], + node.rng, + nodeKey, + parseIpAddress("0.0.0.0"), + 0'u16, + ) + check failRes.isErr + + let okRes = await setupAndStartDiscv5( + node.enr, + node.peerManager, + node.topicSubscriptionQueue, + buildDiscv5Conf(freePort), + @[], + node.rng, + nodeKey, + parseIpAddress("0.0.0.0"), + 0'u16, + ) + check okRes.isOk + if okRes.isOk: + await okRes.get().stop() + finally: + await takenUdp.closeWait() + await node.stop() + + asynctest "exhausted retries err for metrics and discv5": + let origMin = autoPortMin + let origMax = autoPortMax + let pinned = 58888'u16 + autoPortMin = pinned + autoPortMax = pinned + + let takenTcp = createStreamServer(initTAddress("127.0.0.1", Port(pinned))) + + proc dummyCb( + transp: DatagramTransport, raddr: TransportAddress + ): Future[void] {.async: (raises: []).} = + discard + + let takenUdp = + newDatagramTransport(dummyCb, local = initTAddress("0.0.0.0", Port(pinned))) + + let nodeKey = generateSecp256k1Key() + let node = newTestWakuNode(nodeKey, parseIpAddress("0.0.0.0"), Port(0)) + await node.start() + + try: + var mb = MetricsServerConfBuilder.init() + mb.withEnabled(true) + mb.withHttpPort(0'u16) + let metricsRes = + await startMetricsServerAndLogging(mb.build().value.get(), 0'u16) + check metricsRes.isErr + + var db = Discv5ConfBuilder.init() + db.withEnabled(true) + db.withUdpPort(0'u16) + let discv5Res = await setupAndStartDiscv5( + node.enr, + node.peerManager, + node.topicSubscriptionQueue, + db.build().value.get(), + @[], + node.rng, + nodeKey, + parseIpAddress("0.0.0.0"), + 0'u16, + ) + check discv5Res.isErr + finally: + takenTcp.stop() + await takenTcp.closeWait() + await takenUdp.closeWait() + await node.stop() + autoPortMin = origMin + autoPortMax = origMax diff --git a/tests/factory/test_waku_conf.nim b/tests/factory/test_waku_conf.nim index eeacf791b..d7505982a 100644 --- a/tests/factory/test_waku_conf.nim +++ b/tests/factory/test_waku_conf.nim @@ -4,7 +4,7 @@ import libp2p/crypto/[crypto, secp], libp2p/multiaddress, nimcrypto/utils, - std/[options, random, sequtils], + std/[net, options, random, sequtils], results, testutils/unittests import @@ -18,6 +18,7 @@ suite "Waku Conf - build with cluster conf": ## Setup let networkConf = NetworkConf.TheWakuNetworkConf() var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) builder.discv5Conf.withUdpPort(9000) builder.withRelayServiceRatio("50:50") # Mount all shards in network @@ -62,6 +63,7 @@ suite "Waku Conf - build with cluster conf": ## Setup let networkConf = NetworkConf.TheWakuNetworkConf() var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) builder.withRelayServiceRatio("50:50") builder.discv5Conf.withUdpPort(9000) # Mount all shards in network @@ -95,6 +97,8 @@ suite "Waku Conf - build with cluster conf": ## Setup let networkConf = NetworkConf.TheWakuNetworkConf() var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) + builder.discv5Conf.withUdpPort(0'u16) let # Mount all shards in network expectedShards = toSeq[0.uint16 .. 7.uint16] @@ -126,6 +130,8 @@ suite "Waku Conf - build with cluster conf": ## Setup let networkConf = NetworkConf.TheWakuNetworkConf() var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) + builder.discv5Conf.withUdpPort(0'u16) let shards = @[2.uint16, 3.uint16] ## Given @@ -171,6 +177,8 @@ suite "Waku Conf - build with cluster conf": ## Setup let networkConf = NetworkConf.TheWakuNetworkConf() var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) + builder.discv5Conf.withUdpPort(0'u16) builder.rlnRelayConf.withEthClientUrls(@["https://my_eth_rpc_url/"]) # Mount all shards in network @@ -217,6 +225,8 @@ suite "Waku Conf - build with cluster conf": ## Setup let networkConf = NetworkConf.LogosDevConf() var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) + builder.discv5Conf.withUdpPort(0'u16) # Sanity check check networkConf.shardingConf.kind == AutoSharding @@ -246,6 +256,8 @@ suite "Waku Conf - build with cluster conf": ## Given: emulate --preset=logos.dev --num-shards-in-network=0 let networkConf = NetworkConf.LogosDevConf() var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) + builder.discv5Conf.withUdpPort(0'u16) builder.withNetworkConf(networkConf) # Note: builder.withNumShardsInCluster() is not called when the # value that comes from the CLI path is 0 (which means it was @@ -265,6 +277,7 @@ suite "Waku Conf - node key": test "Node key is generated": ## Setup var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) builder.withClusterId(1) ## Given @@ -288,6 +301,7 @@ suite "Waku Conf - node key": let key = SkPrivateKey.init(utils.fromHex(nodeKeyStr)).tryGet() crypto.PrivateKey(scheme: Secp256k1, skkey: key) var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) builder.withClusterId(1) ## Given @@ -309,6 +323,7 @@ suite "Waku Conf - extMultiaddrs": test "Valid multiaddresses are passed and accepted": ## Setup var builder = WakuConfBuilder.init() + builder.withP2pTcpPort(0'u16) builder.withClusterId(1) ## Given @@ -346,3 +361,66 @@ suite "Waku Conf Builder - rate limits": ## Then assert res.isOk(), $res.error + +suite "Waku Conf - port required": + test "p2pTcpPort not specified returns err": + ## Setup: minimal builder with no withP2pTcpPort call + var builder = WakuConfBuilder.init() + + ## When + let res = builder.build() + + ## Then + check res.isErr() + check res.error == "p2pTcpPort is not specified" + + test "discv5 enabled without udpPort returns err": + ## Setup + var builder = WakuConfBuilder.init() + builder.discv5Conf.withEnabled(true) + + ## When + let res = builder.build() + + ## Then + check res.isErr() + check res.error == "Discv5 Conf building failed: discv5.udpPort is not specified" + + test "metricsServer enabled without httpPort returns err": + ## Setup + var builder = WakuConfBuilder.init() + builder.metricsServerConf.withEnabled(true) + + ## When + let res = builder.build() + + ## Then + check res.isErr() + check res.error == + "Metrics Server Conf building failed: metricsServer.httpPort is not specified" + + test "restServer enabled without port returns err": + ## Setup: listenAddress must be set (checked before port) + var builder = WakuConfBuilder.init() + builder.restServerConf.withEnabled(true) + builder.restServerConf.withListenAddress(parseIpAddress("127.0.0.1")) + + ## When + let res = builder.build() + + ## Then + check res.isErr() + check res.error == + "REST Server Conf building failed: restServer.port is not specified" + + test "webSocket enabled without port returns err": + ## Setup + var builder = WakuConfBuilder.init() + builder.webSocketConf.withEnabled(true) + + ## When + let res = builder.build() + + ## Then + check res.isErr() + check res.error == "WebSocket Conf building failed: websocket.port is not specified" diff --git a/tests/waku_discv5/test_waku_discv5.nim b/tests/waku_discv5/test_waku_discv5.nim index 20a0c6965..761959a9e 100644 --- a/tests/waku_discv5/test_waku_discv5.nim +++ b/tests/waku_discv5/test_waku_discv5.nim @@ -506,7 +506,8 @@ suite "Waku Discovery v5": waku.conf.nodeKey, waku.conf.endpointConf.p2pListenAddress, waku.conf.portsShift, - ) + ).valueOr: + raiseAssert error check: waku.node.peerManager.switch.peerStore.peers().anyIt( @@ -537,7 +538,8 @@ suite "Waku Discovery v5": waku.conf.nodeKey, waku.conf.endpointConf.p2pListenAddress, waku.conf.portsShift, - ) + ).valueOr: + raiseAssert error check: not waku.node.peerManager.switch.peerStore.peers().anyIt( diff --git a/tests/wakunode_rest/test_rest_debug.nim b/tests/wakunode_rest/test_rest_debug.nim index 4bd2e8c02..d1ad1df83 100644 --- a/tests/wakunode_rest/test_rest_debug.nim +++ b/tests/wakunode_rest/test_rest_debug.nim @@ -1,6 +1,7 @@ {.used.} import + std/options, testutils/unittests, presto, presto/client as presto_client, @@ -62,6 +63,37 @@ suite "Waku v2 REST API - Debug": await restServer.closeWait() await node.stop() + asyncTest "GET /info exposes node.ports": + let node = testWakuNode() + node.ports = BoundPorts( + tcp: some(1001'u16), + webSocket: some(1002'u16), + rest: some(1003'u16), + discv5Udp: some(1004'u16), + metrics: some(1005'u16), + ) + + let restAddress = parseIpAddress("0.0.0.0") + let restServer = WakuRestServerRef.init(restAddress, Port(0)).tryGet() + installDebugApiHandlers(restServer.router, node) + restServer.start() + + let client = newRestHttpClient( + initTAddress(restAddress, restServer.httpServer.address.port) + ) + let response = await client.debugInfoV1() + + check: + response.status == 200 + response.data.ports.tcp == some(1001'u16) + response.data.ports.webSocket == some(1002'u16) + response.data.ports.rest == some(1003'u16) + response.data.ports.discv5Udp == some(1004'u16) + response.data.ports.metrics == some(1005'u16) + + await restServer.stop() + await restServer.closeWait() + asyncTest "Get node version - GET /version": # Given let node = testWakuNode() diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index d63b5880c..8b6116081 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -192,8 +192,7 @@ type WakuNodeConf* = object name: "listen-address" .}: IpAddress - tcpPort* {.desc: "TCP listening port.", defaultValue: 60000, name: "tcp-port".}: - Port + tcpPort* {.desc: "TCP listening port.", defaultValue: 0, name: "tcp-port".}: uint16 portsShift* {. desc: "Add a shift to all port numbers.", defaultValue: 0, name: "ports-shift" @@ -490,7 +489,7 @@ with the drawback of consuming some more bandwidth.""", restPort* {. desc: "Listening port of the REST HTTP server.", - defaultValue: 8645, + defaultValue: 0, name: "rest-port" .}: uint16 @@ -531,7 +530,7 @@ with the drawback of consuming some more bandwidth.""", metricsServerPort* {. desc: "Listening HTTP port of the metrics server.", - defaultValue: 8008, + defaultValue: 0, name: "metrics-server-port" .}: uint16 @@ -565,9 +564,9 @@ with the drawback of consuming some more bandwidth.""", discv5UdpPort* {. desc: "Listening UDP port for Node Discovery v5.", - defaultValue: 9000, + defaultValue: 0, name: "discv5-udp-port" - .}: Port + .}: uint16 discv5BootstrapNodes* {. desc: @@ -664,8 +663,8 @@ with the drawback of consuming some more bandwidth.""", .}: bool websocketPort* {. - desc: "WebSocket listening port.", defaultValue: 8000, name: "websocket-port" - .}: Port + desc: "WebSocket listening port.", defaultValue: 0, name: "websocket-port" + .}: uint16 websocketSecureSupport* {. desc: "Enable secure websocket: true|false", diff --git a/waku/common/auto_port.nim b/waku/common/auto_port.nim new file mode 100644 index 000000000..bb14f1c58 --- /dev/null +++ b/waku/common/auto_port.nim @@ -0,0 +1,13 @@ +{.push raises: [].} + +import std/random + +const AutoPortRetryCount* = 20 + +var + autoPortMin* = 50000'u16 + autoPortMax* = 59000'u16 + rng = initRand() + +proc getAutoPort*(): uint16 = + uint16(rng.rand(autoPortMin.int .. autoPortMax.int)) diff --git a/waku/discovery/waku_discv5.nim b/waku/discovery/waku_discv5.nim index 0eb329fa4..e8ccf99ef 100644 --- a/waku/discovery/waku_discv5.nim +++ b/waku/discovery/waku_discv5.nim @@ -10,7 +10,7 @@ import eth/keys as eth_keys, eth/p2p/discoveryv5/node, eth/p2p/discoveryv5/protocol -import ../node/peer_manager/peer_manager, ../waku_core, ../waku_enr +import waku/[common/auto_port, node/peer_manager/peer_manager, waku_core, waku_enr] export protocol, waku_enr @@ -409,7 +409,13 @@ proc setupDiscoveryV5*( key: crypto.PrivateKey, p2pListenAddress: IpAddress, portsShift: uint16, -): WakuDiscoveryV5 = +): Result[WakuDiscoveryV5, string] = + if conf.udpPort == Port(0): + return err( + "setupDiscoveryV5: udpPort must be non-zero; " & + "use setupAndStartDiscv5 for port=0 auto-port retry" + ) + let dynamicBootstrapEnrs = dynamicBootstrapNodes.filterIt(it.hasUdpPort()).mapIt(it.enr.get()) @@ -441,10 +447,52 @@ proc setupDiscoveryV5*( autoupdateRecord: conf.enrAutoUpdate, ) - WakuDiscoveryV5.new( - rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue + ok( + WakuDiscoveryV5.new( + rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue + ) ) +proc setupAndStartDiscv5*( + myENR: enr.Record, + nodePeerManager: PeerManager, + nodeTopicSubscriptionQueue: AsyncEventQueue[SubscriptionEvent], + conf: Discv5Conf, + dynamicBootstrapNodes: seq[RemotePeerInfo], + rng: ref HmacDrbgContext, + key: crypto.PrivateKey, + p2pListenAddress: IpAddress, + portsShift: uint16, +): Future[Result[WakuDiscoveryV5, string]] {.async: (raises: []).} = + ## Construct and start a `WakuDiscoveryV5` instance, handling auto-port + ## retry when the caller asks for `udpPort == 0`. + var c = conf + let autoMode = c.udpPort == Port(0) + let attempts = if autoMode: AutoPortRetryCount else: 1 + var lastErr = "" + + for attempt in 1 .. attempts: + if autoMode: + c.udpPort = Port(getAutoPort()) + + let wd = setupDiscoveryV5( + myENR, nodePeerManager, nodeTopicSubscriptionQueue, c, dynamicBootstrapNodes, rng, + key, p2pListenAddress, portsShift, + ).valueOr: + return err(error) + + let startRes = await wd.start() + if startRes.isOk(): + return ok(wd) + lastErr = startRes.error + + if autoMode: + return err("discv5: auto-port bind exhausted; last error: " & lastErr) + return err(lastErr) + +proc udpPort*(wd: WakuDiscoveryV5): Port = + wd.conf.port + proc updateBootstrapRecords*( self: var WakuDiscoveryV5, newRecordsString: string ): Result[void, string] = diff --git a/waku/factory/conf_builder/discv5_conf_builder.nim b/waku/factory/conf_builder/discv5_conf_builder.nim index e2729021e..4c654f89d 100644 --- a/waku/factory/conf_builder/discv5_conf_builder.nim +++ b/waku/factory/conf_builder/discv5_conf_builder.nim @@ -38,8 +38,8 @@ proc withTableIpLimit*(b: var Discv5ConfBuilder, tableIpLimit: uint) = proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: Port) = b.udpPort = some(udpPort) -proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint) = - b.udpPort = some(Port(udpPort.uint16)) +proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint16) = + b.udpPort = some(Port(udpPort)) proc withBootstrapNodes*(b: var Discv5ConfBuilder, bootstrapNodes: seq[string]) = # TODO: validate ENRs? @@ -49,6 +49,9 @@ proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] = if not b.enabled.get(false): return ok(none(Discv5Conf)) + if b.udpPort.isNone(): + return err("discv5.udpPort is not specified") + return ok( some( Discv5Conf( @@ -57,7 +60,7 @@ proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] = bucketIpLimit: b.bucketIpLimit.get(2), enrAutoUpdate: b.enrAutoUpdate.get(true), tableIpLimit: b.tableIpLimit.get(10), - udpPort: b.udpPort.get(9000.Port), + udpPort: b.udpPort.get(), ) ) ) diff --git a/waku/factory/conf_builder/metrics_server_conf_builder.nim b/waku/factory/conf_builder/metrics_server_conf_builder.nim index 0f0d18564..1084752dc 100644 --- a/waku/factory/conf_builder/metrics_server_conf_builder.nim +++ b/waku/factory/conf_builder/metrics_server_conf_builder.nim @@ -36,11 +36,14 @@ proc build*(b: MetricsServerConfBuilder): Result[Option[MetricsServerConf], stri if not b.enabled.get(false): return ok(none(MetricsServerConf)) + if b.httpPort.isNone(): + return err("metricsServer.httpPort is not specified") + return ok( some( MetricsServerConf( httpAddress: b.httpAddress.get(static parseIpAddress("127.0.0.1")), - httpPort: b.httpPort.get(8008.Port), + httpPort: b.httpPort.get(), logging: b.logging.get(false), ) ) diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index 78dbd9eb9..e6bc11706 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -8,11 +8,13 @@ import results import - ../waku_conf, - ../networks_config, - ../../common/logging, - ../../common/utils/parse_size_units, - ../../waku_enr/capabilities, + waku/[ + factory/waku_conf, + factory/networks_config, + common/logging, + common/utils/parse_size_units, + waku_enr/capabilities, + ], tools/confutils/entry_nodes import @@ -574,12 +576,9 @@ proc build*( warn "Nat Strategy is not specified, defaulting to none" "none" - let p2pTcpPort = - if builder.p2pTcpPort.isSome(): - builder.p2pTcpPort.get() - else: - warn "P2P Listening TCP Port is not specified, listening on 60000" - 60000.Port + if builder.p2pTcpPort.isNone(): + return err("p2pTcpPort is not specified") + let p2pTcpPort = builder.p2pTcpPort.get() let p2pListenAddress = if builder.p2pListenAddress.isSome(): diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 45e0edee0..0e78f1fd9 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -202,6 +202,11 @@ proc new*( else: nil + if not restServer.isNil(): + let boundRestPort = restServer.httpServer.address.port + node.ports.rest = some(boundRestPort.uint16) + wakuConf.restServerConf.get().port = boundRestPort + # Set the extMultiAddrsOnly flag so the node knows not to replace explicit addresses node.extMultiAddrsOnly = wakuConf.endpointConf.extMultiAddrsOnly @@ -249,7 +254,7 @@ proc getPorts( return ok((tcpPort: tcpPort, websocketPort: websocketPort)) proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.async.} = - var conf = waku[].conf + let conf = waku[].conf let (tcpPort, websocketPort) = getPorts(waku[].node.switch.peerInfo.listenAddrs).valueOr: return err("Could not retrieve ports: " & error) @@ -281,6 +286,10 @@ proc updateEnr(waku: ptr Waku): Future[Result[void, string]] {.async.} = waku[].node.enr = record + # If TCP/WS was configured with port 0, node.announcedAddresses was built + # pre-bind with a port value of 0. In any case, the resync is harmless. + waku[].node.announcedAddresses = netConf.announcedAddresses + return ok() proc updateAddressInENR(waku: ptr Waku): Result[void, string] = @@ -312,11 +321,8 @@ proc updateAddressInENR(waku: ptr Waku): Result[void, string] = return ok() proc updateWaku(waku: ptr Waku): Future[Result[void, string]] {.async.} = - let conf = waku[].conf - if conf.endpointConf.p2pTcpPort == Port(0) or - (conf.websocketConf.isSome() and conf.websocketConf.get.port == Port(0)): - (await updateEnr(waku)).isOkOr: - return err("error calling updateEnr: " & $error) + (await updateEnr(waku)).isOkOr: + return err("error calling updateEnr: " & $error) ?updateAnnouncedAddrWithPrimaryIpAddr(waku[].node) @@ -390,29 +396,39 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: (await startNode(waku.node, waku.conf, waku.dynamicBootstrapNodes)).isOkOr: return err("error while calling startNode: " & $error) - ## Update waku data that is set dynamically on node start - try: - (await updateWaku(waku)).isOkOr: - return err("Error in updateApp: " & $error) - except CatchableError: - return err("Caught exception in updateApp: " & getCurrentExceptionMsg()) + let bound = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr: + return err("failed to read bound ports from switch: " & $error) + if bound.tcpPort.isSome(): + waku[].node.ports.tcp = some(bound.tcpPort.get().uint16) + if bound.websocketPort.isSome(): + waku[].node.ports.webSocket = some(bound.websocketPort.get().uint16) ## Discv5 if conf.discv5Conf.isSome(): - waku[].wakuDiscV5 = waku_discv5.setupDiscoveryV5( - waku.node.enr, - waku.node.peerManager, - waku.node.topicSubscriptionQueue, - conf.discv5Conf.get(), - waku.dynamicBootstrapNodes, - waku.rng, - conf.nodeKey, - conf.endpointConf.p2pListenAddress, - conf.portsShift, - ) + waku[].wakuDiscV5 = ( + await waku_discv5.setupAndStartDiscv5( + waku.node.enr, + waku.node.peerManager, + waku.node.topicSubscriptionQueue, + conf.discv5Conf.get(), + waku.dynamicBootstrapNodes, + waku.rng, + conf.nodeKey, + conf.endpointConf.p2pListenAddress, + conf.portsShift, + ) + ).valueOr: + return err("failed to start waku discovery v5: " & error) - (await waku.wakuDiscV5.start()).isOkOr: - return err("failed to start waku discovery v5: " & $error) + waku[].node.ports.discv5Udp = some(waku[].wakuDiscV5.udpPort.uint16) + waku[].conf.discv5Conf.get().udpPort = waku[].wakuDiscV5.udpPort + + ## Update waku data that is set dynamically on node start + try: + (await updateWaku(waku)).isOkOr: + return err("Error in updateWaku: " & $error) + except CatchableError: + return err("Caught exception in updateWaku: " & getCurrentExceptionMsg()) ## Reliability if not waku[].deliveryService.isNil(): @@ -482,14 +498,15 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: if conf.metricsServerConf.isSome(): try: - waku[].metricsServer = ( - await ( - waku_metrics.startMetricsServerAndLogging( - conf.metricsServerConf.get(), conf.portsShift - ) + let (server, port) = ( + await waku_metrics.startMetricsServerAndLogging( + conf.metricsServerConf.get(), conf.portsShift ) ).valueOr: return err("Starting monitoring and external interfaces failed: " & error) + waku[].metricsServer = server + waku[].node.ports.metrics = some(port.uint16) + waku[].conf.metricsServerConf.get().httpPort = port except CatchableError: return err( "Caught exception starting monitoring and external interfaces failed: " & diff --git a/waku/node/waku_metrics.nim b/waku/node/waku_metrics.nim index 8d38624c1..d1c30cdbc 100644 --- a/waku/node/waku_metrics.nim +++ b/waku/node/waku_metrics.nim @@ -2,8 +2,11 @@ import chronicles, chronos, metrics, metrics/chronos_httpserver import - ../waku_rln_relay/protocol_metrics as rln_metrics, - ../utils/collector, + waku/[ + common/auto_port, + waku_rln_relay/protocol_metrics as rln_metrics, + utils/collector, + ], ./peer_manager, ./waku_node @@ -57,27 +60,44 @@ proc startMetricsLog*() = discard setTimer(Moment.fromNow(LogInterval), logMetrics) +type StartedMetricsServer* = tuple[server: MetricsHttpServerRef, port: Port] + proc startMetricsServer( serverIp: IpAddress, serverPort: Port -): Future[Result[MetricsHttpServerRef, string]] {.async.} = - info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $serverPort +): Future[Result[StartedMetricsServer, string]] {.async.} = + let autoMode = serverPort == Port(0) + let attempts = if autoMode: AutoPortRetryCount else: 1 + var lastErr = "" - let server = MetricsHttpServerRef.new($serverIp, serverPort).valueOr: - return err("metrics HTTP server start failed: " & $error) + for attempt in 1 .. attempts: + let port = + if autoMode: + Port(getAutoPort()) + else: + serverPort + info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $port - try: - await server.start() - except CatchableError: - return err("metrics HTTP server start failed: " & getCurrentExceptionMsg()) + let server = MetricsHttpServerRef.new($serverIp, port).valueOr: + lastErr = $error + continue - info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $serverPort - return ok(server) + try: + await server.start() + except CatchableError: + lastErr = getCurrentExceptionMsg() + continue + + info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $port + return ok((server: server, port: port)) + + if autoMode: + return err("metrics HTTP server: auto-port bind exhausted; last error: " & lastErr) + return err("metrics HTTP server start failed: " & lastErr) proc startMetricsServerAndLogging*( conf: MetricsServerConf, portsShift: uint16 -): Future[Result[MetricsHttpServerRef, string]] {.async.} = - var metricsServer: MetricsHttpServerRef - metricsServer = ( +): Future[Result[StartedMetricsServer, string]] {.async.} = + let started = ( await ( startMetricsServer(conf.httpAddress, Port(conf.httpPort.uint16 + portsShift)) ) @@ -87,4 +107,4 @@ proc startMetricsServerAndLogging*( if conf.logging: startMetricsLog() - return ok(metricsServer) + return ok(started) diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 506a3e592..b5e2b360f 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -96,11 +96,19 @@ const WakuNodeVersionString* = "version / git commit hash: " & git_version # key and crypto modules different type + BoundPorts* = object ## Set by the factory once each service has bound to a port. + tcp*: Option[uint16] + webSocket*: Option[uint16] + rest*: Option[uint16] + discv5Udp*: Option[uint16] + metrics*: Option[uint16] + # TODO: Move to application instance (e.g., `WakuNode2`) WakuInfo* = object # NOTE One for simplicity, can extend later as needed listenAddresses*: seq[string] enrUri*: string #multiaddrStrings*: seq[string] mixPubKey*: Option[string] + ports*: BoundPorts # NOTE based on Eth2Node in NBC eth2_network.nim WakuNode* = ref object @@ -140,6 +148,7 @@ type wakuMix*: WakuMix kademliaDiscoveryLoop*: Future[void] wakuKademlia*: WakuKademlia + ports*: BoundPorts proc deduceRelayShard( node: WakuNode, @@ -244,11 +253,13 @@ proc info*(node: WakuNode): WakuInfo = let peerInfo = node.switch.peerInfo var listenStr: seq[string] - for address in node.announcedAddresses: + # Post-bind: when a transport was given port=0, this reflects the real + # OS-assigned port rather than the pre-bind configured 0. + for address in peerInfo.listenAddrs: var fulladdr = $address & "/p2p/" & $peerInfo.peerId listenStr &= fulladdr let enrUri = node.enr.toUri() - var wakuInfo = WakuInfo(listenAddresses: listenStr, enrUri: enrUri) + var wakuInfo = WakuInfo(listenAddresses: listenStr, enrUri: enrUri, ports: node.ports) if not node.wakuMix.isNil(): let keyStr = node.wakuMix.pubKey.to0xHex() wakuInfo.mixPubKey = some(keyStr) diff --git a/waku/rest_api/endpoint/debug/types.nim b/waku/rest_api/endpoint/debug/types.nim index c03af0675..c0268b6c4 100644 --- a/waku/rest_api/endpoint/debug/types.nim +++ b/waku/rest_api/endpoint/debug/types.nim @@ -10,6 +10,7 @@ type DebugWakuInfo* = object listenAddresses*: seq[string] enrUri*: Option[string] mixPubKey*: Option[string] + ports*: BoundPorts #### Type conversion @@ -18,10 +19,45 @@ proc toDebugWakuInfo*(nodeInfo: WakuInfo): DebugWakuInfo = listenAddresses: nodeInfo.listenAddresses, enrUri: some(nodeInfo.enrUri), mixPubKey: nodeInfo.mixPubKey, + ports: nodeInfo.ports, ) #### Serialization and deserialization +proc writeValue*( + writer: var JsonWriter[RestJson], value: BoundPorts +) {.raises: [IOError].} = + writer.beginRecord() + if value.tcp.isSome(): + writer.writeField("tcp", value.tcp.get()) + if value.webSocket.isSome(): + writer.writeField("webSocket", value.webSocket.get()) + if value.rest.isSome(): + writer.writeField("rest", value.rest.get()) + if value.discv5Udp.isSome(): + writer.writeField("discv5Udp", value.discv5Udp.get()) + if value.metrics.isSome(): + writer.writeField("metrics", value.metrics.get()) + writer.endRecord() + +proc readValue*( + reader: var JsonReader[RestJson], value: var BoundPorts +) {.raises: [SerializationError, IOError].} = + for fieldName in readObjectFields(reader): + case fieldName + of "tcp": + value.tcp = some(reader.readValue(uint16)) + of "webSocket": + value.webSocket = some(reader.readValue(uint16)) + of "rest": + value.rest = some(reader.readValue(uint16)) + of "discv5Udp": + value.discv5Udp = some(reader.readValue(uint16)) + of "metrics": + value.metrics = some(reader.readValue(uint16)) + else: + unrecognizedFieldWarning(value) + proc writeValue*( writer: var JsonWriter[RestJson], value: DebugWakuInfo ) {.raises: [IOError].} = @@ -31,6 +67,7 @@ proc writeValue*( writer.writeField("enrUri", value.enrUri.get()) if value.mixPubKey.isSome(): writer.writeField("mixPubKey", value.mixPubKey.get()) + writer.writeField("ports", value.ports) writer.endRecord() proc readValue*( @@ -39,6 +76,7 @@ proc readValue*( var listenAddresses: Option[seq[string]] enrUri: Option[string] + ports: BoundPorts for fieldName in readObjectFields(reader): case fieldName @@ -58,6 +96,8 @@ proc readValue*( "Multiple `mixPubKey` fields found", "DebugWakuInfo" ) value.mixPubKey = some(reader.readValue(string)) + of "ports": + ports = reader.readValue(BoundPorts) else: unrecognizedFieldWarning(value) @@ -65,5 +105,8 @@ proc readValue*( reader.raiseUnexpectedValue("Field `listenAddresses` is missing") value = DebugWakuInfo( - listenAddresses: listenAddresses.get, enrUri: enrUri, mixPubKey: value.mixPubKey + listenAddresses: listenAddresses.get, + enrUri: enrUri, + mixPubKey: value.mixPubKey, + ports: ports, )