2026-05-14 12:23:10 +04:00
|
|
|
# Copyright (c) 2026 Status Research & Development GmbH
|
|
|
|
|
# Licensed under either of
|
|
|
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
|
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
|
|
|
# at your option.
|
|
|
|
|
# This file may not be copied, modified, or distributed except according to
|
|
|
|
|
# those terms.
|
|
|
|
|
|
2026-05-18 11:43:28 +04:00
|
|
|
import std/os
|
2026-05-26 14:28:09 +04:00
|
|
|
import std/osproc
|
|
|
|
|
import std/strutils
|
|
|
|
|
import posix
|
2026-05-14 12:23:10 +04:00
|
|
|
import unittest2
|
2026-05-26 14:28:09 +04:00
|
|
|
import std/atomics
|
2026-05-14 12:23:10 +04:00
|
|
|
import chronos
|
|
|
|
|
import libplum/plum
|
2026-05-18 15:16:27 +04:00
|
|
|
import libplum/libplum
|
2026-05-14 12:23:10 +04:00
|
|
|
|
|
|
|
|
suite "plum":
|
|
|
|
|
test "init and cleanup":
|
|
|
|
|
let r = init()
|
|
|
|
|
check r.isOk()
|
|
|
|
|
let c = cleanup()
|
|
|
|
|
check c.isOk()
|
|
|
|
|
|
|
|
|
|
test "double cleanup returns error":
|
|
|
|
|
discard init()
|
|
|
|
|
discard cleanup()
|
|
|
|
|
let c = cleanup()
|
|
|
|
|
check c.isErr()
|
|
|
|
|
|
|
|
|
|
test "getLocalAddress after init":
|
|
|
|
|
discard init()
|
|
|
|
|
let r = getLocalAddress()
|
|
|
|
|
check r.isOk()
|
|
|
|
|
check r.value.len > 0
|
|
|
|
|
discard cleanup()
|
|
|
|
|
|
2026-05-14 16:40:10 +04:00
|
|
|
test "hasMapping returns false for unknown id":
|
|
|
|
|
check not hasMapping(999)
|
|
|
|
|
|
2026-05-18 11:43:28 +04:00
|
|
|
const miniupnp_protocol {.strdefine.} = ""
|
2026-05-26 14:28:09 +04:00
|
|
|
|
2026-05-18 15:29:14 +04:00
|
|
|
# The flag is passed by the Docker / Podman container.
|
|
|
|
|
when miniupnp_protocol != "":
|
2026-05-26 14:28:09 +04:00
|
|
|
const
|
|
|
|
|
discoverMs = 2000
|
|
|
|
|
recheckMs = 500
|
|
|
|
|
upnpRestartMs = discoverMs + 2000
|
|
|
|
|
natpmpEpochMs = 3000
|
|
|
|
|
renewalPollMs = 100
|
|
|
|
|
renewalTimeoutMs = 10_000
|
|
|
|
|
|
|
|
|
|
let mappingTimeout = seconds(40)
|
|
|
|
|
|
|
|
|
|
let logLevel =
|
2026-05-26 14:47:35 +04:00
|
|
|
if getEnv("LIBPLUM_VERBOSE") == "1": PLUM_LOG_LEVEL_VERBOSE else: PLUM_LOG_LEVEL_NONE
|
2026-05-26 14:28:09 +04:00
|
|
|
|
|
|
|
|
var gRenewed: Atomic[bool]
|
|
|
|
|
|
2026-05-26 14:47:35 +04:00
|
|
|
proc onMappingRenewal(
|
|
|
|
|
state: PlumState, mapping: PlumMapping
|
|
|
|
|
) {.cdecl, raises: [], gcsafe.} =
|
2026-05-26 14:28:09 +04:00
|
|
|
if state == Success:
|
|
|
|
|
gRenewed.store(true)
|
|
|
|
|
|
|
|
|
|
proc stopMiniupnpd(proto: string) =
|
|
|
|
|
let pidFile = "/tmp/plum-test/miniupnpd-" & proto & ".pid"
|
|
|
|
|
let pid = readFile(pidFile).strip().parseInt()
|
|
|
|
|
|
|
|
|
|
discard posix.kill(pid.Pid, SIGKILL)
|
|
|
|
|
|
|
|
|
|
proc waitRenewal(): bool =
|
|
|
|
|
for _ in 0 ..< (renewalTimeoutMs div renewalPollMs):
|
|
|
|
|
if gRenewed.load():
|
|
|
|
|
return true
|
|
|
|
|
sleep(renewalPollMs)
|
|
|
|
|
false
|
|
|
|
|
|
|
|
|
|
proc startMiniupnpd(proto: string) =
|
|
|
|
|
let pidFile = "/tmp/plum-test/miniupnpd-" & proto & ".pid"
|
|
|
|
|
let confFile = "/tmp/plum-test/miniupnpd-" & proto & ".conf"
|
|
|
|
|
let logFile = "/tmp/plum-test/miniupnpd-" & proto & ".log"
|
2026-05-26 14:47:35 +04:00
|
|
|
let binary = if proto == "natpmp": "miniupnpd-natpmponly" else: "miniupnpd"
|
2026-05-26 14:28:09 +04:00
|
|
|
|
|
|
|
|
discard execShellCmd(
|
|
|
|
|
binary & " -d -f " & confFile & " >> " & logFile & " 2>&1 & echo $! > " & pidFile
|
|
|
|
|
)
|
2026-05-18 15:16:27 +04:00
|
|
|
|
2026-05-18 11:43:28 +04:00
|
|
|
suite "plum - " & miniupnp_protocol & " using miniupnp":
|
|
|
|
|
test "createMapping TCP and destroyMapping":
|
2026-05-26 14:28:09 +04:00
|
|
|
require init(discoverTimeout = discoverMs, logLevel = logLevel).isOk()
|
2026-05-26 14:47:35 +04:00
|
|
|
defer:
|
|
|
|
|
discard cleanup()
|
2026-05-26 14:28:09 +04:00
|
|
|
|
|
|
|
|
let r = waitFor createMapping(TCP, 8101, timeout = mappingTimeout)
|
|
|
|
|
require r.isOk()
|
|
|
|
|
let res = r.value
|
2026-05-26 14:47:35 +04:00
|
|
|
defer:
|
|
|
|
|
destroyMapping(res.id)
|
2026-05-26 14:28:09 +04:00
|
|
|
|
2026-05-26 14:47:35 +04:00
|
|
|
checkpoint miniupnp_protocol & " TCP: " & res.mapping.externalHost & ":" &
|
|
|
|
|
$res.mapping.externalPort
|
2026-05-26 14:28:09 +04:00
|
|
|
check res.mapping.externalPort > 0
|
|
|
|
|
check res.mapping.externalHost.len > 0
|
|
|
|
|
check hasMapping(res.id)
|
|
|
|
|
|
|
|
|
|
when miniupnp_protocol == "upnp":
|
|
|
|
|
check res.mapping.mappingProtocol == UPnP
|
|
|
|
|
elif miniupnp_protocol == "natpmp":
|
|
|
|
|
check res.mapping.mappingProtocol == NatPmp
|
|
|
|
|
else:
|
|
|
|
|
check res.mapping.mappingProtocol == PCP
|
2026-05-14 12:23:10 +04:00
|
|
|
|
2026-05-18 11:43:28 +04:00
|
|
|
test "createMapping UDP and destroying":
|
2026-05-26 14:28:09 +04:00
|
|
|
require init(discoverTimeout = discoverMs, logLevel = logLevel).isOk()
|
2026-05-26 14:47:35 +04:00
|
|
|
defer:
|
|
|
|
|
discard cleanup()
|
2026-05-26 14:28:09 +04:00
|
|
|
|
|
|
|
|
let r = waitFor createMapping(UDP, 8090, timeout = mappingTimeout)
|
|
|
|
|
require r.isOk()
|
|
|
|
|
|
|
|
|
|
let res = r.value
|
2026-05-26 14:47:35 +04:00
|
|
|
defer:
|
|
|
|
|
destroyMapping(res.id)
|
2026-05-26 14:28:09 +04:00
|
|
|
|
2026-05-26 14:47:35 +04:00
|
|
|
checkpoint miniupnp_protocol & " UDP: " & res.mapping.externalHost & ":" &
|
|
|
|
|
$res.mapping.externalPort
|
2026-05-26 14:28:09 +04:00
|
|
|
check res.mapping.externalPort > 0
|
|
|
|
|
|
|
|
|
|
when miniupnp_protocol == "upnp":
|
|
|
|
|
check res.mapping.mappingProtocol == UPnP
|
|
|
|
|
elif miniupnp_protocol == "natpmp":
|
|
|
|
|
check res.mapping.mappingProtocol == NatPmp
|
|
|
|
|
else:
|
|
|
|
|
check res.mapping.mappingProtocol == PCP
|
|
|
|
|
|
|
|
|
|
test "mapping is renewed after miniupnpd restart":
|
|
|
|
|
require init(
|
2026-05-26 14:47:35 +04:00
|
|
|
discoverTimeout = discoverMs, logLevel = logLevel, recheckPeriod = recheckMs
|
|
|
|
|
)
|
|
|
|
|
.isOk()
|
|
|
|
|
defer:
|
|
|
|
|
discard cleanup()
|
2026-05-26 14:28:09 +04:00
|
|
|
|
|
|
|
|
gRenewed.store(false)
|
|
|
|
|
|
|
|
|
|
let r = waitFor createMapping(
|
|
|
|
|
TCP, 8301, timeout = mappingTimeout, onStateChange = onMappingRenewal
|
|
|
|
|
)
|
|
|
|
|
require r.isOk()
|
|
|
|
|
let res = r.value
|
2026-05-26 14:47:35 +04:00
|
|
|
defer:
|
|
|
|
|
destroyMapping(res.id)
|
2026-05-26 14:28:09 +04:00
|
|
|
|
|
|
|
|
when miniupnp_protocol == "natpmp":
|
|
|
|
|
# Let the server epoch advance so the restart is detectable
|
|
|
|
|
sleep(natpmpEpochMs)
|
|
|
|
|
|
|
|
|
|
stopMiniupnpd(miniupnp_protocol)
|
|
|
|
|
|
|
|
|
|
when miniupnp_protocol == "upnp":
|
|
|
|
|
sleep(upnpRestartMs)
|
|
|
|
|
|
|
|
|
|
startMiniupnpd(miniupnp_protocol)
|
|
|
|
|
|
|
|
|
|
check waitRenewal()
|