2026-06-05 11:32:05 +04:00
2026-05-19 11:45:20 +04:00
2026-05-14 16:40:10 +04:00
2026-06-05 11:32:05 +04:00
2026-06-01 17:22:10 +04:00
2026-05-19 14:15:26 +04:00
2026-05-14 12:23:10 +04:00
2026-05-14 16:40:10 +04:00
2026-06-05 10:06:06 +04:00
2026-05-14 16:40:10 +04:00
2026-05-19 14:15:26 +04:00
2026-05-14 12:23:10 +04:00
2026-05-14 12:23:10 +04:00

nim-libplum

Nim binding for libplum, a portable C library for NAT port mapping via PCP, NAT-PMP, and UPnP-IGD.

libplum tries each protocol in order (PCP → NAT-PMP → UPnP-IGD) and falls back automatically. If the local address is already public, it uses it directly.

Installation

git submodule update --init
nimble install

Usage

import chronos
import libplum/plum

proc main() {.async.} =
  let initRes = init()
  if initRes.isErr():
    echo "init failed: ", initRes.error
    return

  let r = await createMapping(TCP, 8080)
  if r.isErr():
    echo "failed: ", r.error
    discard cleanup()
    return

  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()

waitFor main()

See examples/port_mapping.nim for a complete example that pauses between add and remove so you can verify the mapping on your router:

nim c -r examples/port_mapping.nim

Timeouts

Pass timeout options to init to control how long discovery and mapping wait:

discard init(discoverTimeout = 5000, mappingTimeout = 10000)

Pass a timeout to createMapping to control the overall wait:

let r = await createMapping(TCP, 8080, timeout = seconds(15))

Ongoing state changes

Pass an onStateChange callback to createMapping to be notified when the mapping is renewed or lost:

proc onStateChange(state: PlumState, mapping: PlumMapping) {.cdecl, raises: [], gcsafe.} =
  echo "state changed: ", state, " external: ", mapping.externalHost, ":", mapping.externalPort

let r = await createMapping(TCP, 8080, onStateChange = onStateChange)

Checking mapping state

if hasMapping(id):
  echo "mapping is still active"

API

See api.md for the full API reference.

Testing

Basic tests run without a router:

nimble test

Integration tests run miniupnpd inside a Docker/Podman container and exercise the PCP, NAT-PMP, and UPnP-IGD flows. The container needs NET_ADMIN to create a dummy network interface (plum-wan) with a public IP (1.2.3.4). This is required because miniupnpd disables port forwarding when the external interface has a private/RFC1918 address, which would be the case for any container network interface. Tests run inside the container (not with --network host) so the dummy interface and route changes stay isolated from the host network.

nimble testIntegration

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.

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
TEST_VERBOSE=1 MINIUPNPD_VERBOSE=1 LIBPLUM_VERBOSE=1 nimble testIntegration

Development

Format the code with nph:

nimble format

Updating the libplum submodule

The async wrapper relies on a libplum behavior verified in vendor/libplum/src/client.c: the DESTROYED callback fires exactly once for every created mapping, either on explicit destroy or during plum_cleanup (synchronously, before it returns). createMapping waits for this callback to safely reclaim its internal signal. When bumping the submodule, re-check that this still holds.

License

Licensed and distributed under either of

at your option.

Description
Nim bindings for libplum
Readme
Languages
Nim 62.2%
C 22%
Shell 8.1%
Dockerfile 7.7%