mirror of
https://github.com/logos-storage/nim-libplum.git
synced 2026-06-07 09:40:01 +00:00
Add integration tests
This commit is contained in:
parent
d287a36444
commit
4fef654c60
14
README.md
14
README.md
@ -88,10 +88,20 @@ Basic tests run without a router:
|
||||
nimble test
|
||||
```
|
||||
|
||||
To run tests against a real NAT device:
|
||||
Integration tests run miniupnpd inside a Docker/Podman container and exercise the PCP and UPnP-IGD flows.
|
||||
Podman or Docker as fallback will be used for testing with `NET_ADMIN` capability:
|
||||
|
||||
```bash
|
||||
NAT_TEST_PLUM=1 nimble test
|
||||
nimble testIntegration
|
||||
```
|
||||
|
||||
This builds the image and runs two containers: one for PCP and one for UPnP.
|
||||
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`:
|
||||
|
||||
```bash
|
||||
TEST_VERBOSE=1 nimble testIntegration
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@ -24,7 +24,15 @@ task buildBundledLibs, "build bundled libraries":
|
||||
task test, "run tests":
|
||||
compileStaticLibraries()
|
||||
exec("nimble setup")
|
||||
exec("nim c -r tests/test_plum.nim")
|
||||
exec("nim c -o:tests/test_plum tests/test_plum.nim")
|
||||
exec("./tests/test_plum")
|
||||
|
||||
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)
|
||||
|
||||
before install:
|
||||
compileStaticLibraries()
|
||||
|
||||
51
tests/Dockerfile
Normal file
51
tests/Dockerfile
Normal file
@ -0,0 +1,51 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
cmake gcc make git curl ca-certificates xz-utils \
|
||||
libc-dev \
|
||||
iproute2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build miniupnpd with stub redirector: no iptables/nftables needed, always
|
||||
# returns success for port mapping requests — safe for isolated test containers.
|
||||
COPY tests/miniupnpd_stub_rdr.c /tmp/stub_rdr.c
|
||||
RUN git clone --depth=1 --branch miniupnpd_2_3_9 \
|
||||
https://github.com/miniupnp/miniupnp.git /tmp/miniupnp \
|
||||
&& cd /tmp/miniupnp/miniupnpd \
|
||||
&& ./configure \
|
||||
&& cp /tmp/stub_rdr.c . \
|
||||
&& make NETFILTEROBJS=stub_rdr.o miniupnpd \
|
||||
&& install -m 755 miniupnpd /usr/local/sbin/miniupnpd \
|
||||
&& rm -rf /tmp/miniupnp /tmp/stub_rdr.c
|
||||
|
||||
# Install Nim
|
||||
ARG NIM_VERSION=2.2.10
|
||||
RUN curl -fsSL "https://nim-lang.org/download/nim-${NIM_VERSION}-linux_x64.tar.xz" \
|
||||
| tar -xJ -C /opt && \
|
||||
ln -s "/opt/nim-${NIM_VERSION}/bin/nim" /usr/local/bin/nim && \
|
||||
ln -s "/opt/nim-${NIM_VERSION}/bin/nimble" /usr/local/bin/nimble
|
||||
|
||||
# Install nim deps (cached layer)
|
||||
WORKDIR /app
|
||||
COPY libplum.nimble .
|
||||
COPY libplum/ libplum/
|
||||
COPY vendor/ vendor/
|
||||
RUN nimble setup -y
|
||||
|
||||
# Build libplum static library
|
||||
RUN cmake -B vendor/libplum/build \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
vendor/libplum && \
|
||||
cmake --build 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
|
||||
|
||||
COPY tests/docker-entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
69
tests/docker-entrypoint.sh
Normal file
69
tests/docker-entrypoint.sh
Normal file
@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
RUNDIR=/tmp/plum-test
|
||||
mkdir -p "$RUNDIR"
|
||||
|
||||
LAN_IF=$(ip route show default | awk '/default/{print $5; exit}')
|
||||
LAN_IP=$(ip -4 addr show "$LAN_IF" | awk '/inet /{print $2; exit}' | cut -d/ -f1)
|
||||
|
||||
# Use a public (non-reserved) WAN IP. miniupnpd disables port forwarding when
|
||||
# the external interface has a private/RFC1918 address (treats it as double-NAT).
|
||||
ip link add plum-wan type dummy
|
||||
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 conf="$RUNDIR/miniupnpd-$proto.conf"
|
||||
local pidfile="$RUNDIR/miniupnpd-$proto.pid"
|
||||
|
||||
cat > "$conf" << EOF
|
||||
ext_ifname=plum-wan
|
||||
listening_ip=$listen_on
|
||||
enable_pcp_pmp=$enable_pcp_pmp
|
||||
# port=0: pick a random HTTP port to avoid conflicts with host services.
|
||||
port=0
|
||||
# Without an allow rule miniupnpd denies all mapping requests by default,
|
||||
# returning NO_RESOURCES (PCP) or ActionFailed (UPnP).
|
||||
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 &
|
||||
echo $! > "$pidfile"
|
||||
sleep 1
|
||||
kill -0 "$(cat "$pidfile")" 2>/dev/null \
|
||||
|| { echo "miniupnpd-$proto failed to start" >&2; exit 1; }
|
||||
echo "miniupnpd-$proto started (pid $(cat "$pidfile"))"
|
||||
}
|
||||
|
||||
run_tests() {
|
||||
local proto=$1 logfile=$2
|
||||
local failed=0
|
||||
for mm in orc refc; do
|
||||
echo "--- $proto $mm ---"
|
||||
"/app/tests/test_${proto}_${mm}" || failed=1
|
||||
done
|
||||
if [ "${TEST_VERBOSE:-}" = "1" ]; then
|
||||
echo "--- miniupnpd log ---"
|
||||
cat "$logfile" 2>/dev/null || true
|
||||
fi
|
||||
[ $failed -eq 0 ] || exit 1
|
||||
}
|
||||
|
||||
if [ "${TEST_MINIUPNP_PCP:-}" = "1" ]; then
|
||||
# PCP requires the UDP source IP to equal the client_address field in the
|
||||
# MAP request header. libplum sets client_address by connecting a UDP socket
|
||||
# to the gateway IP. Pointing the default route at LAN_IP makes libplum
|
||||
# use LAN_IP as both the gateway (where it sends PCP) and the source IP,
|
||||
# so the two match and miniupnpd accepts the request without ADDRESS_MISMATCH.
|
||||
ip route replace default via "$LAN_IP" dev "$LAN_IF"
|
||||
start_miniupnpd pcp yes "$LAN_IF"
|
||||
run_tests pcp "$RUNDIR/miniupnpd-pcp.log"
|
||||
fi
|
||||
|
||||
if [ "${TEST_MINIUPNP_UPNP:-}" = "1" ]; then
|
||||
start_miniupnpd upnp no "$LAN_IF"
|
||||
run_tests upnp "$RUNDIR/miniupnpd-upnp.log"
|
||||
fi
|
||||
|
||||
169
tests/miniupnpd_stub_rdr.c
Normal file
169
tests/miniupnpd_stub_rdr.c
Normal file
@ -0,0 +1,169 @@
|
||||
/* Stub firewall backend for miniupnpd.
|
||||
* Replaces iptcrdr.o + iptpinhole.o + nfct_get.o.
|
||||
* All mapping operations succeed without touching the kernel. */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
/* commonrdr.h interface */
|
||||
|
||||
int init_redirect(void) { return 0; }
|
||||
void shutdown_redirect(void) {}
|
||||
|
||||
int get_redirect_rule_count(const char *ifname)
|
||||
{ (void)ifname; return 0; }
|
||||
|
||||
int get_redirect_rule(const char *ifname, unsigned short eport, int proto,
|
||||
char *iaddr, int iaddrlen, unsigned short *iport,
|
||||
char *desc, int desclen,
|
||||
char *rhost, int rhostlen,
|
||||
unsigned int *timestamp,
|
||||
uint64_t *packets, uint64_t *bytes)
|
||||
{ (void)ifname; (void)eport; (void)proto; (void)iaddr; (void)iaddrlen;
|
||||
(void)iport; (void)desc; (void)desclen; (void)rhost; (void)rhostlen;
|
||||
(void)timestamp; (void)packets; (void)bytes; return -1; }
|
||||
|
||||
int get_redirect_rule_by_index(int index,
|
||||
char *ifname, unsigned short *eport,
|
||||
char *iaddr, int iaddrlen, unsigned short *iport,
|
||||
int *proto, char *desc, int desclen,
|
||||
char *rhost, int rhostlen,
|
||||
unsigned int *timestamp,
|
||||
uint64_t *packets, uint64_t *bytes)
|
||||
{ (void)index; (void)ifname; (void)eport; (void)iaddr; (void)iaddrlen;
|
||||
(void)iport; (void)proto; (void)desc; (void)desclen; (void)rhost;
|
||||
(void)rhostlen; (void)timestamp; (void)packets; (void)bytes; return -1; }
|
||||
|
||||
unsigned short *get_portmappings_in_range(unsigned short startport,
|
||||
unsigned short endport,
|
||||
int proto, unsigned int *number)
|
||||
{ (void)startport; (void)endport; (void)proto; *number = 0; return 0; }
|
||||
|
||||
int update_portmapping(const char *ifname, unsigned short eport, int proto,
|
||||
unsigned short iport, const char *desc,
|
||||
unsigned int timestamp)
|
||||
{ (void)ifname; (void)eport; (void)proto; (void)iport; (void)desc;
|
||||
(void)timestamp; return 0; }
|
||||
|
||||
int update_portmapping_desc_timestamp(const char *ifname,
|
||||
unsigned short eport, int proto,
|
||||
const char *desc, unsigned int timestamp)
|
||||
{ (void)ifname; (void)eport; (void)proto; (void)desc; (void)timestamp;
|
||||
return 0; }
|
||||
|
||||
/* iptcrdr.h interface */
|
||||
|
||||
int add_redirect_rule2(const char *ifname,
|
||||
const char *rhost, unsigned short eport,
|
||||
const char *iaddr, unsigned short iport, int proto,
|
||||
const char *desc, unsigned int timestamp)
|
||||
{ (void)ifname; (void)rhost; (void)eport; (void)iaddr; (void)iport;
|
||||
(void)proto; (void)desc; (void)timestamp; return 0; }
|
||||
|
||||
int add_peer_redirect_rule2(const char *ifname,
|
||||
const char *rhost, unsigned short rport,
|
||||
const char *eaddr, unsigned short eport,
|
||||
const char *iaddr, unsigned short iport, int proto,
|
||||
const char *desc, unsigned int timestamp)
|
||||
{ (void)ifname; (void)rhost; (void)rport; (void)eaddr; (void)eport;
|
||||
(void)iaddr; (void)iport; (void)proto; (void)desc; (void)timestamp;
|
||||
return 0; }
|
||||
|
||||
int add_filter_rule2(const char *ifname,
|
||||
const char *rhost, const char *iaddr,
|
||||
unsigned short eport, unsigned short iport,
|
||||
int proto, const char *desc)
|
||||
{ (void)ifname; (void)rhost; (void)iaddr; (void)eport; (void)iport;
|
||||
(void)proto; (void)desc; return 0; }
|
||||
|
||||
int delete_redirect_and_filter_rules(unsigned short eport, int proto)
|
||||
{ (void)eport; (void)proto; return 0; }
|
||||
|
||||
int delete_filter_rule(const char *ifname, unsigned short port, int proto)
|
||||
{ (void)ifname; (void)port; (void)proto; return 0; }
|
||||
|
||||
int add_peer_dscp_rule2(const char *ifname,
|
||||
const char *rhost, unsigned short rport,
|
||||
unsigned char dscp,
|
||||
const char *iaddr, unsigned short iport, int proto,
|
||||
const char *desc, unsigned int timestamp)
|
||||
{ (void)ifname; (void)rhost; (void)rport; (void)dscp; (void)iaddr;
|
||||
(void)iport; (void)proto; (void)desc; (void)timestamp; return 0; }
|
||||
|
||||
int get_peer_rule_by_index(int index,
|
||||
char *ifname, unsigned short *eport,
|
||||
char *iaddr, int iaddrlen, unsigned short *iport,
|
||||
int *proto, char *desc, int desclen,
|
||||
char *rhost, int rhostlen, unsigned short *rport,
|
||||
unsigned int *timestamp,
|
||||
uint64_t *packets, uint64_t *bytes)
|
||||
{ (void)index; (void)ifname; (void)eport; (void)iaddr; (void)iaddrlen;
|
||||
(void)iport; (void)proto; (void)desc; (void)desclen; (void)rhost;
|
||||
(void)rhostlen; (void)rport; (void)timestamp; (void)packets; (void)bytes;
|
||||
return -1; }
|
||||
|
||||
int get_nat_redirect_rule(const char *nat_chain_name, const char *ifname,
|
||||
unsigned short eport, int proto,
|
||||
char *iaddr, int iaddrlen, unsigned short *iport,
|
||||
char *desc, int desclen,
|
||||
char *rhost, int rhostlen,
|
||||
unsigned int *timestamp,
|
||||
uint64_t *packets, uint64_t *bytes)
|
||||
{ (void)nat_chain_name; (void)ifname; (void)eport; (void)proto; (void)iaddr;
|
||||
(void)iaddrlen; (void)iport; (void)desc; (void)desclen; (void)rhost;
|
||||
(void)rhostlen; (void)timestamp; (void)packets; (void)bytes; return -1; }
|
||||
|
||||
int list_redirect_rule(const char *ifname)
|
||||
{ (void)ifname; return 0; }
|
||||
|
||||
/* commonrdr.h USE_NETFILTER interface */
|
||||
|
||||
int set_rdr_name(int param, const char *string)
|
||||
{ (void)param; (void)string; return 0; }
|
||||
|
||||
/* nfct_get.c interface */
|
||||
|
||||
int get_nat_ext_addr(struct sockaddr *src, struct sockaddr *dst, uint8_t proto,
|
||||
struct sockaddr *ret_ext)
|
||||
{ (void)src; (void)dst; (void)proto; (void)ret_ext; return -1; }
|
||||
|
||||
/* iptpinhole.h interface */
|
||||
|
||||
int find_pinhole(const char *ifname,
|
||||
const char *rem_host, unsigned short rem_port,
|
||||
const char *int_client, unsigned short int_port,
|
||||
int proto, char *desc, int desc_len, unsigned int *timestamp)
|
||||
{ (void)ifname; (void)rem_host; (void)rem_port; (void)int_client;
|
||||
(void)int_port; (void)proto; (void)desc; (void)desc_len; (void)timestamp;
|
||||
return -1; }
|
||||
|
||||
int add_pinhole(const char *ifname,
|
||||
const char *rem_host, unsigned short rem_port,
|
||||
const char *int_client, unsigned short int_port,
|
||||
int proto, const char *desc, unsigned int timestamp)
|
||||
{ (void)ifname; (void)rem_host; (void)rem_port; (void)int_client;
|
||||
(void)int_port; (void)proto; (void)desc; (void)timestamp; return 0; }
|
||||
|
||||
int update_pinhole(unsigned short uid, unsigned int timestamp)
|
||||
{ (void)uid; (void)timestamp; return 0; }
|
||||
|
||||
int delete_pinhole(unsigned short uid)
|
||||
{ (void)uid; return 0; }
|
||||
|
||||
int get_pinhole_info(unsigned short uid,
|
||||
char *rem_host, int rem_hostlen, unsigned short *rem_port,
|
||||
char *int_client, int int_clientlen,
|
||||
unsigned short *int_port,
|
||||
int *proto, char *desc, int desclen,
|
||||
unsigned int *timestamp,
|
||||
uint64_t *packets, uint64_t *bytes)
|
||||
{ (void)uid; (void)rem_host; (void)rem_hostlen; (void)rem_port;
|
||||
(void)int_client; (void)int_clientlen; (void)int_port; (void)proto;
|
||||
(void)desc; (void)desclen; (void)timestamp; (void)packets; (void)bytes;
|
||||
return -1; }
|
||||
|
||||
int get_pinhole_uid_by_index(int index)
|
||||
{ (void)index; return -1; }
|
||||
|
||||
int clean_pinhole_list(unsigned int *next_timestamp)
|
||||
{ (void)next_timestamp; return 0; }
|
||||
@ -6,7 +6,7 @@
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import std/envvars
|
||||
import std/os
|
||||
import unittest2
|
||||
import chronos
|
||||
import libplum/plum
|
||||
@ -34,48 +34,36 @@ suite "plum":
|
||||
test "hasMapping returns false for unknown id":
|
||||
check not hasMapping(999)
|
||||
|
||||
test "createMapping fails without router":
|
||||
# In CI with no NAT device, expect Failure or timeout — both return err.
|
||||
if getEnv("NAT_TEST_PLUM") == "1":
|
||||
skip()
|
||||
return
|
||||
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()
|
||||
|
||||
discard init()
|
||||
let r = waitFor createMapping(UDP, 12345, timeout = seconds(5))
|
||||
check r.isErr()
|
||||
discard cleanup()
|
||||
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 hasMapping(res.id)
|
||||
if getEnv("TEST_VERBOSE") == "1":
|
||||
echo miniupnp_protocol & " TCP: " & res.mapping.externalHost & ":" & $res.mapping.externalPort
|
||||
destroyMapping(res.id)
|
||||
|
||||
suite "plum - NAT port mapping (requires NAT_TEST_PLUM=1)":
|
||||
test "createMapping TCP and destroyMapping":
|
||||
if getEnv("NAT_TEST_PLUM") != "1":
|
||||
skip()
|
||||
return
|
||||
discard cleanup()
|
||||
|
||||
check init(discoverTimeout = 15000).isOk()
|
||||
test "createMapping UDP and destroying":
|
||||
check init(discoverTimeout = 2000).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 hasMapping(res.id)
|
||||
destroyMapping(res.id)
|
||||
let r = waitFor createMapping(UDP, 8090, timeout = seconds(40))
|
||||
check r.isOk()
|
||||
if r.isOk():
|
||||
let res = r.value
|
||||
check res.mapping.externalPort > 0
|
||||
if getEnv("TEST_VERBOSE") == "1":
|
||||
echo miniupnp_protocol & " UDP: " & res.mapping.externalHost & ":" & $res.mapping.externalPort
|
||||
destroyMapping(res.id)
|
||||
|
||||
discard cleanup()
|
||||
|
||||
test "createMapping UDP":
|
||||
if getEnv("NAT_TEST_PLUM") != "1":
|
||||
skip()
|
||||
return
|
||||
|
||||
check init(discoverTimeout = 2000).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
|
||||
destroyMapping(res.id)
|
||||
|
||||
discard cleanup()
|
||||
discard cleanup()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user