Add custom address mapper to remap the port mapping by UPnP / PCP

This commit is contained in:
Arnaud 2026-06-15 14:55:04 +04:00
parent 1ec0651139
commit fdf5396e60
No known key found for this signature in database
GPG Key ID: A6C7C781817146FA
4 changed files with 135 additions and 0 deletions

View File

@ -165,6 +165,34 @@ proc setupPeerInfoObserver*(
switch.peerInfo.addObserver(observer)
observer
proc setupMappedAddrMapper*(switch: Switch, natMapper: NatPortMapper) =
## We define a custom mapper that adds the external port to peerInfo.addrs when
## a port mapping is active, so AutoNAT tests that port.
## PCP/NAT-PMP may grant an external port different from the listen port.
let mapper: AddressMapper = proc(
addrs: seq[MultiAddress]
): Future[seq[MultiAddress]] {.gcsafe, async: (raises: [CancelledError]).} =
result = addrs
if natMapper.activeTcpPort.isNone:
return result
let mappedPort = natMapper.activeTcpPort.get
for listenAddr in switch.peerInfo.listenAddrs:
# Dialable IP (observed public, or the listen IP if already public)
# used with the mapped port.
let mappedAddr = switch.peerStore.guessDialableAddr(listenAddr).remapAddr(
port = some(mappedPort)
)
if mappedAddr.isPublicMA():
# Insert first so AutoNAT dials it before the listen-port candidate (the
# server tests only the first dialable address).
result.insert(mappedAddr, 0)
return result.deduplicate()
switch.peerInfo.addressMappers.add(mapper)
method handleNatStatus*(
m: NatPortMapper,
networkReachability: NetworkReachability,

View File

@ -490,6 +490,8 @@ proc new*(
peerInfoObserver =
some(setupPeerInfoObserver(switch, autonatService.get, discovery, natMapper.get))
setupMappedAddrMapper(switch, natMapper.get)
autonatService.get.setStatusAndConfidenceHandler(
proc(
networkReachability: NetworkReachability,

View File

@ -30,6 +30,8 @@ const
discoveryPort = Port(8090)
# ms — AutoNAT probe + confidence + reaction
detectTimeout = 20000
mockMappedTcpPort = Port(40000)
mockMappedUdpPort = Port(40001)
type MockNatPortMapper = ref object of NatPortMapper
@ -40,6 +42,19 @@ method mapNatPorts*(
.} =
none((Port, Port, MappingProtocol))
# Simulates a successful PCP mapping
type MockMappingNatPortMapper = ref object of NatPortMapper
method mapNatPorts*(
m: MockMappingNatPortMapper
): Future[Option[(Port, Port, MappingProtocol)]] {.
async: (raises: [CancelledError]), gcsafe
.} =
m.activeTcpPort = some(mockMappedTcpPort)
m.activeUdpPort = some(mockMappedUdpPort)
m.activeMappingProtocol = some(MappingProtocol.PCP)
some((mockMappedTcpPort, mockMappedUdpPort, MappingProtocol.PCP))
# Captures the candidate addresses the service sends and answers Reachable, so
# the service flips to reachable and runs its address mapper — without dialing.
type MockAutonatV2Client = ref object of AutonatV2Client
@ -241,3 +256,59 @@ asyncchecksuite "NAT detection - dial request candidates":
await autonat.stop(sw)
await sw2.stop()
test "after a port mapping, the mapped address is AutoNAT's first dial candidate":
let mapper = MockMappingNatPortMapper()
setupMappedAddrMapper(sw, mapper)
# Reach the observation quorum so guessDialableAddr trusts 8.8.8.8
let observed = MultiAddress.init("/ip4/8.8.8.8/tcp/4001").expect("valid")
let quorum = 3
for _ in 0 ..< quorum:
discard sw.peerStore.identify.observedAddrManager.addObservation(observed)
# Setup AutoRelayService
let relay = AutoRelayService.new(
1, relayClientModule.RelayClient.new(), nil, Rng.instance().libp2pRng
)
autorelayservice.setup(relay, sw)
# Define our handleNatStatus callback
let disc = Discovery.new(
PrivateKey.random(Rng.instance().libp2pRng).get(), announceAddrs = @[]
)
let dialBack = MultiAddress.init("/ip4/8.8.8.8/tcp/8080").expect("valid")
await mapper.handleNatStatus(
NotReachable, Opt.some(dialBack), discoveryPort, disc, sw, relay
)
# Define our AutonatV2Service
let mockClient = MockAutonatV2Client()
let autonat = AutonatV2Service.new(
Rng.instance().libp2pRng,
mockClient,
AutonatV2ServiceConfig.new(
enableDialableCandidates = true, maxQueueSize = 1, minConfidence = 0.5
),
)
service.setup(autonat, sw)
await autonat.start(sw)
# Connect to a second switch to test NAT detection
let sw2 = newStandardSwitch()
await sw2.start()
await sw.connect(sw2.peerInfo.peerId, sw2.peerInfo.addrs)
# The expected mapped address should be the guessDialableAddr (8.8.8.8)
# using the mapping mocked port (40000) because a mapping was created.
let mapped =
MultiAddress.init("/ip4/8.8.8.8/tcp/" & $mockMappedTcpPort).expect("valid")
check eventually(mapped in mockClient.reqAddrs)
# Ensute that it comes first (because AutonatV2 test only the first candidate)
check mockClient.reqAddrs[0] == mapped
await autonat.stop(sw)
await sw2.stop()
if relay.isRunning:
await relay.stop(sw)

View File

@ -5,6 +5,7 @@ import pkg/libp2p/protocols/connectivity/autonat/types
import pkg/libp2p/protocols/connectivity/autonatv2/service except setup
import pkg/libp2p/protocols/connectivity/relay/client as relayClientModule
import pkg/libp2p/services/autorelayservice except setup
import pkg/libp2p/observedaddrmanager
import pkg/results
import ./helpers
@ -223,3 +224,36 @@ asyncchecksuite "NAT reaction - address announcing":
await sw.peerInfo.update()
check disc.announceAddrs == newSeq[MultiAddress]()
test "mapped-addr mapper injects the mapped port as the first candidate":
const mockMappedTcpPort = 40000
setupMappedAddrMapper(
sw, NatPortMapper(activeTcpPort: some(Port(mockMappedTcpPort)))
)
# Reach the observation quorum so guessDialableAddr trusts 8.8.8.8
let observed = MultiAddress.init("/ip4/8.8.8.8/tcp/4001").expect("valid")
let quorum = 3
for _ in 0 ..< quorum:
discard sw.peerStore.identify.observedAddrManager.addObservation(observed)
await sw.peerInfo.update()
# Ensure that the address mapper injects the mapped port as the first candidate
# after peer info update
check sw.peerInfo.addrs[0] ==
MultiAddress.init("/ip4/8.8.8.8/tcp/" & $mockMappedTcpPort).expect("valid")
test "mapped-addr mapper is a no-op without an active mapping":
setupMappedAddrMapper(sw, NatPortMapper())
let observed = MultiAddress.init("/ip4/8.8.8.8/tcp/4001").expect("valid")
let quorum = 3
for _ in 0 ..< quorum:
discard sw.peerStore.identify.observedAddrManager.addObservation(observed)
await sw.peerInfo.update()
# Ensure that nothing is injected because there is no active mapping
check sw.peerInfo.addrs == sw.peerInfo.listenAddrs