mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-06-29 22:10:21 +00:00
Add nat simulation util
This commit is contained in:
parent
deeb3f9ce5
commit
b8dc03b4aa
@ -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,
|
||||
|
||||
141
storage/utils/natsimulation.nim
Normal file
141
storage/utils/natsimulation.nim
Normal 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)
|
||||
)
|
||||
115
tests/storage/testnatsimulation.nim
Normal file
115
tests/storage/testnatsimulation.nim
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user