mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-07-17 06:39:57 +00:00
Add PCP tests
This commit is contained in:
parent
1692ae7df2
commit
cc6135b1a1
64
tests/integration/nat/pcp/README.md
Normal file
64
tests/integration/nat/pcp/README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# NAT pcp scenario
|
||||
|
||||
## Scenario
|
||||
|
||||
A node behind a NAT becomes `Reachable` by mapping its port over PCP — the router
|
||||
forwards nothing on its own, the node asks for the mapping and no relay is needed.
|
||||
|
||||
## Topology
|
||||
|
||||
```
|
||||
node B ──── lan ──── router (NAT + miniupnpd/PCP) ──── wan ──── bootstrap A
|
||||
```
|
||||
|
||||
- **bootstrap A** — public node on the wan, runs the relay + autonat server.
|
||||
- **router** — `lan -> wan` masquerade and *no* static forward. It runs
|
||||
`miniupnpd` (real nftables backend) with PCP/NAT-PMP enabled. libplum tries PCP
|
||||
first, so the mapping request goes over PCP and installs a real DNAT into the
|
||||
nft chains the entrypoint pre-creates.
|
||||
- **node B** — `nat=auto`, on the lan. First detected `NotReachable`, it maps its
|
||||
TCP listen (8070) and UDP disc (8090) ports over PCP; the resulting DNAT lets
|
||||
A's dial-back reach it, so the next AutoNAT round flips it to `Reachable`.
|
||||
|
||||
The wan public range and `internal` flag work as in
|
||||
[not-reachable](../not-reachable/README.md); the public wan IP also keeps
|
||||
miniupnpd from refusing PCP/NAT-PMP as double-NAT.
|
||||
|
||||
## Run
|
||||
|
||||
Every NAT scenario:
|
||||
|
||||
```bash
|
||||
make testNatIntegration
|
||||
```
|
||||
|
||||
Just this one — same `STORAGE_INTEGRATION_TEST_INCLUDES` filter as testIntegration,
|
||||
with the test file path:
|
||||
|
||||
```bash
|
||||
make testNatIntegration \
|
||||
STORAGE_INTEGRATION_TEST_INCLUDES=tests/integration/nat/pcp/testpcp.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 ends up `Reachable`, the relay not running, announcing its direct address with
|
||||
an active PCP mapping. Its `debug/info`:
|
||||
|
||||
```json
|
||||
{
|
||||
"nat": {
|
||||
"reachability": "Reachable",
|
||||
"clientMode": false,
|
||||
"relayRunning": false,
|
||||
"portMapping": "pcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Per-run container logs (router, bootstrap, node) are written before teardown to
|
||||
`tests/integration/logs/<timestamp>__NAT_pcp/<test>/<service>.log`.
|
||||
96
tests/integration/nat/pcp/compose.yml
Normal file
96
tests/integration/nat/pcp/compose.yml
Normal file
@ -0,0 +1,96 @@
|
||||
# Same NAT topology as upnp, but miniupnpd has PCP enabled and the node maps its
|
||||
# port over PCP (libplum's preferred protocol), which installs a real DNAT on the
|
||||
# router, so AutoNAT's dial-back reaches it and it is detected Reachable — no
|
||||
# relay. Run via testpcp.nim.
|
||||
#
|
||||
# node B ──── lan ──── router (NAT + miniupnpd/PCP) ──── wan ──── bootstrap A
|
||||
name: nat-pcp
|
||||
|
||||
# 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, relay + autonat server
|
||||
bootstrap_ip: &bootstrap_ip 7.7.7.10
|
||||
# router's public face
|
||||
router_wan_ip: &router_wan_ip 7.7.7.2
|
||||
# router's private face = B's gateway, and the PCP/NAT-PMP control point
|
||||
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
|
||||
ROUTER_LAN_IP: *router_lan_ip
|
||||
LAN_SUBNET: *lan_subnet
|
||||
# scripts mounted, not baked, 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
|
||||
|
||||
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 poll it
|
||||
ports:
|
||||
- "127.0.0.1:18083: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
|
||||
EXTRA_STORAGE_ARGS: >-
|
||||
--nat-port-mapping-discover-timeout=5000
|
||||
--nat-port-mapping-timeout=5000
|
||||
volumes:
|
||||
- ../node-entrypoint.sh:/scripts/node-entrypoint.sh:ro,z
|
||||
entrypoint: ["bash", "/scripts/node-entrypoint.sh"]
|
||||
48
tests/integration/nat/pcp/router-entrypoint.sh
Normal file
48
tests/integration/nat/pcp/router-entrypoint.sh
Normal file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source "$(dirname "$0")/router-common.sh"
|
||||
|
||||
# miniupnpd serves PCP/NAT-PMP (UDP 5351) on the lan face, so find it by its IP
|
||||
# like router-common.sh finds the wan one.
|
||||
lanif=$(ip -o -4 addr show | awk -v ip="$ROUTER_LAN_IP" '$0 ~ ip {print $2; exit}')
|
||||
|
||||
# Reuse miniupnpd's chains (as nft_init.sh sets them up) without its forward drop
|
||||
# policy.
|
||||
nft -f - <<'EOF'
|
||||
table inet filter {
|
||||
chain prerouting_miniupnpd {}
|
||||
chain postrouting_miniupnpd {}
|
||||
chain miniupnpd {}
|
||||
chain prerouting {
|
||||
type nat hook prerouting priority -100; policy accept;
|
||||
jump prerouting_miniupnpd
|
||||
}
|
||||
chain postrouting {
|
||||
type nat hook postrouting priority 100; policy accept;
|
||||
jump postrouting_miniupnpd
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
conf=/tmp/miniupnpd.conf
|
||||
cat > "$conf" <<EOF
|
||||
ext_ifname=$wanif
|
||||
listening_ip=$lanif
|
||||
# Enable PCP/NAT-PMP: libplum tries PCP first, so it picks it here.
|
||||
enable_pcp_pmp=yes
|
||||
# port=0: pick a random HTTP port, no conflict with the storage API.
|
||||
port=0
|
||||
# Without an allow rule miniupnpd denies every mapping request by default.
|
||||
allow 1024-65535 0.0.0.0/0 1024-65535
|
||||
EOF
|
||||
|
||||
# -d: stay in the foreground; background it so the SIGTERM trap below still fires.
|
||||
miniupnpd-nft -d -f "$conf" &
|
||||
upnpd_pid=$!
|
||||
sleep 1
|
||||
kill -0 "$upnpd_pid" 2>/dev/null \
|
||||
|| { echo "ERROR: miniupnpd failed to start" >&2; exit 1; }
|
||||
|
||||
echo "router ready (wan iface $wanif, miniupnpd on $lanif)"
|
||||
|
||||
hold_until_stopped
|
||||
70
tests/integration/nat/pcp/testpcp.nim
Normal file
70
tests/integration/nat/pcp/testpcp.nim
Normal file
@ -0,0 +1,70 @@
|
||||
## NAT pcp scenario — node behind a real NAT becomes Reachable by mapping its
|
||||
## port over PCP.
|
||||
##
|
||||
## Same shape as the upnp test, but miniupnpd has PCP enabled and the node maps
|
||||
## its TCP/UDP ports via PCP (libplum's preferred protocol), which installs a real
|
||||
## DNAT on the router. AutoNAT's dial-back then reaches the node, so it is
|
||||
## detected Reachable with an active PCP mapping — no 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
|
||||
|
||||
const
|
||||
detectTimeout = 300_000 # ms
|
||||
pollInterval = 5_000 # ms
|
||||
|
||||
proc announcesDirectAddr(info: JsonNode): bool =
|
||||
## A reachable node announces at least one direct (non-circuit) address.
|
||||
info{"announceAddresses"}.getElems.anyIt("p2p-circuit" notin it.getStr)
|
||||
|
||||
asyncchecksuite "NAT pcp":
|
||||
let
|
||||
composeFile = currentSourcePath.parentDir / "compose.yml"
|
||||
nodeApiUrl = "http://127.0.0.1:18083/api/storage/v1"
|
||||
suiteName = "NAT pcp"
|
||||
testName = "node behind NAT maps its port over PCP and is Reachable"
|
||||
services = ["router", "bootstrap", "node"]
|
||||
startTime = now().format("yyyy-MM-dd'_'HH:mm:ss")
|
||||
var client: StorageClient
|
||||
|
||||
setup:
|
||||
compose(composeFile, "up -d")
|
||||
client = StorageClient.new(nodeApiUrl)
|
||||
|
||||
teardown:
|
||||
await client.close()
|
||||
saveContainerLogs(composeFile, suiteName, testName, startTime, services)
|
||||
compose(composeFile, "down -v")
|
||||
|
||||
test testName:
|
||||
# Reachable is the settling signal: wait for it, then assert each expected
|
||||
# property separately so a failure points at the exact condition.
|
||||
check eventuallySafe(
|
||||
block:
|
||||
var reachable = false
|
||||
try:
|
||||
let info = await client.info()
|
||||
reachable =
|
||||
info.isOk and info.get{"nat"}{"reachability"}.getStr == "Reachable"
|
||||
except HttpError:
|
||||
discard # B's API is not up yet, keep polling
|
||||
reachable,
|
||||
timeout = detectTimeout,
|
||||
pollInterval = pollInterval,
|
||||
)
|
||||
|
||||
let info = (await client.info()).get
|
||||
let nat = info{"nat"}
|
||||
check nat{"reachability"}.getStr == "Reachable"
|
||||
check nat{"relayRunning"}.getBool == false
|
||||
check nat{"portMapping"}.getStr == "pcp"
|
||||
check info.announcesDirectAddr()
|
||||
Loading…
x
Reference in New Issue
Block a user