Add relay integration and tests

This commit is contained in:
Arnaud 2026-04-22 12:21:43 +04:00
parent bc05f8154d
commit e48ca913a9
No known key found for this signature in database
GPG Key ID: A6C7C781817146FA
3 changed files with 183 additions and 31 deletions

View File

@ -18,6 +18,7 @@ import pkg/chronos
import pkg/chronicles
import pkg/libp2p
import pkg/libp2p/protocols/connectivity/autonatv2/service
import pkg/libp2p/services/autorelayservice
import ./utils
import ./utils/natutils
@ -66,12 +67,18 @@ type PrefSrcStatus = enum
type NatMapper* = ref object of RootObj
method mapNatAddresses*(
m: NatMapper, addrs: seq[MultiAddress], discoveryPort: Port
m: NatMapper, addrs: seq[MultiAddress]
): tuple[libp2p, discovery: seq[MultiAddress]] {.base, gcsafe, raises: [].} =
raiseAssert "mapNatAddresses not implemented"
method getReachableAddresses*(
m: NatMapper, addrs: seq[MultiAddress]
): tuple[libp2p, discovery: seq[MultiAddress]] {.base, gcsafe, raises: [].} =
raiseAssert "getReachableAddresses not implemented"
type DefaultNatMapper* = ref object of NatMapper
natConfig*: NatConfig
discoveryPort*: Port
## Also does threadvar initialisation.
## Must be called before redirectPorts() in each thread.
@ -451,29 +458,70 @@ proc nattedAddress*(
(newAddrs, discoveryAddrs)
method mapNatAddresses*(
m: DefaultNatMapper, addrs: seq[MultiAddress], discoveryPort: Port
m: DefaultNatMapper, addrs: seq[MultiAddress]
): tuple[libp2p, discovery: seq[MultiAddress]] {.gcsafe, raises: [].} =
nattedAddress(m.natConfig, addrs, discoveryPort)
nattedAddress(m.natConfig, addrs, m.discoveryPort)
method getReachableAddresses*(
m: DefaultNatMapper, addrs: seq[MultiAddress]
): tuple[libp2p, discovery: seq[MultiAddress]] {.gcsafe, raises: [].} =
let ip =
if m.natConfig.hasExtIp:
some(m.natConfig.extIp)
else:
let (routeIp, _) = getRoutePrefSrc(static parseIpAddress("0.0.0.0"))
routeIp
if ip.isNone:
return (@[], @[])
let announceAddrs =
addrs.mapIt(it.remapAddr(ip = ip, port = none(Port))).deduplicate()
(announceAddrs, @[getMultiAddrWithIPAndUDPPort(ip.get, m.discoveryPort)])
proc hasPublicIp*(addrs: seq[MultiAddress]): bool =
for addr in addrs:
let (ip, _) = getAddressAndPort(addr)
if ip.isSome and isGlobalUnicast(ip.get):
return true
proc handleNatStatus*(
networkReachability: NetworkReachability,
confidence: Opt[float],
mapper: NatMapper,
listenAddrs: seq[MultiAddress],
discoveryPort: Port,
discovery: Discovery,
switch: Switch,
autoRelayService: AutoRelayService,
) {.async: (raises: [CancelledError]).} =
debug "AutoNAT status", reachability = networkReachability, confidence
case networkReachability
of Reachable:
# TODO: switch DHT to server mode, stop relay if running
discard
of NotReachable:
let (announceAddrs, discoveryAddrs) =
mapper.mapNatAddresses(listenAddrs, discoveryPort)
discovery.updateAnnounceRecord(announceAddrs)
discovery.updateDhtRecord(announceAddrs & discoveryAddrs)
of Unknown:
# Nothing to do here, not enough confidence score result
discard
of Reachable:
# For UPnP, it the mapping was a success,
# the autorelay service has been stopped
# and the address was already announced
if autoRelayService.isRunning:
if not await autoRelayService.stop(switch):
debug "AutoRelayService stop method returned false"
let (announceAddrs, discoveryAddrs) =
mapper.getReachableAddresses(switch.peerInfo.addrs)
discovery.updateAnnounceRecord(announceAddrs)
discovery.updateDhtRecord(announceAddrs & discoveryAddrs)
# TODO: switch DHT to server mode
of NotReachable:
let (announceAddrs, discoveryAddrs) = mapper.mapNatAddresses(switch.peerInfo.addrs)
# With a UPnP / NatPmP successful mapping,
# we suppose that having a public IP make it Reachable.
# If not, the state will be updated in the next Autonat iteration.
# TODO: Do we need to manually call dialMe to make sure we are Reachable ?
if hasPublicIp(announceAddrs):
discovery.updateAnnounceRecord(announceAddrs)
discovery.updateDhtRecord(announceAddrs & discoveryAddrs)
if autoRelayService.isRunning:
if not await autoRelayService.stop(switch):
debug "AutoRelayService stop method returned false"
else:
if not autoRelayService.isRunning:
if not await autoRelayService.setup(switch):
debug "AutoRelayService setup method returned false"

View File

@ -18,6 +18,8 @@ import pkg/taskpools
import pkg/presto
import pkg/libp2p
import pkg/libp2p/protocols/connectivity/autonatv2/[service, client]
import pkg/libp2p/protocols/connectivity/relay/client as relayClientModule
import pkg/libp2p/services/autorelayservice
import pkg/confutils
import pkg/confutils/defs
import pkg/stew/io2
@ -54,6 +56,7 @@ type
maintenance: BlockMaintainer
taskpool: Taskpool
autonatService*: AutonatV2Service
autoRelayService: AutoRelayService
isStarted: bool
StoragePrivateKey* = libp2p.PrivateKey # alias
@ -195,6 +198,8 @@ proc new*(
## create StorageServer including setting up datastore, repostore, etc
let listenMultiAddr = getMultiAddrWithIpAndTcpPort(config.listenIp, config.listenPort)
let relayClient = relayClientModule.RelayClient.new()
let autonatClient = AutonatV2Client.new(random.Rng.instance())
let autonatService = AutonatV2Service.new(
rng = random.Rng.instance(),
@ -220,6 +225,7 @@ proc new*(
.withSignedPeerRecord(true)
.withTcpTransport({ServerFlags.ReuseAddr, ServerFlags.TcpNoDelay})
.withAutonatV2Server()
.withCircuitRelay(relayClient)
.withServices(@[Service(autonatService)])
.build()
@ -327,6 +333,16 @@ proc new*(
taskPool = taskPool,
)
autoRelayService = AutoRelayService.new(
maxNumRelays = config.natMaxRelays,
client = relayClient,
onReservation = proc(addresses: seq[MultiAddress]) {.gcsafe, raises: [].} =
debug "Relay reservation updated", addresses
discovery.updateAnnounceRecord(addresses)
discovery.updateDhtRecord(addresses),
rng = random.Rng.instance(),
)
var restServer: RestServerRef = nil
if config.apiBindAddress.isSome:
@ -344,14 +360,15 @@ proc new*(
switch.mount(network)
switch.mount(manifestProto)
let natMapper = DefaultNatMapper(natConfig: config.nat)
let natMapper =
DefaultNatMapper(natConfig: config.nat, discoveryPort: config.discoveryPort)
autonatService.setStatusAndConfidenceHandler(
proc(
networkReachability: NetworkReachability, confidence: Opt[float]
) {.async: (raises: [CancelledError]).} =
debug "AutoNAT status", reachability = networkReachability, confidence
await handleNatStatus(
networkReachability, confidence, natMapper, switch.peerInfo.addrs,
config.discoveryPort, discovery,
networkReachability, natMapper, discovery, switch, autoRelayService
)
)
@ -364,4 +381,5 @@ proc new*(
taskPool: taskPool,
logFile: logFile,
autonatService: autonatService,
autoRelayService: autoRelayService,
)

View File

@ -1,10 +1,15 @@
import std/[unittest, net]
import std/net
import pkg/chronos
import pkg/libp2p
import pkg/libp2p/[multiaddress, multihash, multicodec]
import pkg/libp2p/protocols/connectivity/autonatv2/service
import pkg/libp2p/protocols/connectivity/autonatv2/service except setup
import pkg/libp2p/protocols/connectivity/autonatv2/types
import pkg/libp2p/protocols/connectivity/relay/client as relayClientModule
import pkg/libp2p/services/autorelayservice except setup
import pkg/results
import ./helpers
import ../asynctest
import ../../storage/nat
import ../../storage/discovery
import ../../storage/rng
@ -15,7 +20,12 @@ type MockNatMapper = ref object of NatMapper
mapped: tuple[libp2p, discovery: seq[MultiAddress]]
method mapNatAddresses*(
m: MockNatMapper, addrs: seq[MultiAddress], discoveryPort: Port
m: MockNatMapper, addrs: seq[MultiAddress]
): tuple[libp2p, discovery: seq[MultiAddress]] {.raises: [].} =
m.mapped
method getReachableAddresses*(
m: MockNatMapper, addrs: seq[MultiAddress]
): tuple[libp2p, discovery: seq[MultiAddress]] {.raises: [].} =
m.mapped
@ -44,7 +54,6 @@ suite "NAT Address Tests":
MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"),
MultiAddress.init("/ip4/8.8.8.8/tcp/5000").expect("valid multiaddr"),
]
#ipv6Addr = MultiAddress.init("/ip6/::1/tcp/5000").expect("valid multiaddr")
addrs = @[localAddr, anyAddr, publicAddr]
@ -78,17 +87,94 @@ suite "setupAddress":
check tcpPort == some(Port(5000))
check udpPort == some(Port(5001))
suite "handleNatStatus":
let key = PrivateKey.random(Rng.instance[]).get()
suite "getReachableAddresses":
test "returns remapped addresses when extIp is configured":
let
natConfig = NatConfig(hasExtIp: true, extIp: parseIpAddress("1.2.3.4"))
mapper = DefaultNatMapper(natConfig: natConfig, discoveryPort: Port(8090))
listenAddr = MultiAddress.init("/ip4/0.0.0.0/tcp/5000").expect("valid")
test "NotReachable updates announce addresses":
let disc = Discovery.new(key, announceAddrs = @[])
let (libp2pAddrs, discAddrs) = mapper.getReachableAddresses(@[listenAddr])
check libp2pAddrs == @[MultiAddress.init("/ip4/1.2.3.4/tcp/5000").expect("valid")]
check discAddrs == @[MultiAddress.init("/ip4/1.2.3.4/udp/8090").expect("valid")]
suite "hasPublicIp":
test "hasPublicIp returns true when the address is public":
let ma = MultiAddress.init("/ip4/8.8.8.8/tcp/8080").expect("valid")
check hasPublicIp(@[ma])
test "hasPublicIp returns false when the address is private":
let ma = MultiAddress.init("/ip4/192.168.1.1/tcp/8080").expect("valid")
check not hasPublicIp(@[ma])
test "hasPublicIp returns false when the address is empty":
check not hasPublicIp(@[])
asyncchecksuite "handleNatStatus":
var sw: Switch
var key: PrivateKey
var disc: Discovery
let autoRelay =
AutoRelayService.new(1, relayClientModule.RelayClient.new(), nil, Rng.instance())
setup:
key = PrivateKey.random(Rng.instance[]).get()
disc = Discovery.new(key, announceAddrs = @[])
sw = newStandardSwitch()
await sw.start()
teardown:
await sw.stop()
if autoRelay.isRunning:
discard await autoRelay.stop(sw)
test "handleNatStatus announces address when the node is not Reachable and the UPnP succeed with public ip":
let announceAddr = MultiAddress.init("/ip4/1.2.3.4/tcp/8080").expect("valid")
let discAddr = MultiAddress.init("/ip4/1.2.3.4/udp/8090").expect("valid")
let mapper = MockNatMapper(mapped: (@[announceAddr], @[discAddr]))
waitFor handleNatStatus(
NotReachable, Opt.none(float), mapper, @[], Port(8090), disc
)
await handleNatStatus(NotReachable, mapper, disc, sw, autoRelay)
check disc.announceAddrs == @[announceAddr]
check not autoRelay.isRunning
# test "handleNatStatus does not announce address when the node is not Reachable and the UPnP succeed with private ip":
# let privateAddr = MultiAddress.init("/ip4/192.168.1.1/tcp/8080").expect("valid")
# let mapper = MockNatMapper(mapped: (@[privateAddr], @[]))
# await handleNatStatus(
# NotReachable, mapper, disc, sw, autoRelay
# )
# check disc.announceAddrs == @[]
# check not autoRelay.isRunning
test "handleNatStatus starts autoRelay when node is not Reachable and UPnP failed":
let mapper = MockNatMapper(mapped: (@[], @[]))
await handleNatStatus(NotReachable, mapper, disc, sw, autoRelay)
check autoRelay.isRunning
# The addresses will be announced in the onReservation callback
# after a node accepted a Relay reservation.
test "handleNatStatus does not announce address when node is Reachable and relay is not running":
let mapper = MockNatMapper(mapped: (@[], @[]))
await handleNatStatus(Reachable, mapper, disc, sw, autoRelay)
check disc.announceAddrs == newSeq[MultiAddress]()
check not autoRelay.isRunning
test "handleNatStatus stops relay and announces address when node is Reachable and relay is running":
let announceAddr = MultiAddress.init("/ip4/1.2.3.4/tcp/8080").expect("valid")
let discAddr = MultiAddress.init("/ip4/1.2.3.4/udp/8090").expect("valid")
let mapper = MockNatMapper(mapped: (@[announceAddr], @[discAddr]))
discard await autorelayservice.setup(autoRelay, sw)
await handleNatStatus(Reachable, mapper, disc, sw, autoRelay)
check not autoRelay.isRunning
check disc.announceAddrs == @[announceAddr]