mirror of
https://github.com/logos-storage/nim-libplum.git
synced 2026-06-07 09:40:01 +00:00
Add pmp tests and mapping protocol
This commit is contained in:
parent
adde1b5d27
commit
1269e80605
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -2,7 +2,7 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
@ -12,3 +12,4 @@ jobs:
|
||||
with:
|
||||
test-command: |
|
||||
nimble test
|
||||
nimble testIntegration
|
||||
|
||||
13
README.md
13
README.md
@ -31,6 +31,7 @@ proc main() {.async.} =
|
||||
|
||||
let res = r.value
|
||||
echo "external: ", res.mapping.externalHost, ":", res.mapping.externalPort
|
||||
echo "protocol: ", res.mapping.mappingProtocol # PCP, NatPmp, UPnP, or Direct
|
||||
|
||||
destroyMapping(res.id)
|
||||
discard cleanup()
|
||||
@ -95,13 +96,19 @@ Podman or Docker as fallback will be used for testing with `NET_ADMIN` capabilit
|
||||
nimble testIntegration
|
||||
```
|
||||
|
||||
This builds the image and runs two containers: one for PCP and one for UPnP.
|
||||
This builds the image and runs three containers: PCP, UPnP, and a NAT-PMP fallback scenario
|
||||
(miniupnpd compiled without PCP so libplum must fall back from PCP timeout to NAT-PMP).
|
||||
Each protocol is tested under both `orc` and `refc` memory managers.
|
||||
miniupnpd is built with a stub firewall backend (`tests/miniupnpd_stub_rdr.c`) so it accepts mapping requests without requiring iptables or nftables in the container.
|
||||
To see the miniupnpd logs and the resolved external addresses, pass `TEST_VERBOSE=1`:
|
||||
|
||||
Three env vars control verbosity:
|
||||
|
||||
- `TEST_VERBOSE=1`: print resolved external addresses
|
||||
- `MINIUPNPD_VERBOSE=1`: print miniupnpd logs
|
||||
- `LIBPLUM_VERBOSE=1`: enable verbose libplum internal logs
|
||||
|
||||
```bash
|
||||
TEST_VERBOSE=1 nimble testIntegration
|
||||
TEST_VERBOSE=1 MINIUPNPD_VERBOSE=1 LIBPLUM_VERBOSE=1 nimble testIntegration
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
16
api.md
16
api.md
@ -17,9 +17,19 @@ type PlumState* = enum
|
||||
Destroying
|
||||
```
|
||||
|
||||
```nim
|
||||
type MappingProtocol* = enum
|
||||
Unknown ## not yet determined (mapping pending)
|
||||
PCP ## Port Control Protocol
|
||||
NatPmp ## NAT Port Mapping Protocol
|
||||
UPnP ## UPnP-IGD
|
||||
Direct ## no mapping needed, local address is already public
|
||||
```
|
||||
|
||||
```nim
|
||||
type PlumMapping* = object
|
||||
protocol*: PlumProtocol
|
||||
protocol*: PlumProtocol ## IP protocol (TCP/UDP)
|
||||
mappingProtocol*: MappingProtocol ## NAT traversal protocol used
|
||||
internalPort*: uint16
|
||||
externalPort*: uint16
|
||||
externalHost*: string
|
||||
@ -51,7 +61,7 @@ proc init*(
|
||||
|
||||
Initializes the library and starts the internal thread. Must be called before any other proc.
|
||||
|
||||
- `logLevel`: verbosity of internal logs (default: none)
|
||||
- `logLevel`: verbosity of internal logs, from `PLUM_LOG_LEVEL_VERBOSE` to `PLUM_LOG_LEVEL_NONE` (default: none); import `libplum/libplum` to access these constants
|
||||
- `discoverTimeout`: how long to probe for a NAT device, in ms (default: 10000)
|
||||
- `mappingTimeout`: how long to wait for a mapping response, in ms (default: 10000)
|
||||
- `recheckPeriod`: interval between periodic mapping rechecks, in ms (default: 300000)
|
||||
@ -84,7 +94,7 @@ Requests a port mapping from the NAT device. Tries PCP, then NAT-PMP, then UPnP-
|
||||
- `timeout`: how long to wait for the mapping to be established (default: 30s)
|
||||
- `onStateChange`: optional callback invoked when the mapping state changes after the initial result
|
||||
|
||||
Returns a `MappingResult` containing the mapping `id` (needed for `destroyMapping`) and the `PlumMapping` with the external address and port.
|
||||
Returns a `MappingResult` containing the mapping `id` (needed for `destroyMapping`) and the `PlumMapping` with the external address, port, and `mappingProtocol` indicating which protocol was used (`PCP`, `NatPmp`, or `UPnP`).
|
||||
|
||||
Returns an error if no NAT device is found, the mapping fails, or the timeout expires.
|
||||
|
||||
|
||||
@ -30,9 +30,12 @@ task test, "run tests":
|
||||
task testIntegration, "run miniupnpd integration tests in Docker / Podman":
|
||||
let docker = if findExe("podman") != "": "podman" else: "docker"
|
||||
exec(docker & " build -t " & packageName & " -f tests/Dockerfile .")
|
||||
let verbose = if getEnv("TEST_VERBOSE") != "": " -e TEST_VERBOSE=" & getEnv("TEST_VERBOSE") else: ""
|
||||
exec(docker & " run --rm --cap-add=NET_ADMIN -e TEST_MINIUPNP_PCP=1" & verbose & " " & packageName)
|
||||
exec(docker & " run --rm --cap-add=NET_ADMIN -e TEST_MINIUPNP_UPNP=1" & verbose & " " & packageName)
|
||||
proc envFlag(name: string): string =
|
||||
if getEnv(name) != "": " -e " & name & "=" & getEnv(name) else: ""
|
||||
let flags = envFlag("TEST_VERBOSE") & envFlag("MINIUPNPD_VERBOSE") & envFlag("LIBPLUM_VERBOSE")
|
||||
exec(docker & " run --rm --cap-add=NET_ADMIN -e TEST_MINIUPNP_PCP=1" & flags & " " & packageName)
|
||||
exec(docker & " run --rm --cap-add=NET_ADMIN -e TEST_MINIUPNP_UPNP=1" & flags & " " & packageName)
|
||||
exec(docker & " run --rm --cap-add=NET_ADMIN -e TEST_MINIUPNP_NATPMP=1" & flags & " " & packageName)
|
||||
|
||||
before install:
|
||||
compileStaticLibraries()
|
||||
|
||||
@ -60,6 +60,14 @@ type
|
||||
PLUM_STATE_SUCCESS = 2
|
||||
PLUM_STATE_FAILURE = 3
|
||||
PLUM_STATE_DESTROYING = 4
|
||||
|
||||
plum_mapping_protocol_t* {.importc: "plum_mapping_protocol_t", header: "plum.h",
|
||||
size: sizeof(cint).} = enum
|
||||
PLUM_MAPPING_PROTOCOL_UNKNOWN = 0
|
||||
PLUM_MAPPING_PROTOCOL_PCP = 1
|
||||
PLUM_MAPPING_PROTOCOL_NATPMP = 2
|
||||
PLUM_MAPPING_PROTOCOL_UPNP = 3
|
||||
PLUM_MAPPING_PROTOCOL_DIRECT = 4
|
||||
|
||||
# Define the callback to receive the plum logs
|
||||
plum_log_callback_t* = proc(level: plum_log_level_t, message: cstring) {.cdecl.}
|
||||
@ -76,11 +84,12 @@ type
|
||||
# Define the mapping struct, passed by copy (usual for struct).
|
||||
# The user_ptr is a pointer to the MappingHandle in order to receive the result
|
||||
plum_mapping_t* {.importc: "plum_mapping_t", header: "plum.h", bycopy.} = object
|
||||
protocol* {.importc: "protocol".}: plum_ip_protocol_t
|
||||
internal_port* {.importc: "internal_port".}: uint16
|
||||
external_port* {.importc: "external_port".}: uint16
|
||||
external_host* {.importc: "external_host".}: array[PLUM_MAX_HOST_LEN, char]
|
||||
user_ptr* {.importc: "user_ptr".}: pointer
|
||||
protocol* {.importc: "protocol".}: plum_ip_protocol_t
|
||||
mapping_protocol* {.importc: "mapping_protocol".}: plum_mapping_protocol_t
|
||||
internal_port* {.importc: "internal_port".}: uint16
|
||||
external_port* {.importc: "external_port".}: uint16
|
||||
external_host* {.importc: "external_host".}: array[PLUM_MAX_HOST_LEN, char]
|
||||
user_ptr* {.importc: "user_ptr".}: pointer
|
||||
|
||||
# Define the callback to receive the mapping result
|
||||
plum_mapping_callback_t* =
|
||||
|
||||
@ -28,8 +28,16 @@ type
|
||||
Failure = PLUM_STATE_FAILURE.int
|
||||
Destroying = PLUM_STATE_DESTROYING.int
|
||||
|
||||
MappingProtocol* = enum
|
||||
Unknown = PLUM_MAPPING_PROTOCOL_UNKNOWN.int
|
||||
PCP = PLUM_MAPPING_PROTOCOL_PCP.int
|
||||
NatPmp = PLUM_MAPPING_PROTOCOL_NATPMP.int
|
||||
UPnP = PLUM_MAPPING_PROTOCOL_UPNP.int
|
||||
Direct = PLUM_MAPPING_PROTOCOL_DIRECT.int
|
||||
|
||||
PlumMapping* = object
|
||||
protocol*: PlumProtocol
|
||||
mappingProtocol*: MappingProtocol
|
||||
internalPort*: uint16
|
||||
externalPort*: uint16
|
||||
externalHost*: string
|
||||
@ -51,6 +59,7 @@ type
|
||||
resolved: Atomic[bool]
|
||||
resolvedState: PlumState
|
||||
resolvedProtocol: PlumProtocol
|
||||
resolvedMappingProtocol: MappingProtocol
|
||||
resolvedInternalPort: uint16
|
||||
resolvedExternalPort: uint16
|
||||
resolvedExternalHost: array[PLUM_MAX_HOST_LEN, char]
|
||||
@ -111,6 +120,7 @@ proc mappingCallback(id: cint, state: plum_state_t,
|
||||
# and fire the signal.
|
||||
handle.resolvedState = plumState
|
||||
handle.resolvedProtocol = PlumProtocol(raw[].protocol.int)
|
||||
handle.resolvedMappingProtocol = MappingProtocol(raw[].mapping_protocol.int)
|
||||
handle.resolvedInternalPort = raw[].internal_port
|
||||
handle.resolvedExternalPort = raw[].external_port
|
||||
handle.resolvedExternalHost = raw[].external_host
|
||||
@ -120,6 +130,7 @@ proc mappingCallback(id: cint, state: plum_state_t,
|
||||
if not handle.onStateChange.isNil:
|
||||
let mapping = PlumMapping(
|
||||
protocol: PlumProtocol(raw[].protocol.int),
|
||||
mappingProtocol: MappingProtocol(raw[].mapping_protocol.int),
|
||||
internalPort: raw[].internal_port,
|
||||
externalPort: raw[].external_port,
|
||||
externalHost: $cast[cstring](addr raw[].external_host)
|
||||
@ -232,6 +243,7 @@ proc createMapping*(
|
||||
resolvedState = h.resolvedState
|
||||
resolvedMapping = PlumMapping(
|
||||
protocol: h.resolvedProtocol,
|
||||
mappingProtocol: h.resolvedMappingProtocol,
|
||||
internalPort: h.resolvedInternalPort,
|
||||
externalPort: h.resolvedExternalPort,
|
||||
externalHost: $cast[cstring](unsafeAddr h.resolvedExternalHost)
|
||||
|
||||
@ -16,6 +16,9 @@ RUN git clone --depth=1 --branch miniupnpd_2_3_9 \
|
||||
&& cp /tmp/stub_rdr.c . \
|
||||
&& make NETFILTEROBJS=stub_rdr.o miniupnpd \
|
||||
&& install -m 755 miniupnpd /usr/local/sbin/miniupnpd \
|
||||
&& sed -i 's/^#define ENABLE_PCP$/\/*#define ENABLE_PCP*\//' config.h \
|
||||
&& make NETFILTEROBJS=stub_rdr.o miniupnpd \
|
||||
&& install -m 755 miniupnpd /usr/local/sbin/miniupnpd-natpmponly \
|
||||
&& rm -rf /tmp/miniupnp /tmp/stub_rdr.c
|
||||
|
||||
# Install Nim
|
||||
@ -41,10 +44,12 @@ RUN cmake -B vendor/libplum/build \
|
||||
|
||||
# Compile test binaries (protocol x memory manager)
|
||||
COPY tests/ tests/
|
||||
RUN nim c -d:miniupnp_protocol=pcp --mm:orc -o:tests/test_pcp_orc tests/test_plum.nim && \
|
||||
nim c -d:miniupnp_protocol=pcp --mm:refc -o:tests/test_pcp_refc tests/test_plum.nim && \
|
||||
nim c -d:miniupnp_protocol=upnp --mm:orc -o:tests/test_upnp_orc tests/test_plum.nim && \
|
||||
nim c -d:miniupnp_protocol=upnp --mm:refc -o:tests/test_upnp_refc tests/test_plum.nim
|
||||
RUN nim c -d:miniupnp_protocol=pcp --mm:orc -o:tests/test_pcp_orc tests/test_plum.nim && \
|
||||
nim c -d:miniupnp_protocol=pcp --mm:refc -o:tests/test_pcp_refc tests/test_plum.nim && \
|
||||
nim c -d:miniupnp_protocol=upnp --mm:orc -o:tests/test_upnp_orc tests/test_plum.nim && \
|
||||
nim c -d:miniupnp_protocol=upnp --mm:refc -o:tests/test_upnp_refc tests/test_plum.nim && \
|
||||
nim c -d:miniupnp_protocol=natpmp --mm:orc -o:tests/test_natpmp_orc tests/test_plum.nim && \
|
||||
nim c -d:miniupnp_protocol=natpmp --mm:refc -o:tests/test_natpmp_refc tests/test_plum.nim
|
||||
|
||||
COPY tests/docker-entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
@ -14,7 +14,7 @@ ip addr add 1.2.3.4/24 dev plum-wan
|
||||
ip link set plum-wan up
|
||||
|
||||
start_miniupnpd() {
|
||||
local proto=$1 enable_pcp_pmp=$2 listen_on=$3
|
||||
local proto=$1 enable_pcp_pmp=$2 listen_on=$3 binary=${4:-miniupnpd}
|
||||
local conf="$RUNDIR/miniupnpd-$proto.conf"
|
||||
local pidfile="$RUNDIR/miniupnpd-$proto.pid"
|
||||
|
||||
@ -29,7 +29,7 @@ port=0
|
||||
allow 1024-65535 0.0.0.0/0 1024-65535
|
||||
EOF
|
||||
# -d: don't daemonize; we background it ourselves with & to capture the pid.
|
||||
miniupnpd -d -f "$conf" > "$RUNDIR/miniupnpd-$proto.log" 2>&1 &
|
||||
"$binary" -d -f "$conf" > "$RUNDIR/miniupnpd-$proto.log" 2>&1 &
|
||||
echo $! > "$pidfile"
|
||||
sleep 1
|
||||
kill -0 "$(cat "$pidfile")" 2>/dev/null \
|
||||
@ -44,7 +44,7 @@ run_tests() {
|
||||
echo "--- $proto $mm ---"
|
||||
"/app/tests/test_${proto}_${mm}" || failed=1
|
||||
done
|
||||
if [ "${TEST_VERBOSE:-}" = "1" ]; then
|
||||
if [ "${MINIUPNPD_VERBOSE:-}" = "1" ]; then
|
||||
echo "--- miniupnpd log ---"
|
||||
cat "$logfile" 2>/dev/null || true
|
||||
fi
|
||||
@ -67,3 +67,13 @@ if [ "${TEST_MINIUPNP_UPNP:-}" = "1" ]; then
|
||||
run_tests upnp "$RUNDIR/miniupnpd-upnp.log"
|
||||
fi
|
||||
|
||||
if [ "${TEST_MINIUPNP_NATPMP:-}" = "1" ]; then
|
||||
# miniupnpd-natpmponly is compiled without ENABLE_PCP: PCP probes get no
|
||||
# response (timeout) and libplum must fall back to NAT-PMP on its own.
|
||||
# Same route trick as PCP: point the default route at LAN_IP so libplum
|
||||
# sends NAT-PMP to the local miniupnpd rather than the real gateway.
|
||||
ip route replace default via "$LAN_IP" dev "$LAN_IF"
|
||||
start_miniupnpd natpmp yes "$LAN_IF" miniupnpd-natpmponly
|
||||
run_tests natpmp "$RUNDIR/miniupnpd-natpmp.log"
|
||||
fi
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import std/os
|
||||
import unittest2
|
||||
import chronos
|
||||
import libplum/plum
|
||||
import libplum/libplum
|
||||
|
||||
suite "plum":
|
||||
test "init and cleanup":
|
||||
@ -36,10 +37,14 @@ suite "plum":
|
||||
|
||||
const miniupnp_protocol {.strdefine.} = ""
|
||||
# The flag is passed by the Docker / Podman container
|
||||
when miniupnp_protocol != "":
|
||||
suite "plum - " & miniupnp_protocol & " using miniupnp":
|
||||
test "createMapping TCP and destroyMapping":
|
||||
check init(discoverTimeout = 15000).isOk()
|
||||
when miniupnp_protocol == "natpmp":
|
||||
# miniupnpd is compiled without PCP: PCP probes time out and libplum must
|
||||
# fall back to NAT-PMP on its own.
|
||||
suite "plum - natpmp fallback (PCP timeout)":
|
||||
test "createMapping TCP via NAT-PMP fallback":
|
||||
let logLevel = if getEnv("LIBPLUM_VERBOSE") == "1": PLUM_LOG_LEVEL_VERBOSE
|
||||
else: PLUM_LOG_LEVEL_NONE
|
||||
check init(discoverTimeout = 15000, logLevel = logLevel).isOk()
|
||||
|
||||
let r = waitFor createMapping(TCP, 8101, timeout = seconds(40))
|
||||
check r.isOk()
|
||||
@ -47,6 +52,28 @@ when miniupnp_protocol != "":
|
||||
let res = r.value
|
||||
check res.mapping.externalPort > 0
|
||||
check res.mapping.externalHost.len > 0
|
||||
check res.mapping.mappingProtocol == NatPmp
|
||||
check hasMapping(res.id)
|
||||
if getEnv("TEST_VERBOSE") == "1":
|
||||
echo "NAT-PMP TCP: " & res.mapping.externalHost & ":" & $res.mapping.externalPort
|
||||
destroyMapping(res.id)
|
||||
|
||||
discard cleanup()
|
||||
|
||||
elif miniupnp_protocol != "":
|
||||
suite "plum - " & miniupnp_protocol & " using miniupnp":
|
||||
test "createMapping TCP and destroyMapping":
|
||||
let logLevel = if getEnv("LIBPLUM_VERBOSE") == "1": PLUM_LOG_LEVEL_VERBOSE
|
||||
else: PLUM_LOG_LEVEL_NONE
|
||||
check init(discoverTimeout = 15000, logLevel = logLevel).isOk()
|
||||
|
||||
let r = waitFor createMapping(TCP, 8101, timeout = seconds(40))
|
||||
check r.isOk()
|
||||
if r.isOk():
|
||||
let res = r.value
|
||||
check res.mapping.externalPort > 0
|
||||
check res.mapping.externalHost.len > 0
|
||||
check res.mapping.mappingProtocol == (if miniupnp_protocol == "pcp": PCP else: UPnP)
|
||||
check hasMapping(res.id)
|
||||
if getEnv("TEST_VERBOSE") == "1":
|
||||
echo miniupnp_protocol & " TCP: " & res.mapping.externalHost & ":" & $res.mapping.externalPort
|
||||
@ -55,13 +82,16 @@ when miniupnp_protocol != "":
|
||||
discard cleanup()
|
||||
|
||||
test "createMapping UDP and destroying":
|
||||
check init(discoverTimeout = 2000).isOk()
|
||||
let logLevel = if getEnv("LIBPLUM_VERBOSE") == "1": PLUM_LOG_LEVEL_VERBOSE
|
||||
else: PLUM_LOG_LEVEL_NONE
|
||||
check init(discoverTimeout = 2000, logLevel = logLevel).isOk()
|
||||
|
||||
let r = waitFor createMapping(UDP, 8090, timeout = seconds(40))
|
||||
check r.isOk()
|
||||
if r.isOk():
|
||||
let res = r.value
|
||||
check res.mapping.externalPort > 0
|
||||
check res.mapping.mappingProtocol == (if miniupnp_protocol == "pcp": PCP else: UPnP)
|
||||
if getEnv("TEST_VERBOSE") == "1":
|
||||
echo miniupnp_protocol & " UDP: " & res.mapping.externalHost & ":" & $res.mapping.externalPort
|
||||
destroyMapping(res.id)
|
||||
|
||||
2
vendor/libplum
vendored
2
vendor/libplum
vendored
@ -1 +1 @@
|
||||
Subproject commit 7b3639bcf973ff9e3de554545f703f5a595a0536
|
||||
Subproject commit f6d2c12a9f347dcd6958b97c9df6418c43d975f2
|
||||
Loading…
x
Reference in New Issue
Block a user