logos-storage-nim/storage/utils/natsimulation.nim

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

190 lines
5.4 KiB
Nim
Raw Normal View History

2026-05-25 17:00:49 +04:00
# NAT simulation for integration testing.
#
# Testing NAT traversal in CI requires controlling inbound/outbound filtering
# rules, which is not possible with real network interfaces. This module wraps
# the TCP transport to enforce configurable filtering behaviors (endpoint-
# independent, address-dependent, address-and-port-dependent, double NAT) at
# the connection level, so the full AutoNAT detection and relay
# stack can be exercised without actual NAT hardware.
2026-05-12 10:05:16 +04:00
{.push raises: [].}
2026-05-22 21:14:06 +04:00
import std/[options, sequtils]
2026-05-12 10:05:16 +04:00
import pkg/chronos
2026-05-25 12:08:49 +04:00
import pkg/chronicles
2026-05-12 10:05:16 +04:00
import pkg/results
import pkg/libp2p
import pkg/libp2p/transports/tcptransport
import pkg/libp2p/transports/transport
import pkg/libp2p/wire
2026-05-22 21:14:06 +04:00
import ../nat
2026-05-25 17:00:49 +04:00
logScope:
topics = "nat simulation"
2026-05-12 10:05:16 +04:00
type FilteringBehavior* = enum
EndpointIndependent
AddressDependent
AddressAndPortDependent
2026-05-22 21:14:06 +04:00
DoubleNat
2026-05-12 10:05:16 +04:00
type NatRouter* = ref object
filtering*: FilteringBehavior
2026-05-25 11:23:28 +04:00
conntrack: seq[TransportAddress] # remote addrs we dialed; allows them to connect back
2026-05-22 21:14:06 +04:00
natMapper*: Option[NatPortMapper]
2026-05-12 10:05:16 +04:00
type NatTransport* = ref object of Transport
tcp: TcpTransport
router: NatRouter
2026-05-12 10:25:02 +04:00
proc fromString*(
T: type FilteringBehavior, s: string
): Result[FilteringBehavior, string] =
2026-05-12 10:05:16 +04:00
case s
2026-05-12 10:25:02 +04:00
of "endpoint-independent":
ok(EndpointIndependent)
of "address-dependent":
ok(AddressDependent)
of "address-and-port-dependent":
ok(AddressAndPortDependent)
2026-05-22 21:14:06 +04:00
of "double-nat":
ok(DoubleNat)
2026-05-12 10:25:02 +04:00
else:
err("Unknown filtering behavior: " & s)
2026-05-12 10:05:16 +04:00
2026-05-25 11:23:28 +04:00
proc new*(T: type NatRouter, filtering: FilteringBehavior): T =
T(filtering: filtering)
2026-05-12 10:05:16 +04:00
proc setFiltering*(r: NatRouter, filtering: FilteringBehavior) =
2026-05-25 17:00:49 +04:00
debug "NAT filtering changed", previous = r.filtering, next = filtering
2026-05-12 10:05:16 +04:00
r.filtering = filtering
r.conntrack = @[]
2026-05-22 21:14:06 +04:00
proc allowInbound(r: NatRouter, remote: TransportAddress, localPort: Port): bool =
2026-05-12 10:05:16 +04:00
case r.filtering
2026-05-22 21:14:06 +04:00
of DoubleNat:
2026-05-25 12:07:15 +04:00
return
false
# always blocks: simulates a scenario where inbound connections are never possible
2026-05-12 10:05:16 +04:00
of EndpointIndependent:
2026-05-22 21:14:06 +04:00
return true
else:
discard
if r.natMapper.isSome and r.natMapper.get.isPortMapped(localPort):
return true
case r.filtering
2026-05-12 10:05:16 +04:00
of AddressDependent:
r.conntrack.anyIt(
try:
it.address == remote.address
except ValueError:
false
)
of AddressAndPortDependent:
remote in r.conntrack
2026-05-22 21:14:06 +04:00
else:
false
2026-05-12 10:05:16 +04:00
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:
2026-05-25 11:23:28 +04:00
let remote = transportAddr.get
self.router.conntrack.add(remote)
proc cleanupConntrack() {.async: (raises: []).} =
await noCancel conn.closeEvent.wait()
self.router.conntrack.keepItIf(it != remote)
2026-05-12 10:05:16 +04:00
2026-05-25 11:23:28 +04:00
asyncSpawn cleanupConntrack()
2026-05-12 10:05:16 +04:00
2026-05-25 11:23:28 +04:00
return conn
2026-05-12 10:05:16 +04:00
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:
2026-05-25 12:08:49 +04:00
debug "Dropping inbound connection: invalid observed address",
address = conn.observedAddr.get
2026-05-12 10:05:16 +04:00
await conn.close()
continue
2026-05-22 21:14:06 +04:00
var localPort = Port(0)
2026-05-25 11:55:40 +04:00
if conn.localAddr.isSome:
# Local address read from the accepted socket.
let localAddr = initTAddress(conn.localAddr.get)
2026-05-22 21:14:06 +04:00
if localAddr.isOk:
localPort = localAddr.get.port
if not self.router.allowInbound(transportAddr.get, localPort):
2026-05-25 11:23:28 +04:00
# The rejected connection is not closed here: tcp.stop() closes all
# accepted TCP connections on teardown.
2026-05-12 10:05:16 +04:00
continue
2026-05-25 17:00:49 +04:00
debug "Inbound connection accepted",
remote = transportAddr.get, filtering = self.router.filtering
2026-05-12 10:05:16 +04:00
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)
)