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
This commit is contained in:
Fabiana Cecin 2026-06-17 07:55:45 -03:00 committed by GitHub
parent 7e98489a24
commit c3090fb62f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 451 additions and 99 deletions

View File

@ -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),

View File

@ -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

View File

@ -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))))

View File

@ -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,

View File

@ -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),

View File

@ -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

View File

@ -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():

View File

@ -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]

View File

@ -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 =

View File

@ -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,

View File

@ -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()

View File

@ -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: @[],

View File

@ -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)

View File

@ -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())

View File

@ -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

View File

@ -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":

View File

@ -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,

View File

@ -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")

View File

@ -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)

View File

@ -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]

View File

@ -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)