mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-06-27 21:09:28 +00:00
Add relay download test
This commit is contained in:
parent
df40f4c6e3
commit
7edb8bf664
@ -22,6 +22,19 @@ proc compose*(composeFile, action: string) =
|
||||
let cmd = composeCmd(composeFile) & " " & action
|
||||
doAssert execShellCmd(cmd) == 0, "command failed: " & cmd
|
||||
|
||||
proc serviceLogs*(composeFile, service: string): string =
|
||||
## Current logs (stdout+stderr) of a compose service, or "" on error.
|
||||
try:
|
||||
let
|
||||
cmd = composeCmd(composeFile) & " logs " & service
|
||||
(output, code) = execCmdEx(cmd)
|
||||
if code != 0:
|
||||
echo "warning: '", cmd, "' exited ", code
|
||||
output
|
||||
except CatchableError as e:
|
||||
echo "could not read logs for ", service, ": ", e.msg
|
||||
""
|
||||
|
||||
proc saveContainerLogs*(
|
||||
composeFile, suiteName, testName, startTime: string, services: openArray[string]
|
||||
) =
|
||||
@ -30,12 +43,26 @@ proc saveContainerLogs*(
|
||||
## <testName>/<service>.log. Must run before `down` destroys the containers.
|
||||
for service in services:
|
||||
try:
|
||||
let
|
||||
logFile = getLogFile("", startTime, suiteName, testName, service)
|
||||
cmd = composeCmd(composeFile) & " logs " & service
|
||||
(output, code) = execCmdEx(cmd)
|
||||
if code != 0:
|
||||
echo "warning: '", cmd, "' exited ", code
|
||||
writeFile(logFile, output)
|
||||
let logFile = getLogFile("", startTime, suiteName, testName, service)
|
||||
writeFile(logFile, serviceLogs(composeFile, service))
|
||||
except CatchableError as e:
|
||||
echo "could not save logs for ", service, ": ", e.msg
|
||||
|
||||
template eventuallyInfo*(client, predicate: untyped): bool =
|
||||
## Poll `client.info()` until `predicate` holds, swallowing HttpError while the
|
||||
## node's API is still starting. The decoded info JsonNode is exposed as `info`
|
||||
## inside `predicate`.
|
||||
eventuallySafe(
|
||||
block:
|
||||
var satisfied = false
|
||||
try:
|
||||
let res = await client.info()
|
||||
if res.isOk:
|
||||
let info {.inject.} = res.get
|
||||
satisfied = predicate
|
||||
except HttpError:
|
||||
discard
|
||||
satisfied,
|
||||
timeout = 300000,
|
||||
pollInterval = 5000,
|
||||
)
|
||||
|
||||
43
tests/integration/nat/relay-download/README.md
Normal file
43
tests/integration/nat/relay-download/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# NAT relay-download scenario
|
||||
|
||||
## Scenario
|
||||
|
||||
A node behind a NAT falls back to bootstrap A's relay and announces its circuit
|
||||
address. A reachable node C finds it as a provider and downloads its data
|
||||
through the relay.
|
||||
|
||||
## Topology
|
||||
|
||||
```
|
||||
node B ──── lan ──── router (NAT) ──── wan ──── bootstrap A (relay)
|
||||
└────── node C (reachable)
|
||||
```
|
||||
|
||||
- **bootstrap A** — public node on the wan, autonat + relay server, started with
|
||||
`--nat=extip`.
|
||||
- **router** — `lan -> wan` masquerade and *no* inbound forward, so B can dial
|
||||
out but nothing can dial back in.
|
||||
- **node B** — `nat=auto`, on the lan. AutoNAT finds it unreachable, so it takes
|
||||
a relay reservation on A and announces its circuit address.
|
||||
- **node C** — `nat=auto`, directly on the wan, so AutoNAT finds it
|
||||
`Reachable`. It is the peer that downloads from B through the relay.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
make testNatIntegration \
|
||||
STORAGE_INTEGRATION_TEST_INCLUDES=tests/integration/nat/relay-download/testrelaydownload.nim
|
||||
```
|
||||
|
||||
Builds the shared image and brings the compose topology up and down. Rootless, but
|
||||
needs the host netfilter modules — if the router fails on iptables:
|
||||
`sudo modprobe iptable_nat nf_conntrack`.
|
||||
|
||||
## Expected result
|
||||
|
||||
B is `NotReachable` and announces its circuit address, while C is `Reachable`.
|
||||
B uploads a file, then C fetches it over the network through the relay and gets
|
||||
the same content back.
|
||||
|
||||
Per-run container logs (router, bootstrap, client, node) are written before teardown to
|
||||
`tests/integration/logs/<timestamp>__NAT_relay_download/<test>/<service>.log`.
|
||||
113
tests/integration/nat/relay-download/compose.yml
Normal file
113
tests/integration/nat/relay-download/compose.yml
Normal file
@ -0,0 +1,113 @@
|
||||
# A node behind a NAT falls back to bootstrap A's relay and announces its
|
||||
# circuit address, so a reachable node C can download from it through the relay.
|
||||
# Same real iptables NAT as not-reachable, with C added as the downloader. Run
|
||||
# via testrelaydownload.nim.
|
||||
#
|
||||
# node B ──── lan ──── router (NAT) ──── wan ──── bootstrap A (relay)
|
||||
# └────── node C (reachable)
|
||||
name: nat-relay-download
|
||||
|
||||
# Topology addresses, named for their role (defined once, referenced below).
|
||||
x-addresses:
|
||||
# fake public internet; a routable range so B looks public to A
|
||||
wan_subnet: &wan_subnet 7.7.7.0/24
|
||||
# private network behind the NAT
|
||||
lan_subnet: &lan_subnet 10.99.0.0/24
|
||||
# A: public bootstrap, autonat + relay server
|
||||
bootstrap_ip: &bootstrap_ip 7.7.7.10
|
||||
# C: public node on the wan, reachable, the one that downloads from B
|
||||
client_ip: &client_ip 7.7.7.20
|
||||
# router's public face
|
||||
router_wan_ip: &router_wan_ip 7.7.7.2
|
||||
# router's private face = B's gateway
|
||||
router_lan_ip: &router_lan_ip 10.99.0.2
|
||||
# B, behind the NAT
|
||||
node_ip: &node_ip 10.99.0.10
|
||||
|
||||
networks:
|
||||
wan:
|
||||
# Keep the fake public range private, not exposed to the host
|
||||
internal: true
|
||||
ipam:
|
||||
config:
|
||||
- subnet: *wan_subnet
|
||||
lan:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: *lan_subnet
|
||||
|
||||
services:
|
||||
router:
|
||||
image: localhost/storage-nat
|
||||
cap_add: [NET_ADMIN]
|
||||
sysctls:
|
||||
net.ipv4.ip_forward: 1
|
||||
networks:
|
||||
wan:
|
||||
ipv4_address: *router_wan_ip
|
||||
lan:
|
||||
ipv4_address: *router_lan_ip
|
||||
environment:
|
||||
ROUTER_WAN_IP: *router_wan_ip
|
||||
LAN_SUBNET: *lan_subnet
|
||||
# scripts mounted, so editing them needs no image rebuild
|
||||
volumes:
|
||||
- ../router-common.sh:/scripts/router-common.sh:ro,z
|
||||
- ./router-entrypoint.sh:/scripts/router-entrypoint.sh:ro,z
|
||||
entrypoint: ["bash", "/scripts/router-entrypoint.sh"]
|
||||
|
||||
bootstrap:
|
||||
image: localhost/storage-nat
|
||||
networks:
|
||||
wan:
|
||||
ipv4_address: *bootstrap_ip
|
||||
entrypoint: ["/app/build/storage"]
|
||||
command:
|
||||
- --listen-ip=0.0.0.0
|
||||
- --api-bindaddr=0.0.0.0
|
||||
- --listen-port=8070
|
||||
- --disc-port=8090
|
||||
- --api-port=8080
|
||||
# bootstrap_ip (anchors can't go inside a string)
|
||||
- --nat=extip:7.7.7.10
|
||||
- --relay-server
|
||||
- --autonat-server
|
||||
- --no-bootstrap-node
|
||||
- --data-dir=/data
|
||||
- --log-level=DEBUG
|
||||
|
||||
# C sits on the wan, directly reachable: no NAT, so it leaves ROUTER_LAN_IP
|
||||
# unset and keeps its default route (see node-entrypoint.sh).
|
||||
client:
|
||||
image: localhost/storage-nat
|
||||
depends_on: [bootstrap]
|
||||
networks:
|
||||
wan:
|
||||
ipv4_address: *client_ip
|
||||
# C's API, published so the test can drive the download from C and poll it
|
||||
ports:
|
||||
- "127.0.0.1:18087:8080"
|
||||
environment:
|
||||
# C fetches A's SPR from this API at startup to join the network (bootstrap_ip)
|
||||
BOOTSTRAP_API: http://7.7.7.10:8080
|
||||
volumes:
|
||||
- ../node-entrypoint.sh:/scripts/node-entrypoint.sh:ro,z
|
||||
entrypoint: ["bash", "/scripts/node-entrypoint.sh"]
|
||||
|
||||
node:
|
||||
image: localhost/storage-nat
|
||||
cap_add: [NET_ADMIN]
|
||||
depends_on: [router, bootstrap]
|
||||
networks:
|
||||
lan:
|
||||
ipv4_address: *node_ip
|
||||
# B's API, published so the test can upload to it and poll it
|
||||
ports:
|
||||
- "127.0.0.1:18086:8080"
|
||||
environment:
|
||||
ROUTER_LAN_IP: *router_lan_ip
|
||||
# B fetches A's SPR from this API at startup to join the network (bootstrap_ip)
|
||||
BOOTSTRAP_API: http://7.7.7.10:8080
|
||||
volumes:
|
||||
- ../node-entrypoint.sh:/scripts/node-entrypoint.sh:ro,z
|
||||
entrypoint: ["bash", "/scripts/node-entrypoint.sh"]
|
||||
7
tests/integration/nat/relay-download/router-entrypoint.sh
Executable file
7
tests/integration/nat/relay-download/router-entrypoint.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source "$(dirname "$0")/router-common.sh"
|
||||
|
||||
echo "router ready (wan iface $wanif)"
|
||||
|
||||
hold_until_stopped
|
||||
72
tests/integration/nat/relay-download/testrelaydownload.nim
Normal file
72
tests/integration/nat/relay-download/testrelaydownload.nim
Normal file
@ -0,0 +1,72 @@
|
||||
## NAT relay-download scenario — a node behind a NAT can be downloaded from
|
||||
## through the relay.
|
||||
##
|
||||
## Same shape as the not-reachable test: compose.yml brings up a real NAT
|
||||
## topology with bootstrap A running the relay server. B stays NotReachable,
|
||||
## falls back to the relay and announces its circuit address, so a reachable
|
||||
## peer C can fetch its data through the relay.
|
||||
##
|
||||
## Requires podman-compose and the scenario image:
|
||||
## podman build -t localhost/storage-nat \
|
||||
## -f tests/integration/nat/Dockerfile .
|
||||
|
||||
import std/[json, os, sequtils, strutils, times]
|
||||
import pkg/chronos
|
||||
import pkg/questionable/results
|
||||
|
||||
import ../../../asynctest
|
||||
import ../../../checktest
|
||||
import ../../storageclient
|
||||
import ../composehelper
|
||||
|
||||
proc announcesCircuitAddr(info: JsonNode): bool =
|
||||
## A node behind the relay announces its circuit (p2p-circuit) address.
|
||||
info{"announceAddresses"}.getElems.anyIt("p2p-circuit" in it.getStr)
|
||||
|
||||
asyncchecksuite "NAT relay download":
|
||||
let
|
||||
composeFile = currentSourcePath.parentDir / "compose.yml"
|
||||
nodeApiUrl = "http://127.0.0.1:18086/api/storage/v1"
|
||||
clientApiUrl = "http://127.0.0.1:18087/api/storage/v1"
|
||||
suiteName = "NAT relay download"
|
||||
testName = "a NAT'd node behind a relay can be downloaded from"
|
||||
services = ["router", "bootstrap", "client", "node"]
|
||||
startTime = now().format("yyyy-MM-dd'_'HH:mm:ss")
|
||||
var
|
||||
nodeClient: StorageClient
|
||||
clientC: StorageClient
|
||||
|
||||
setup:
|
||||
compose(composeFile, "up -d")
|
||||
nodeClient = StorageClient.new(nodeApiUrl)
|
||||
clientC = StorageClient.new(clientApiUrl)
|
||||
|
||||
teardown:
|
||||
await nodeClient.close()
|
||||
await clientC.close()
|
||||
saveContainerLogs(composeFile, suiteName, testName, startTime, services)
|
||||
compose(composeFile, "down -v")
|
||||
|
||||
test testName:
|
||||
# B is NotReachable and falls back to the relay, announcing its circuit address
|
||||
check eventuallyInfo(
|
||||
nodeClient,
|
||||
info{"nat"}{"reachability"}.getStr == "NotReachable" and
|
||||
info.announcesCircuitAddr(),
|
||||
)
|
||||
|
||||
let info = (await nodeClient.info()).get
|
||||
# Double check B announces only its circuit address
|
||||
check info.announcesCircuitAddr()
|
||||
|
||||
# C is reachable
|
||||
check eventuallyInfo(clientC, info{"nat"}{"reachability"}.getStr == "Reachable")
|
||||
|
||||
# B uploads a file
|
||||
let content = "hello from behind the relay"
|
||||
let cid = (await nodeClient.upload(content)).get
|
||||
|
||||
# C downloads it through the relay and gets the same content back
|
||||
let res = await clientC.download(cid)
|
||||
check res.isOk
|
||||
check res.get == content
|
||||
Loading…
x
Reference in New Issue
Block a user