Add nat simulation util

This commit is contained in:
Arnaud 2026-05-12 10:05:16 +04:00
parent deeb3f9ce5
commit b8dc03b4aa
No known key found for this signature in database
GPG Key ID: A6C7C781817146FA
3 changed files with 264 additions and 0 deletions

View File

@ -341,6 +341,14 @@ type
name: "nat-max-relays"
.}: int
natSimulation* {.
desc:
"Simulate NAT filtering behavior for testing: endpoint-independent, address-dependent, address-and-port-dependent",
defaultValue: string.none,
name: "nat-simulation",
hidden
.}: Option[string]
relay* {.
desc: "Enable circuit relay server (hop) - use on publicly reachable nodes only",
defaultValue: false,

View File

@ -0,0 +1,141 @@
{.push raises: [].}
import std/sequtils
import pkg/chronos
import pkg/results
import pkg/libp2p
import pkg/libp2p/transports/tcptransport
import pkg/libp2p/transports/transport
import pkg/libp2p/wire
type FilteringBehavior* = enum
EndpointIndependent
AddressDependent
AddressAndPortDependent
type NatRouter* = ref object
filtering*: FilteringBehavior
conntrack: seq[TransportAddress]
type NatTransport* = ref object of Transport
tcp: TcpTransport
router: NatRouter
proc fromString*(T: type FilteringBehavior, s: string): Result[FilteringBehavior, string] =
case s
of "endpoint-independent": ok(EndpointIndependent)
of "address-dependent": ok(AddressDependent)
of "address-and-port-dependent": ok(AddressAndPortDependent)
else: err("Unknown filtering behavior: " & s)
proc new*(T: type NatRouter, filtering: FilteringBehavior): T =
T(filtering: filtering)
proc setFiltering*(r: NatRouter, filtering: FilteringBehavior) =
r.filtering = filtering
r.conntrack = @[]
proc allowInbound(r: NatRouter, remote: TransportAddress): bool =
case r.filtering
of EndpointIndependent:
true
of AddressDependent:
r.conntrack.anyIt(
try:
it.address == remote.address
except ValueError:
false
)
of AddressAndPortDependent:
remote in r.conntrack
proc new*(
T: type NatTransport,
router: NatRouter,
upgrade: Upgrade,
flags: set[ServerFlags] = {},
): T =
let self = T(tcp: TcpTransport.new(flags, upgrade), upgrader: upgrade, router: router)
procCall Transport(self).initialize()
return self
method start*(
self: NatTransport, addrs: seq[MultiAddress]
) {.async: (raises: [LPError, transport.TransportError, CancelledError]).} =
await self.tcp.start(addrs)
self.addrs = self.tcp.addrs
self.running = true
self.onRunning.fire()
method stop*(self: NatTransport) {.async: (raises: []).} =
await self.tcp.stop()
self.running = false
self.onStop.fire()
method dial*(
self: NatTransport,
hostname: string,
address: MultiAddress,
peerId: Opt[PeerId] = Opt.none(PeerId),
): Future[Connection] {.async: (raises: [transport.TransportError, CancelledError]).} =
## establishes an outgoing TCP connection and records the remote address
## so it can connect back to us later
let conn = await self.tcp.dial(hostname, address)
if conn.observedAddr.isSome:
let transportAddr = initTAddress(conn.observedAddr.get)
if transportAddr.isOk:
self.router.conntrack.add(transportAddr.get)
return conn
proc dropAfterTimeout(conn: Connection) {.async: (raises: []).} =
# Hold the connection open long enough for the remote's dial to time out,
# then close it. This simulates a NAT that drops packets rather than RSTs
# them, which is what AutoNAT needs to detect NotReachable.
await noCancel sleepAsync(20.seconds)
await noCancel conn.close()
method accept*(
self: NatTransport
): Future[Connection] {.async: (raises: [transport.TransportError, CancelledError]).} =
## waits for an incoming TCP connection and applies the NAT filtering rules
while true:
let conn = await self.tcp.accept()
if self.router.filtering == EndpointIndependent:
return conn
if conn.observedAddr.isNone:
await conn.close()
continue
let transportAddr = initTAddress(conn.observedAddr.get)
if transportAddr.isErr:
await conn.close()
continue
if not self.router.allowInbound(transportAddr.get):
# Do not close immediately: let the remote's dial time out naturally,
# then clean up. Returning a fast RST would produce EDialRefused (Unknown)
# instead of EDialError (NotReachable) in AutoNAT.
asyncSpawn dropAfterTimeout(conn)
continue
return conn
method handles*(
self: NatTransport, address: MultiAddress
): bool {.gcsafe, raises: [].} =
## returns true if this transport handles the given address (TCP only)
if procCall Transport(self).handles(address):
if address.protocols.isOk:
return TCP.match(address)
proc withNatTransport*(
b: SwitchBuilder, router: NatRouter, flags: set[ServerFlags] = {}
): SwitchBuilder =
b.withTransport(
proc(config: TransportConfig): Transport =
NatTransport.new(router, config.upgr, flags)
)

View File

@ -0,0 +1,115 @@
import pkg/chronos
import ./helpers
import ../asynctest
import ../../storage/rng
import ../../storage/utils/natsimulation
const flags = {ServerFlags.ReuseAddr}
const listenAddr = "/ip4/127.0.0.1/tcp/0"
proc newSwitch(rng: Rng): Switch =
SwitchBuilder
.new()
.withRng(rng)
.withPrivateKey(PrivateKey.random(rng[]).get())
.withAddresses(@[MultiAddress.init(listenAddr).get()])
.withTcpTransport(flags)
.withNoise()
.withYamux()
.build()
proc newNatSwitch(router: NatRouter, rng: Rng): Switch =
SwitchBuilder
.new()
.withRng(rng)
.withPrivateKey(PrivateKey.random(rng[]).get())
.withAddresses(@[MultiAddress.init(listenAddr).get()])
.withNatTransport(router, flags)
.withNoise()
.withYamux()
.build()
asyncchecksuite "NatTransport - Endpoint-Independent Filtering":
var bootstrap, natNode: Switch
setup:
let router = NatRouter.new(EndpointIndependent)
bootstrap = newSwitch(Rng.instance())
natNode = newNatSwitch(router, Rng.instance())
await bootstrap.start()
await natNode.start()
teardown:
await bootstrap.stop()
await natNode.stop()
test "bootstrap can connect to nat node without any prior outbound":
await bootstrap.connect(natNode.peerInfo.peerId, natNode.peerInfo.addrs)
check bootstrap.isConnected(natNode.peerInfo.peerId)
asyncchecksuite "NatTransport - Address-Dependent Filtering":
var bootstrap, thirdNode, natNode: Switch
setup:
let router = NatRouter.new(AddressDependent)
bootstrap = newSwitch(Rng.instance())
thirdNode = newSwitch(Rng.instance())
natNode = newNatSwitch(router, Rng.instance())
await bootstrap.start()
await thirdNode.start()
await natNode.start()
teardown:
await bootstrap.stop()
await thirdNode.stop()
await natNode.stop()
test "bootstrap can connect to nat node with a pre-existing connection":
await natNode.connect(bootstrap.peerInfo.peerId, bootstrap.peerInfo.addrs)
check natNode.isConnected(bootstrap.peerInfo.peerId)
await bootstrap.connect(natNode.peerInfo.peerId, natNode.peerInfo.addrs)
check bootstrap.isConnected(natNode.peerInfo.peerId)
test "third node can connect to nat node after nat node connected to bootstrap":
await natNode.connect(bootstrap.peerInfo.peerId, bootstrap.peerInfo.addrs)
await thirdNode.connect(natNode.peerInfo.peerId, natNode.peerInfo.addrs)
check thirdNode.isConnected(natNode.peerInfo.peerId)
test "bootstrap cannot connect to nat node without a pre-existing connection":
expect(LPError):
await bootstrap.connect(natNode.peerInfo.peerId, natNode.peerInfo.addrs)
asyncchecksuite "NatTransport - Address-and-Port-Dependent Filtering":
var bootstrap, thirdNode, natNode: Switch
setup:
let router = NatRouter.new(AddressAndPortDependent)
bootstrap = newSwitch(Rng.instance())
thirdNode = newSwitch(Rng.instance())
natNode = newNatSwitch(router, Rng.instance())
await bootstrap.start()
await thirdNode.start()
await natNode.start()
teardown:
await bootstrap.stop()
await thirdNode.stop()
await natNode.stop()
test "bootstrap can connect to nat node with a pre-existing connection":
await natNode.connect(bootstrap.peerInfo.peerId, bootstrap.peerInfo.addrs)
check natNode.isConnected(bootstrap.peerInfo.peerId)
await bootstrap.connect(natNode.peerInfo.peerId, natNode.peerInfo.addrs)
check bootstrap.isConnected(natNode.peerInfo.peerId)
test "bootstrap cannot connect to nat node without a pre-existing connection":
expect(LPError):
await bootstrap.connect(natNode.peerInfo.peerId, natNode.peerInfo.addrs)
test "third node cannot connect to nat node even after nat node connected to bootstrap":
await natNode.connect(bootstrap.peerInfo.peerId, bootstrap.peerInfo.addrs)
expect(LPError):
await thirdNode.connect(natNode.peerInfo.peerId, natNode.peerInfo.addrs)