From c3090fb62febef41a4436eb328944a63486b8e30 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Wed, 17 Jun 2026 07:55:45 -0300 Subject: [PATCH] feat: QUIC transport support (#3951) * additive quic transport, off by default (--quic-support) * add QuicConf + QuicConfBuilder, --quic-support / --quic-port flags * net_config announces quic-v1 host/ext/dns4 addrs, adds quic-v1 to enr multiaddrs * newWakuSwitch mounts quic transport when a quic addr is set * toRemotePeerInfo: quic from enr multiaddrs ext, sorted quic-first * BoundPorts.quic, read back the bound quic port at start (handles --quic-port=0) * skip auto quic addr when operator supplies one via --ext-multiaddr * tests: nodes dual-stack by default, tcp-only where single transport asserted * tests: drop hardcoded ephemeral ports (port 0) to fix quic-churn bind flakes * use setupNat to discover NAT-mapped UDP port when QUIC enabled --- logos_delivery/waku/factory/builder.nim | 1 + .../factory/conf_builder/conf_builder.nim | 7 +- .../conf_builder/quic_conf_builder.nim | 33 +++++++ .../conf_builder/waku_conf_builder.nim | 7 ++ .../waku/factory/internal_config.nim | 33 +++++-- logos_delivery/waku/factory/node_factory.nim | 4 +- logos_delivery/waku/factory/waku.nim | 21 ++++- logos_delivery/waku/factory/waku_conf.nim | 4 + logos_delivery/waku/net/bound_ports.nim | 8 +- logos_delivery/waku/net/net_config.nim | 57 +++++++++++- logos_delivery/waku/node/waku_switch.nim | 18 +++- logos_delivery/waku/waku_core/peers.nim | 75 +++++++-------- tests/factory/test_node_factory.nim | 33 ++++--- tests/test_peer_manager.nim | 28 ++++-- tests/test_waku_netconfig.nim | 57 ++++++++++++ tests/test_wakunode.nim | 93 +++++++++++++++++-- tests/testlib/wakunode.nim | 15 +++ tests/waku_relay/test_wakunode_relay.nim | 6 +- tests/wakunode2/test_app.nim | 30 +++++- tests/wakunode_rest/test_rest_admin.nim | 6 +- tools/confutils/cli_args.nim | 14 +++ 21 files changed, 451 insertions(+), 99 deletions(-) create mode 100644 logos_delivery/waku/factory/conf_builder/quic_conf_builder.nim diff --git a/logos_delivery/waku/factory/builder.nim b/logos_delivery/waku/factory/builder.nim index aff49e873..3ca6313e0 100644 --- a/logos_delivery/waku/factory/builder.nim +++ b/logos_delivery/waku/factory/builder.nim @@ -190,6 +190,7 @@ proc build*(builder: WakuNodeBuilder): Result[WakuNode, string] = privKey = builder.nodekey, address = builder.netConfig.get().hostAddress, wsAddress = builder.netConfig.get().wsHostAddress, + quicAddress = builder.netConfig.get().quicHostAddress, transportFlags = {ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay}, rng = rng, maxConnections = builder.switchMaxConnections.get(MaxConnections), diff --git a/logos_delivery/waku/factory/conf_builder/conf_builder.nim b/logos_delivery/waku/factory/conf_builder/conf_builder.nim index b8d0316c3..1ac9fdef7 100644 --- a/logos_delivery/waku/factory/conf_builder/conf_builder.nim +++ b/logos_delivery/waku/factory/conf_builder/conf_builder.nim @@ -7,6 +7,7 @@ import ./dns_discovery_conf_builder, ./discv5_conf_builder, ./web_socket_conf_builder, + ./quic_conf_builder, ./metrics_server_conf_builder, ./rate_limit_conf_builder, ./rln_relay_conf_builder, @@ -16,6 +17,6 @@ import export waku_conf_builder, filter_service_conf_builder, store_sync_conf_builder, store_service_conf_builder, rest_server_conf_builder, dns_discovery_conf_builder, - discv5_conf_builder, web_socket_conf_builder, metrics_server_conf_builder, - rate_limit_conf_builder, rln_relay_conf_builder, mix_conf_builder, - kademlia_discovery_conf_builder + discv5_conf_builder, web_socket_conf_builder, quic_conf_builder, + metrics_server_conf_builder, rate_limit_conf_builder, rln_relay_conf_builder, + mix_conf_builder, kademlia_discovery_conf_builder diff --git a/logos_delivery/waku/factory/conf_builder/quic_conf_builder.nim b/logos_delivery/waku/factory/conf_builder/quic_conf_builder.nim new file mode 100644 index 000000000..8233b1581 --- /dev/null +++ b/logos_delivery/waku/factory/conf_builder/quic_conf_builder.nim @@ -0,0 +1,33 @@ +import chronicles, std/[net, options], results +import logos_delivery/waku/factory/waku_conf + +logScope: + topics = "waku conf builder quic" + +# same value as tcp default port. quic is udp, no conflict. +const DefaultQuicPort*: Port = Port(60000) + +######################### +## QUIC Config Builder ## +######################### +type QuicConfBuilder* = object + enabled*: Option[bool] + quicPort*: Option[Port] + +proc init*(T: type QuicConfBuilder): QuicConfBuilder = + QuicConfBuilder() + +proc withEnabled*(b: var QuicConfBuilder, enabled: bool) = + b.enabled = some(enabled) + +proc withQuicPort*(b: var QuicConfBuilder, quicPort: Port) = + b.quicPort = some(quicPort) + +proc withQuicPort*(b: var QuicConfBuilder, quicPort: uint16) = + b.quicPort = some(Port(quicPort)) + +proc build*(b: QuicConfBuilder): Result[Option[QuicConf], string] = + if not b.enabled.get(false): + return ok(none(QuicConf)) + + return ok(some(QuicConf(port: b.quicPort.get(DefaultQuicPort)))) diff --git a/logos_delivery/waku/factory/conf_builder/waku_conf_builder.nim b/logos_delivery/waku/factory/conf_builder/waku_conf_builder.nim index 4bbb0145b..dc17fb4ff 100644 --- a/logos_delivery/waku/factory/conf_builder/waku_conf_builder.nim +++ b/logos_delivery/waku/factory/conf_builder/waku_conf_builder.nim @@ -30,6 +30,7 @@ import ./dns_discovery_conf_builder, ./discv5_conf_builder, ./web_socket_conf_builder, + ./quic_conf_builder, ./metrics_server_conf_builder, ./rate_limit_conf_builder, ./rln_relay_conf_builder, @@ -119,6 +120,7 @@ type WakuConfBuilder* = object storeServiceConf*: StoreServiceConfBuilder mixConf*: MixConfBuilder webSocketConf*: WebSocketConfBuilder + quicConf*: QuicConfBuilder rateLimitConf*: RateLimitConfBuilder kademliaDiscoveryConf*: KademliaDiscoveryConfBuilder # End conf builders @@ -182,6 +184,7 @@ proc init*(T: type WakuConfBuilder): WakuConfBuilder = rlnRelayConf: RlnRelayConfBuilder.init(), storeServiceConf: StoreServiceConfBuilder.init(), webSocketConf: WebSocketConfBuilder.init(), + quicConf: QuicConfBuilder.init(), rateLimitConf: RateLimitConfBuilder.init(), kademliaDiscoveryConf: KademliaDiscoveryConfBuilder.init(), ) @@ -648,6 +651,9 @@ proc build*( let webSocketConf = builder.webSocketConf.build().valueOr: return err("WebSocket Conf building failed: " & $error) + let quicConf = builder.quicConf.build().valueOr: + return err("QUIC Conf building failed: " & $error) + let rateLimit = builder.rateLimitConf.build().valueOr: return err("Rate limits Conf building failed: " & $error) @@ -802,6 +808,7 @@ proc build*( ), portsShift: portsShift, webSocketConf: webSocketConf, + quicConf: quicConf, dnsAddrsNameServers: dnsAddrsNameServers, peerPersistence: peerPersistence, peerStoreCapacity: builder.peerStoreCapacity, diff --git a/logos_delivery/waku/factory/internal_config.nim b/logos_delivery/waku/factory/internal_config.nim index 75e5be1ad..08f6e2568 100644 --- a/logos_delivery/waku/factory/internal_config.nim +++ b/logos_delivery/waku/factory/internal_config.nim @@ -87,18 +87,24 @@ proc networkConfiguration*( conf: EndpointConf, discv5Conf: Option[Discv5Conf], webSocketConf: Option[WebSocketConf], + quicConf: Option[QuicConf], wakuFlags: CapabilitiesBitfield, dnsAddrsNameServers: seq[IpAddress], portsShift: uint16, clientId: string, ): Future[NetConfigResult] {.async.} = - ## `udpPort` is only supplied to satisfy underlying APIs but is not - ## actually a supported transport for libp2p traffic. - var (extIp, extTcpPort, _) = setupNat( - conf.natStrategy.string, - clientId, - Port(uint16(conf.p2pTcpPort) + portsShift), - Port(uint16(conf.p2pTcpPort) + portsShift), + let tcpBindPort = Port(uint16(conf.p2pTcpPort) + portsShift) + + let (quicEnabled, quicBindPort) = + if quicConf.isSome(): + let qConf = quicConf.get() + (true, some(Port(qConf.port.uint16 + portsShift))) + else: + (false, none(Port)) + + # NAT-map the QUIC UDP port (placeholder when QUIC off) + var (extIp, extTcpPort, extUdpPort) = setupNat( + conf.natStrategy.string, clientId, tcpBindPort, quicBindPort.get(tcpBindPort) ).valueOr: return err("failed to setup NAT: " & $error) @@ -115,10 +121,16 @@ proc networkConfiguration*( ## manual config, the external port is the same as the bind port. extPort = if (extIp.isSome() or conf.dns4DomainName.isSome()) and extTcpPort.isNone(): - some(Port(uint16(conf.p2pTcpPort) + portsShift)) + some(tcpBindPort) else: extTcpPort + extQuicPort = + if (extIp.isSome() or conf.dns4DomainName.isSome()) and extUdpPort.isNone(): + quicBindPort + else: + extUdpPort + # Resolve and use DNS domain IP if conf.dns4DomainName.isSome() and extIp.isNone(): try: @@ -143,7 +155,7 @@ proc networkConfiguration*( let netConfigRes = NetConfig.init( clusterId = clusterId, bindIp = conf.p2pListenAddress, - bindPort = Port(uint16(conf.p2pTcpPort) + portsShift), + bindPort = tcpBindPort, extIp = extIp, extPort = extPort, extMultiAddrs = conf.extMultiAddrs, @@ -151,6 +163,9 @@ proc networkConfiguration*( wsBindPort = wsBindPort, wsEnabled = wsEnabled, wssEnabled = wssEnabled, + quicBindPort = quicBindPort, + quicEnabled = quicEnabled, + extQuicPort = extQuicPort, dns4DomainName = conf.dns4DomainName, discv5UdpPort = discv5UdpPort, wakuFlags = some(wakuFlags), diff --git a/logos_delivery/waku/factory/node_factory.nim b/logos_delivery/waku/factory/node_factory.nim index f19e9632b..b02364f8b 100644 --- a/logos_delivery/waku/factory/node_factory.nim +++ b/logos_delivery/waku/factory/node_factory.nim @@ -465,8 +465,8 @@ proc setupNode*( let netConfig = ( await networkConfiguration( wakuConf.clusterId, wakuConf.endpointConf, wakuConf.discv5Conf, - wakuConf.webSocketConf, wakuConf.wakuFlags, wakuConf.dnsAddrsNameServers, - wakuConf.portsShift, clientId, + wakuConf.webSocketConf, wakuConf.quicConf, wakuConf.wakuFlags, + wakuConf.dnsAddrsNameServers, wakuConf.portsShift, clientId, ) ).valueOr: error "failed to create internal config", error = error diff --git a/logos_delivery/waku/factory/waku.nim b/logos_delivery/waku/factory/waku.nim index 2eee46103..e15a92fd0 100644 --- a/logos_delivery/waku/factory/waku.nim +++ b/logos_delivery/waku/factory/waku.nim @@ -249,8 +249,8 @@ proc new*( proc getPorts( listenAddrs: seq[MultiAddress] -): Result[tuple[tcpPort, websocketPort: Option[Port]], string] = - var tcpPort, websocketPort = none(Port) +): Result[tuple[tcpPort, websocketPort, quicPort: Option[Port]], string] = + var tcpPort, websocketPort, quicPort = none(Port) for a in listenAddrs: if a.isWsAddress(): @@ -258,16 +258,23 @@ proc getPorts( let wsAddress = initTAddress(a).valueOr: return err("getPorts wsAddr error:" & $error) websocketPort = some(wsAddress.port) + elif a.isQuicAddress(): + if quicPort.isNone(): + let quicAddress = initTAddress(a).valueOr: + return err("getPorts quicAddr error:" & $error) + quicPort = some(quicAddress.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)) + return ok((tcpPort: tcpPort, websocketPort: websocketPort, quicPort: quicPort)) proc getRunningNetConfig(waku: Waku): Future[Result[NetConfig, string]] {.async.} = let conf = waku.conf - let (tcpPort, websocketPort) = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr: + let (tcpPort, websocketPort, quicPort) = getPorts( + waku.node.switch.peerInfo.listenAddrs + ).valueOr: return err("Could not retrieve ports: " & error) if tcpPort.isSome(): @@ -276,11 +283,14 @@ proc getRunningNetConfig(waku: Waku): Future[Result[NetConfig, string]] {.async. if websocketPort.isSome() and conf.webSocketConf.isSome(): conf.webSocketConf.get().port = websocketPort.get() + if quicPort.isSome() and conf.quicConf.isSome(): + conf.quicConf.get().port = quicPort.get() + # Rebuild NetConfig with bound port values let netConf = ( await networkConfiguration( conf.clusterId, conf.endpointConf, conf.discv5Conf, conf.webSocketConf, - conf.wakuFlags, conf.dnsAddrsNameServers, conf.portsShift, clientId, + conf.quicConf, conf.wakuFlags, conf.dnsAddrsNameServers, conf.portsShift, clientId, ) ).valueOr: return err("Could not update NetConfig: " & error) @@ -444,6 +454,7 @@ proc start*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = return err("failed to read bound ports from switch: " & $error) waku.node.ports.tcp = bound.tcpPort.get(Port(0)).uint16 waku.node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16 + waku.node.ports.quic = bound.quicPort.get(Port(0)).uint16 ## Discv5 if conf.discv5Conf.isSome(): diff --git a/logos_delivery/waku/factory/waku_conf.nim b/logos_delivery/waku/factory/waku_conf.nim index 9edc12a44..265a650c4 100644 --- a/logos_delivery/waku/factory/waku_conf.nim +++ b/logos_delivery/waku/factory/waku_conf.nim @@ -32,6 +32,9 @@ type WebSocketConf* = object port*: Port secureConf*: Option[WebSocketSecureConf] +type QuicConf* = object + port*: Port + # TODO: should be defined in validator_signed.nim and imported here type ProtectedShard* {.requiresInit.} = object shard*: uint16 @@ -112,6 +115,7 @@ type WakuConf* {.requiresInit.} = ref object restServerConf*: Option[RestServerConf] metricsServerConf*: Option[MetricsServerConf] webSocketConf*: Option[WebSocketConf] + quicConf*: Option[QuicConf] mixConf*: Option[MixConf] kademliaDiscoveryConf*: Option[KademliaDiscoveryConf] diff --git a/logos_delivery/waku/net/bound_ports.nim b/logos_delivery/waku/net/bound_ports.nim index f8f561940..9fb390b5d 100644 --- a/logos_delivery/waku/net/bound_ports.nim +++ b/logos_delivery/waku/net/bound_ports.nim @@ -7,13 +7,19 @@ type BoundPorts* {.requiresInit.} = object ## A value of 0 means the service was not enabled or did not bind. tcp*: uint16 webSocket*: uint16 + quic*: uint16 rest*: uint16 discv5Udp*: uint16 metrics*: uint16 proc init*(T: type BoundPorts): BoundPorts = return BoundPorts( - tcp: 0'u16, webSocket: 0'u16, rest: 0'u16, discv5Udp: 0'u16, metrics: 0'u16 + tcp: 0'u16, + webSocket: 0'u16, + quic: 0'u16, + rest: 0'u16, + discv5Udp: 0'u16, + metrics: 0'u16, ) proc `$`*(p: BoundPorts): string = diff --git a/logos_delivery/waku/net/net_config.nim b/logos_delivery/waku/net/net_config.nim index fc4b42fe6..b5eda9d6c 100644 --- a/logos_delivery/waku/net/net_config.nim +++ b/logos_delivery/waku/net/net_config.nim @@ -9,6 +9,7 @@ type NetConfig* = object hostAddress*: MultiAddress clusterId*: uint16 wsHostAddress*: Option[MultiAddress] + quicHostAddress*: Option[MultiAddress] hostExtAddress*: Option[MultiAddress] wsExtAddress*: Option[MultiAddress] wssEnabled*: bool @@ -46,6 +47,18 @@ template wsFlag(wssEnabled: bool): MultiAddress = else: MultiAddress.init("/ws").tryGet() +template udpPortMa(port: Port): MultiAddress = + MultiAddress.init("/udp/" & $port).tryGet() + +template quicFlag(): MultiAddress = + MultiAddress.init("/quic-v1").tryGet() + +template ipQuicEndPoint(address: IpAddress, port: Port): MultiAddress = + MultiAddress.init(address, udpProtocol, port) & quicFlag() + +template dns4QuicEndPoint(dns4DomainName: string, port: Port): MultiAddress = + dns4Ma(dns4DomainName) & udpPortMa(port) & quicFlag() + proc formatListenAddress(inputMultiAdd: MultiAddress): MultiAddress = let inputStr = $inputMultiAdd # If MultiAddress contains "0.0.0.0", replace it for "127.0.0.1" @@ -58,9 +71,15 @@ proc isWsAddress*(ma: MultiAddress): bool = return isWs or isWss +proc isQuicAddress*(ma: MultiAddress): bool = + return ma.hasProtocol("quic-v1") + proc containsWsAddress(extMultiAddrs: seq[MultiAddress]): bool = return extMultiAddrs.filterIt(it.isWsAddress()).len > 0 +proc containsQuicAddress(extMultiAddrs: seq[MultiAddress]): bool = + return extMultiAddrs.filterIt(it.isQuicAddress()).len > 0 + const DefaultWsBindPort = static(Port(8000)) # TODO: migrate to builder pattern with nested configs proc init*( @@ -74,6 +93,9 @@ proc init*( wsBindPort: Option[Port] = some(DefaultWsBindPort), wsEnabled: bool = false, wssEnabled: bool = false, + quicBindPort = none(Port), + quicEnabled: bool = false, + extQuicPort = none(Port), dns4DomainName = none(string), discv5UdpPort = none(Port), clusterId: uint16 = 0, @@ -94,6 +116,13 @@ proc init*( except CatchableError: return err(getCurrentExceptionMsg()) + var quicHostAddress = none(MultiAddress) + if quicEnabled: + try: + quicHostAddress = some(ipQuicEndPoint(bindIp, quicBindPort.get(bindPort))) + except CatchableError: + return err("failed to initialize quic address: " & getCurrentExceptionMsg()) + let enrIp = if extIp.isSome(): extIp @@ -106,7 +135,7 @@ proc init*( some(bindPort) # Setup external addresses, if available - var hostExtAddress, wsExtAddress = none(MultiAddress) + var hostExtAddress, wsExtAddress, quicExtAddress = none(MultiAddress) if dns4DomainName.isSome(): # Use dns4 for externally announced addresses @@ -123,6 +152,16 @@ proc init*( ) except CatchableError: return err(getCurrentExceptionMsg()) + + if quicHostAddress.isSome(): + try: + quicExtAddress = some( + dns4QuicEndPoint( + dns4DomainName.get(), extQuicPort.get(quicBindPort.get(bindPort)) + ) + ) + except CatchableError: + return err("failed to set dns quic endpoint: " & getCurrentExceptionMsg()) else: # No public domain name, use ext IP if available if extIp.isSome() and extPort.isSome(): @@ -137,6 +176,14 @@ proc init*( except CatchableError: return err(getCurrentExceptionMsg()) + if quicHostAddress.isSome(): + try: + quicExtAddress = some( + ipQuicEndPoint(extIp.get(), extQuicPort.get(quicBindPort.get(bindPort))) + ) + except CatchableError: + return err("failed to set ip quic endpoint: " & getCurrentExceptionMsg()) + var announcedAddresses = newSeq[MultiAddress]() if not extMultiAddrsOnly: @@ -152,6 +199,11 @@ proc init*( # Only publish wsHostAddress if a WS address is not set in extMultiAddrs announcedAddresses.add(wsHostAddress.get()) + if quicExtAddress.isSome(): + announcedAddresses.add(quicExtAddress.get()) + elif quicHostAddress.isSome() and not containsQuicAddress(extMultiAddrs): + announcedAddresses.add(formatListenAddress(quicHostAddress.get())) + # External multiaddrs that the operator may have configured if extMultiAddrs.len > 0: announcedAddresses.add(extMultiAddrs) @@ -164,7 +216,7 @@ proc init*( enrMultiaddrs = deduplicate( announcedAddresses.filterIt( it.hasProtocol("dns4") or it.hasProtocol("dns6") or it.hasProtocol("ws") or - it.hasProtocol("wss") + it.hasProtocol("wss") or it.hasProtocol("quic-v1") ) ) @@ -173,6 +225,7 @@ proc init*( hostAddress: hostAddress, clusterId: clusterId, wsHostAddress: wsHostAddress, + quicHostAddress: quicHostAddress, hostExtAddress: hostExtAddress, wsExtAddress: wsExtAddress, extIp: extIp, diff --git a/logos_delivery/waku/node/waku_switch.nim b/logos_delivery/waku/node/waku_switch.nim index 31177591e..0cba923e4 100644 --- a/logos_delivery/waku/node/waku_switch.nim +++ b/logos_delivery/waku/node/waku_switch.nim @@ -59,6 +59,7 @@ proc newWakuSwitch*( privKey = none(crypto.PrivateKey), address = MultiAddress.init("/ip4/127.0.0.1/tcp/0").tryGet(), wsAddress = none(MultiAddress), + quicAddress = none(MultiAddress), secureManagers: openarray[SecureProtocol] = [SecureProtocol.Noise], transportFlags: set[ServerFlags] = {}, rng: crypto.Rng, @@ -85,7 +86,6 @@ proc newWakuSwitch*( .withYamux() .withMplex(inTimeout, outTimeout) .withNoise() - .withTcpTransport(transportFlags) .withNameResolver(nameResolver) .withSignedPeerRecord(sendSignedPeerRecord) .withCircuitRelay(circuitRelay) @@ -109,15 +109,25 @@ proc newWakuSwitch*( b = b.withAgentVersion(agentString.get()) if privKey.isSome(): b = b.withPrivateKey(privKey.get()) + # tcp always; ws/quic added when their addr is set + var addresses: seq[MultiAddress] if wsAddress.isSome(): - b = b.withAddresses(@[wsAddress.get(), address]) + addresses.add(wsAddress.get()) + addresses.add(address) + if quicAddress.isSome(): + addresses.add(quicAddress.get()) + b = b.withAddresses(addresses) + b = b.withTcpTransport(transportFlags) + + if wsAddress.isSome(): if wssEnabled: b = b.withWssTransport(secureKeyPath, secureCertPath) else: b = b.withWsTransport() - else: - b = b.withAddress(address) + + if quicAddress.isSome(): + b = b.withQuicTransport() if not rendezvous.isNil(): b = b.withRendezVous() diff --git a/logos_delivery/waku/waku_core/peers.nim b/logos_delivery/waku/waku_core/peers.nim index d6cc2db36..6e23c848f 100644 --- a/logos_delivery/waku/waku_core/peers.nim +++ b/logos_delivery/waku/waku_core/peers.nim @@ -239,15 +239,6 @@ proc parsePeerInfo*(maddrs: varargs[string]): Result[RemotePeerInfo, string] = parsePeerInfo(multiAddresses) -func getTransportProtocol(typedR: enr.TypedRecord): Option[IpTransportProtocol] = - if typedR.tcp6.isSome() or typedR.tcp.isSome(): - return some(IpTransportProtocol.tcpProtocol) - - if typedR.udp6.isSome() or typedR.udp.isSome(): - return some(IpTransportProtocol.udpProtocol) - - return none(IpTransportProtocol) - proc parseUrlPeerAddr*( peerAddr: Option[string] ): Result[Option[RemotePeerInfo], string] = @@ -262,9 +253,15 @@ proc parseUrlPeerAddr*( return ok(some(parsedPeerInfo)) +proc sortQuicFirst(addrs: seq[MultiAddress]): seq[MultiAddress] = + ## QUIC addresses first, so they are dialed ahead of TCP. + addrs.filterIt("/quic-v1" in $it) & addrs.filterIt("/quic-v1" notin $it) + proc toRemotePeerInfo*(enrRec: enr.Record): Result[RemotePeerInfo, cstring] = - ## Converts an ENR to dialable RemotePeerInfo - let typedR = enr.TypedRecord.fromRecord(enrRec) + ## enr to dialable RemotePeerInfo. tcp from tcp/tcp6 fields, quic from the + ## multiaddrs ext (udp field is discv5, not quic). quic sorted first. + let typedR = enrRec.toTyped().valueOr: + return err(cstring("enr: failed to construct typed record: " & $error)) if not typedR.secp256k1.isSome(): return err("enr: no secp256k1 key in record") @@ -273,41 +270,35 @@ proc toRemotePeerInfo*(enrRec: enr.Record): Result[RemotePeerInfo, cstring] = peerId = ?PeerID.init(crypto.PublicKey(scheme: Secp256k1, skkey: secp.SkPublicKey(pubKey))) - let transportProto = getTransportProtocol(typedR) - if transportProto.isNone(): - return err("enr: could not determine transport protocol") - var addrs = newSeq[MultiAddress]() - case transportProto.get() - of tcpProtocol: - if typedR.ip.isSome() and typedR.tcp.isSome(): - let ip = ipv4(typedR.ip.get()) - addrs.add MultiAddress.init(ip, tcpProtocol, Port(typedR.tcp.get())) - if typedR.ip6.isSome(): - let ip = ipv6(typedR.ip6.get()) - if typedR.tcp6.isSome(): - addrs.add MultiAddress.init(ip, tcpProtocol, Port(typedR.tcp6.get())) - elif typedR.tcp.isSome(): - addrs.add MultiAddress.init(ip, tcpProtocol, Port(typedR.tcp.get())) - else: - discard - of udpProtocol: - if typedR.ip.isSome() and typedR.udp.isSome(): - let ip = ipv4(typedR.ip.get()) - addrs.add MultiAddress.init(ip, udpProtocol, Port(typedR.udp.get())) + # multiaddrs ext, may include quic + let multiaddrsField = typedR.multiaddrs() + if multiaddrsField.isSome(): + addrs.add(multiaddrsField.get()) - if typedR.ip6.isSome(): - let ip = ipv6(typedR.ip6.get()) - if typedR.udp6.isSome(): - addrs.add MultiAddress.init(ip, udpProtocol, Port(typedR.udp6.get())) - elif typedR.udp.isSome(): - addrs.add MultiAddress.init(ip, udpProtocol, Port(typedR.udp.get())) - else: - discard + # tcp/tcp6 fields, skip if already in multiaddrs + if typedR.ip.isSome() and typedR.tcp.isSome(): + let ip = ipv4(typedR.ip.get()) + let tcpAddr = MultiAddress.init(ip, tcpProtocol, Port(typedR.tcp.get())) + if tcpAddr notin addrs: + addrs.add(tcpAddr) + + if typedR.ip6.isSome(): + let ip = ipv6(typedR.ip6.get()) + if typedR.tcp6.isSome(): + let tcp6Addr = MultiAddress.init(ip, tcpProtocol, Port(typedR.tcp6.get())) + if tcp6Addr notin addrs: + addrs.add(tcp6Addr) + elif typedR.tcp.isSome(): + let tcp6Addr = MultiAddress.init(ip, tcpProtocol, Port(typedR.tcp.get())) + if tcp6Addr notin addrs: + addrs.add(tcp6Addr) if addrs.len == 0: - return err("enr: no addresses in record") + return err("enr: no dialable addresses in record") + + addrs = sortQuicFirst(addrs) let protocolsRes = catch: enrRec.getCapabilitiesCodecs() @@ -331,7 +322,7 @@ converter toRemotePeerInfo*(peerInfo: PeerInfo): RemotePeerInfo = ## Useful for testing or internal connections RemotePeerInfo( peerId: peerInfo.peerId, - addrs: peerInfo.listenAddrs, + addrs: sortQuicFirst(peerInfo.listenAddrs), enr: none(enr.Record), protocols: peerInfo.protocols, shards: @[], diff --git a/tests/factory/test_node_factory.nim b/tests/factory/test_node_factory.nim index a31452c55..6a08ccfd1 100644 --- a/tests/factory/test_node_factory.nim +++ b/tests/factory/test_node_factory.nim @@ -137,12 +137,18 @@ asynctest "Start a node based on default test configuration": 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)) + let taken = createStreamServer(initTAddress("127.0.0.1", Port(0))) defer: taken.stop() await taken.closeWait() + let takenPort = taken.localAddress().port + + let freePort = block: + let probe = createStreamServer(initTAddress("127.0.0.1", Port(0))) + let p = probe.localAddress().port + probe.stop() + await probe.closeWait() + p proc buildMetricsConf(port: Port): MetricsServerConf = var b = MetricsServerConfBuilder.init() @@ -159,25 +165,30 @@ suite "Auto-port retry": await okRes.get().server.close() 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)) - defer: - await takenUdp.closeWait() - let nodeKey = generateSecp256k1Key() let node = newTestWakuNode(nodeKey, parseIpAddress("0.0.0.0"), Port(0)) await node.start() defer: await node.stop() + let takenUdp = + newDatagramTransport(dummyCb, local = initTAddress("0.0.0.0", Port(0))) + defer: + await takenUdp.closeWait() + let takenPort = takenUdp.localAddress().port + + let freePort = block: + let probe = + newDatagramTransport(dummyCb, local = initTAddress("0.0.0.0", Port(0))) + let p = probe.localAddress().port + await probe.closeWait() + p + proc buildDiscv5Conf(port: Port): Discv5Conf = var b = Discv5ConfBuilder.init() b.withEnabled(true) diff --git a/tests/test_peer_manager.nim b/tests/test_peer_manager.nim index 1505a686f..d4c2af5b5 100644 --- a/tests/test_peer_manager.nim +++ b/tests/test_peer_manager.nim @@ -307,10 +307,17 @@ procSuite "Peer Manager": let database = SqliteDatabase.new(":memory:")[] storage = WakuPeerStorage.new(database)[] + # tcp-only: asserts exact AddressBook contents node1 = newTestWakuNode( - generateSecp256k1Key(), getPrimaryIPAddr(), Port(44048), peerStorage = storage + generateSecp256k1Key(), + getPrimaryIPAddr(), + Port(44048), + peerStorage = storage, + quicEnabled = false, + ) + node2 = newTestWakuNode( + generateSecp256k1Key(), getPrimaryIPAddr(), Port(34023), quicEnabled = false ) - node2 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(34023)) node1.mountMetadata(0, @[0'u16]).expect("Mounted Waku Metadata") node2.mountMetadata(0, @[0'u16]).expect("Mounted Waku Metadata") @@ -349,6 +356,7 @@ procSuite "Peer Manager": parseIpAddress("127.0.0.1"), Port(56037), peerStorage = storage, + quicEnabled = false, ) node3.mountMetadata(0, @[0'u16]).expect("Mounted Waku Metadata") @@ -380,10 +388,17 @@ procSuite "Peer Manager": let database = SqliteDatabase.new(":memory:")[] storage = WakuPeerStorage.new(database)[] + # tcp-only: asserts exact AddressBook contents node1 = newTestWakuNode( - generateSecp256k1Key(), getPrimaryIPAddr(), Port(44048), peerStorage = storage + generateSecp256k1Key(), + getPrimaryIPAddr(), + Port(44048), + peerStorage = storage, + quicEnabled = false, + ) + node2 = newTestWakuNode( + generateSecp256k1Key(), getPrimaryIPAddr(), Port(34023), quicEnabled = false ) - node2 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(34023)) node1.mountMetadata(0, @[0'u16]).expect("Mounted Waku Metadata") node2.mountMetadata(0, @[0'u16]).expect("Mounted Waku Metadata") @@ -422,6 +437,7 @@ procSuite "Peer Manager": parseIpAddress("127.0.0.1"), Port(56037), peerStorage = storage, + quicEnabled = false, ) node3.mountMetadata(0, @[0'u16]).expect("Mounted Waku Metadata") @@ -1234,8 +1250,8 @@ procSuite "Peer Manager": asyncTest "Retrieve peer that mounted peer exchange": let - node1 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(55048)) - node2 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(55023)) + node1 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(0)) + node2 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(0)) await allFutures(node1.start(), node2.start()) await allFutures(node1.mountRelay(), node2.mountRelay()) diff --git a/tests/test_waku_netconfig.nim b/tests/test_waku_netconfig.nim index 7e8cc1a2b..08d153ee7 100644 --- a/tests/test_waku_netconfig.nim +++ b/tests/test_waku_netconfig.nim @@ -130,6 +130,60 @@ suite "Waku NetConfig": netConfig.announcedAddresses.len == 2 # Bind address + extAddress netConfig.announcedAddresses[1] == extMultiAddrs[0] + asyncTest "Operator QUIC extMultiAddr suppresses the auto QUIC address": + let + conf = defaultTestWakuConf() + bindPort = conf.endpointConf.p2pTcpPort + operatorQuic = ipQuicEndPoint(parseIpAddress("1.2.3.4"), Port(9999)) + + # no operator quic addr: node auto-announces its bind-port quic addr + let autoRes = NetConfig.init( + bindIp = conf.endpointConf.p2pListenAddress, + bindPort = bindPort, + quicEnabled = true, + quicBindPort = some(bindPort), + ) + assert autoRes.isOk(), $autoRes.error + check autoRes.get().announcedAddresses.filterIt(it.isQuicAddress()).len == 1 + + # operator quic addr given: auto bind-port addr suppressed, only theirs remains + let opRes = NetConfig.init( + bindIp = conf.endpointConf.p2pListenAddress, + bindPort = bindPort, + quicEnabled = true, + quicBindPort = some(bindPort), + extMultiAddrs = @[operatorQuic], + ) + assert opRes.isOk(), $opRes.error + let opNetConfig = opRes.get() + check: + opNetConfig.announcedAddresses.filterIt(it.isQuicAddress()) == @[operatorQuic] + opNetConfig.enrMultiaddrs.filterIt(it.isQuicAddress()) == @[operatorQuic] + + asyncTest "Announced QUIC address uses the NAT-mapped external port, not the bind port": + let + conf = defaultTestWakuConf() + extIp = parseIpAddress("1.2.3.4") + quicBindPort = Port(60000) + natQuicPort = Port(9999) # external UDP port a NAT (UPnP/PMP) mapped for us + + let netConfigRes = NetConfig.init( + bindIp = conf.endpointConf.p2pListenAddress, + bindPort = conf.endpointConf.p2pTcpPort, + extIp = some(extIp), + extPort = some(Port(1234)), + quicEnabled = true, + quicBindPort = some(quicBindPort), + extQuicPort = some(natQuicPort), + ) + assert netConfigRes.isOk(), $netConfigRes.error + + let quicAddrs = netConfigRes.get().announcedAddresses.filterIt(it.isQuicAddress()) + check: + quicAddrs.len == 1 + ("/udp/" & $(natQuicPort.uint16) & "/quic-v1") in $quicAddrs[0] + ("/udp/" & $(quicBindPort.uint16) & "/quic-v1") notin $quicAddrs[0] + asyncTest "AnnouncedAddresses uses dns4DomainName over extIp when both are provided": let conf = defaultTestWakuConf() @@ -441,6 +495,8 @@ suite "Waku NetConfig": let netConfigRes = NetConfig.init( bindIp = conf.endpointConf.p2pListenAddress, bindPort = conf.endpointConf.p2pTcpPort, + quicEnabled = true, + quicBindPort = some(Port(60000)), extMultiAddrs = extMultiAddrs, extMultiAddrsOnly = true, ) @@ -452,3 +508,4 @@ suite "Waku NetConfig": check: netConfig.announcedAddresses.len == 1 # ExtAddress netConfig.announcedAddresses[0] == extMultiAddrs[0] + netConfig.announcedAddresses.filterIt(it.isQuicAddress()).len == 0 diff --git a/tests/test_wakunode.nim b/tests/test_wakunode.nim index e52a8089f..902ca7192 100644 --- a/tests/test_wakunode.nim +++ b/tests/test_wakunode.nim @@ -10,6 +10,8 @@ import libp2p/crypto/secp, libp2p/multiaddress, libp2p/switch, + libp2p/muxers/muxer, + libp2p/stream/connection, libp2p/protocols/pubsub/rpc/messages, libp2p/protocols/pubsub/pubsub, libp2p/protocols/pubsub/gossipsub, @@ -122,10 +124,7 @@ suite "WakuNode": maxConnections = 20 nodeKey1 = generateSecp256k1Key() node1 = newTestWakuNode( - nodeKey1, - parseIpAddress("127.0.0.1"), - Port(60010), - maxConnections = maxConnections, + nodeKey1, parseIpAddress("127.0.0.1"), Port(0), maxConnections = maxConnections ) # Initialize and start node1 @@ -137,11 +136,10 @@ suite "WakuNode": var otherNodes: seq[WakuNode] = @[] # Create and start 20 other nodes - for i in 0 ..< maxConnections + 1: + for _ in 0 ..< maxConnections + 1: let nodeKey = generateSecp256k1Key() - port = 60012 + i * 2 # Ensure unique ports for each node - node = newTestWakuNode(nodeKey, parseIpAddress("127.0.0.1"), Port(port)) + node = newTestWakuNode(nodeKey, parseIpAddress("127.0.0.1"), Port(0)) await node.start() (await node.mountRelay()).isOkOr: assert false, "Failed to mount relay" @@ -187,7 +185,9 @@ suite "WakuNode": bindPort = Port(61006) extIp = some(getPrimaryIPAddr()) extPort = some(Port(61008)) - node = newTestWakuNode(nodeKey, bindIp, bindPort, extIp, extPort) + # tcp-only: asserts exact single listen addr; quic variant below + node = + newTestWakuNode(nodeKey, bindIp, bindPort, extIp, extPort, quicEnabled = false) let bindEndpoint = MultiAddress.init(bindIp, tcpProtocol, bindPort) @@ -216,6 +216,80 @@ suite "WakuNode": await node.stop() + asyncTest "Peer info updates with correct announced addresses (QUIC)": + # dual-stack node announces both tcp and quic-v1 + let + nodeKey = generateSecp256k1Key() + bindIp = parseIpAddress("0.0.0.0") + bindPort = Port(61006) + extIp = some(getPrimaryIPAddr()) + extPort = some(Port(61008)) + node = + newTestWakuNode(nodeKey, bindIp, bindPort, extIp, extPort, quicEnabled = true) + + let tcpAnnounced = MultiAddress.init(extIp.get(), tcpProtocol, extPort.get()) + + check: + node.announcedAddresses.len >= 2 + node.announcedAddresses.contains(tcpAnnounced) + node.announcedAddresses.anyIt("/quic-v1" in $it) + + await node.start() + + check: + node.started + node.switch.peerInfo.addrs.len >= 2 + node.switch.peerInfo.addrs.contains(tcpAnnounced) + node.switch.peerInfo.addrs.anyIt("/quic-v1" in $it) + + await node.stop() + + test "toRemotePeerInfo sorts quic-v1 addresses first": + let + nodeKey = generateSecp256k1Key() + node = + newTestWakuNode(nodeKey, parseIpAddress("0.0.0.0"), Port(0), quicEnabled = true) + + # peerinfo path + let fromPeerInfo = node.switch.peerInfo.toRemotePeerInfo() + check: + fromPeerInfo.addrs.anyIt("/quic-v1" in $it) + "/quic-v1" in $fromPeerInfo.addrs[0] + + # enr path + let fromEnr = toRemotePeerInfo(node.enr).valueOr: + raiseAssert "toRemotePeerInfo(enr) failed: " & $error + check: + fromEnr.addrs.anyIt("/quic-v1" in $it) + "/quic-v1" in $fromEnr.addrs[0] + + asyncTest "Dual-stack nodes connect over QUIC": + let + nodeKey1 = generateSecp256k1Key() + node1 = newTestWakuNode( + nodeKey1, parseIpAddress("0.0.0.0"), Port(0), quicEnabled = true + ) + nodeKey2 = generateSecp256k1Key() + node2 = newTestWakuNode( + nodeKey2, parseIpAddress("0.0.0.0"), Port(0), quicEnabled = true + ) + + await allFutures(node1.start(), node2.start()) + + await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) + + let conns = node1.switch.connManager.getConnections().getOrDefault( + node2.switch.peerInfo.peerId + ) + check conns.len >= 1 + let obAddr = conns[0].connection.observedAddr.valueOr: + raiseAssert "connection has no observed address" + check: + "/udp/" in $obAddr + "/tcp/" notin $obAddr + + await allFutures(node1.stop(), node2.stop()) + asyncTest "Node can use dns4 in announced addresses": let nodeKey = generateSecp256k1Key() @@ -231,7 +305,8 @@ suite "WakuNode": ) check: - node.announcedAddresses.len == 1 + # dual-stack also adds a dns4 quic addr, don't assert exact count + node.announcedAddresses.len >= 1 node.announcedAddresses.contains(expectedDns4Addr) asyncTest "Node uses dns4 resolved ip in announced addresses if no extIp is provided": diff --git a/tests/testlib/wakunode.nim b/tests/testlib/wakunode.nim index aeb7037ee..65b165a08 100644 --- a/tests/testlib/wakunode.nim +++ b/tests/testlib/wakunode.nim @@ -61,6 +61,8 @@ proc newTestWakuNode*( wsBindPort: Port = (Port) 8000, wsEnabled: bool = false, wssEnabled: bool = false, + quicBindPort: Port = (Port) 0, + quicEnabled: bool = true, secureKey: string = "", secureCert: string = "", wakuFlags = none(CapabilitiesBitfield), @@ -75,6 +77,17 @@ proc newTestWakuNode*( ): WakuNode = logging.setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT) + # quic won't bind 0.0.0.0 wildcard, use loopback for tests + let bindIp = + if quicEnabled and $bindIp == "0.0.0.0": + parseIpAddress("127.0.0.1") + else: + bindIp + + # reuse bindPort for quic so the enr addr is dialable; port 0 would advertise /udp/0 + let quicBindPort = + if quicEnabled and quicBindPort == Port(0): bindPort else: quicBindPort + var resolvedExtIp = extIp # Update extPort to default value if it's missing and there's an extIp or a DNS domain @@ -106,6 +119,8 @@ proc newTestWakuNode*( wsBindPort = some(wsBindPort), wsEnabled = wsEnabled, wssEnabled = wssEnabled, + quicBindPort = some(quicBindPort), + quicEnabled = quicEnabled, dns4DomainName = dns4DomainName, discv5UdpPort = discv5UdpPort, wakuFlags = wakuFlags, diff --git a/tests/waku_relay/test_wakunode_relay.nim b/tests/waku_relay/test_wakunode_relay.nim index 28d35e883..311454e22 100644 --- a/tests/waku_relay/test_wakunode_relay.nim +++ b/tests/waku_relay/test_wakunode_relay.nim @@ -400,7 +400,10 @@ suite "WakuNode - Relay": asyncTest "Messages relaying fails with non-overlapping transports (TCP or Websockets)": let nodeKey1 = generateSecp256k1Key() - node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), bindPort = Port(0)) + # tcp/ws isolation: node1 tcp, node2 ws, no shared transport. quic off or they'd share it. + node1 = newTestWakuNode( + nodeKey1, parseIpAddress("0.0.0.0"), bindPort = Port(0), quicEnabled = false + ) nodeKey2 = generateSecp256k1Key() node2 = newTestWakuNode( nodeKey2, @@ -408,6 +411,7 @@ suite "WakuNode - Relay": bindPort = Port(0), wsBindPort = Port(0), wsEnabled = true, + quicEnabled = false, ) shard = DefaultRelayShard contentTopic = ContentTopic("/waku/2/default-content/proto") diff --git a/tests/wakunode2/test_app.nim b/tests/wakunode2/test_app.nim index c59802ba3..eefa866db 100644 --- a/tests/wakunode2/test_app.nim +++ b/tests/wakunode2/test_app.nim @@ -2,7 +2,7 @@ import logos_delivery/waku/compat/option_valueor {.used.} import - std/json, + std/[json, net, sequtils, strutils], testutils/unittests, chronicles, chronos, @@ -142,3 +142,31 @@ suite "Wakunode2 - Waku initialization": parsed["rest"].getInt() != 0 parsed["discv5Udp"].getInt() != 0 parsed["metrics"].getInt() != 0 + + test "QUIC port=0 auto-binds and advertises the real port": + var builder = defaultTestWakuConfBuilder() + builder.withP2pListenAddress(parseIpAddress("127.0.0.1")) + builder.withP2pTcpPort(Port(0)) + builder.quicConf.withEnabled(true) + builder.quicConf.withQuicPort(Port(0)) + + let conf = builder.build().valueOr: + raiseAssert error + check conf.quicConf.get().port == Port(0) + + var waku = (waitFor Waku.new(conf)).valueOr: + raiseAssert error + defer: + (waitFor waku.stop()).isOkOr: + raiseAssert error + + (waitFor waku.start()).isOkOr: + raiseAssert error + + let parsed = parseJson(waku.stateInfo.getNodeInfoItem(NodeInfoId.MyBoundPorts)) + check parsed["quic"].getInt() != 0 + + let quicAddrs = waku.node.announcedAddresses.filterIt("/quic-v1" in $it) + check: + quicAddrs.len >= 1 + quicAddrs.allIt("/udp/0/quic-v1" notin $it) diff --git a/tests/wakunode_rest/test_rest_admin.nim b/tests/wakunode_rest/test_rest_admin.nim index e1247fb62..81ef7e6ea 100644 --- a/tests/wakunode_rest/test_rest_admin.nim +++ b/tests/wakunode_rest/test_rest_admin.nim @@ -38,9 +38,9 @@ suite "Waku v2 Rest API - Admin": var client {.threadvar.}: RestClientRef asyncSetup: - node1 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(60600)) - node2 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(60602)) - node3 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(60604)) + node1 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(0)) + node2 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(0)) + node3 = newTestWakuNode(generateSecp256k1Key(), getPrimaryIPAddr(), Port(0)) let clusterId = 1.uint16 let shards: seq[uint16] = @[0] diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 98a6774d3..222f92e5f 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -714,6 +714,17 @@ hence would have reachability issues.""", name: "websocket-secure-cert-path" .}: string + ## quic config + quicSupport* {. + desc: "Enable QUIC transport: true|false", + defaultValue: false, + name: "quic-support" + .}: bool + + quicPort* {. + desc: "QUIC (UDP) listening port.", defaultValue: 60000, name: "quic-port" + .}: Port + ## Rate limitation config, if not set, rate limit checks will not be performed rateLimits* {. desc: @@ -1160,6 +1171,9 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.webSocketConf.withKeyPath(n.websocketSecureKeyPath) b.webSocketConf.withCertPath(n.websocketSecureCertPath) + b.quicConf.withEnabled(n.quicSupport) + b.quicConf.withQuicPort(n.quicPort) + if n.rateLimits.len > 0: b.rateLimitConf.withRateLimits(n.rateLimits)