mirror of
https://github.com/logos-storage/nim-libplum.git
synced 2026-06-13 20:49:29 +00:00
Update project
This commit is contained in:
parent
21e98b41a1
commit
d1470443fa
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +1,4 @@
|
||||
[submodule "vendor/libplum"]
|
||||
path = vendor/libplum
|
||||
url = file:///home/arnaud/Work/2-towns/libplum
|
||||
url = https://github.com/2-towns/libplum.git
|
||||
branch = feat/configurable-timeouts
|
||||
|
||||
69
README.md
69
README.md
@ -6,20 +6,6 @@ libplum tries each protocol in order (PCP → NAT-PMP → UPnP-IGD) and falls ba
|
||||
|
||||
## Installation
|
||||
|
||||
Clone with submodules:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules <REPO_URL>
|
||||
```
|
||||
|
||||
Or after cloning:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Install via Nimble:
|
||||
|
||||
```bash
|
||||
nimble install
|
||||
```
|
||||
@ -31,11 +17,15 @@ import chronos
|
||||
import libplum/plum
|
||||
|
||||
proc main() {.async.} =
|
||||
check init().isOk()
|
||||
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
|
||||
@ -47,7 +37,25 @@ proc main() {.async.} =
|
||||
waitFor main()
|
||||
```
|
||||
|
||||
See the [examples](examples) directory for a complete example.
|
||||
See [examples/port_mapping.nim](examples/port_mapping.nim) for a complete example that pauses between add and remove so you can verify the mapping on your router:
|
||||
|
||||
```bash
|
||||
nim c -r examples/port_mapping.nim
|
||||
```
|
||||
|
||||
### Timeouts
|
||||
|
||||
Pass timeout options to `init` to control how long discovery and mapping wait:
|
||||
|
||||
```nim
|
||||
discard init(discoverTimeout = 5000, mappingTimeout = 10000)
|
||||
```
|
||||
|
||||
Pass a `timeout` to `createMapping` to control the overall wait:
|
||||
|
||||
```nim
|
||||
let r = await createMapping(TCP, 8080, timeout = seconds(15))
|
||||
```
|
||||
|
||||
### Ongoing state changes
|
||||
|
||||
@ -60,16 +68,29 @@ proc onStateChange(state: PlumState, mapping: PlumMapping) {.cdecl, raises: [],
|
||||
let r = await createMapping(TCP, 8080, onStateChange = onStateChange)
|
||||
```
|
||||
|
||||
### Configurable timeouts
|
||||
|
||||
The discovery and mapping timeouts can be configured via `init`:
|
||||
### Checking mapping state
|
||||
|
||||
```nim
|
||||
discard init(
|
||||
discoverTimeout = 5000, # ms, default 10000
|
||||
mappingTimeout = 5000, # ms, default 10000
|
||||
recheckPeriod = 60000, # ms, default 300000
|
||||
)
|
||||
if hasMapping(id):
|
||||
echo "mapping is still active"
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
See [api.md](api.md) for the full API reference.
|
||||
|
||||
## Testing
|
||||
|
||||
Basic tests run without a router:
|
||||
|
||||
```bash
|
||||
nimble test
|
||||
```
|
||||
|
||||
To run tests against a real NAT device:
|
||||
|
||||
```bash
|
||||
NAT_TEST_PLUM=1 nimble test
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
113
api.md
Normal file
113
api.md
Normal file
@ -0,0 +1,113 @@
|
||||
# API Reference
|
||||
|
||||
## Types
|
||||
|
||||
```nim
|
||||
type PlumProtocol* = enum
|
||||
TCP
|
||||
UDP
|
||||
```
|
||||
|
||||
```nim
|
||||
type PlumState* = enum
|
||||
Destroyed
|
||||
Pending
|
||||
Success
|
||||
Failure
|
||||
Destroying
|
||||
```
|
||||
|
||||
```nim
|
||||
type PlumMapping* = object
|
||||
protocol*: PlumProtocol
|
||||
internalPort*: uint16
|
||||
externalPort*: uint16
|
||||
externalHost*: string
|
||||
```
|
||||
|
||||
```nim
|
||||
type MappingResult* = object
|
||||
id*: cint
|
||||
mapping*: PlumMapping
|
||||
```
|
||||
|
||||
```nim
|
||||
type PlumStateCallback* =
|
||||
proc(state: PlumState, mapping: PlumMapping) {.cdecl, raises: [], gcsafe.}
|
||||
```
|
||||
|
||||
## Procedures
|
||||
|
||||
### init
|
||||
|
||||
```nim
|
||||
proc init*(
|
||||
logLevel: plum_log_level_t = PLUM_LOG_LEVEL_NONE,
|
||||
discoverTimeout: int = 0,
|
||||
mappingTimeout: int = 0,
|
||||
recheckPeriod: int = 0
|
||||
): Result[void, string]
|
||||
```
|
||||
|
||||
Initializes the library and starts the internal thread. Must be called before any other proc.
|
||||
|
||||
- `logLevel`: verbosity of internal logs (default: none)
|
||||
- `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)
|
||||
|
||||
### cleanup
|
||||
|
||||
```nim
|
||||
proc cleanup*(): Result[void, string]
|
||||
```
|
||||
|
||||
Stops the internal thread and releases all resources. Returns an error if called more than once or before `init`.
|
||||
|
||||
### createMapping
|
||||
|
||||
```nim
|
||||
proc createMapping*(
|
||||
protocol: PlumProtocol,
|
||||
internalPort: uint16,
|
||||
externalPort: uint16 = 0,
|
||||
timeout: Duration = seconds(30),
|
||||
onStateChange: PlumStateCallback = nil
|
||||
): Future[Result[MappingResult, string]]
|
||||
```
|
||||
|
||||
Requests a port mapping from the NAT device. Tries PCP, then NAT-PMP, then UPnP-IGD in order.
|
||||
|
||||
- `protocol`: `TCP` or `UDP`
|
||||
- `internalPort`: the local port to map
|
||||
- `externalPort`: preferred external port, or 0 to let the router choose
|
||||
- `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 an error if no NAT device is found, the mapping fails, or the timeout expires.
|
||||
|
||||
### destroyMapping
|
||||
|
||||
```nim
|
||||
proc destroyMapping*(id: cint)
|
||||
```
|
||||
|
||||
Removes a mapping. Must be called exactly once after a successful `createMapping`.
|
||||
|
||||
### hasMapping
|
||||
|
||||
```nim
|
||||
proc hasMapping*(id: cint): bool
|
||||
```
|
||||
|
||||
Returns true if the mapping exists and has not been destroyed yet.
|
||||
|
||||
### getLocalAddress
|
||||
|
||||
```nim
|
||||
proc getLocalAddress*(): Result[string, string]
|
||||
```
|
||||
|
||||
Returns the local IP address as seen from the network interface. Useful to check whether the machine already has a public IP (in which case no NAT mapping is needed).
|
||||
@ -1,7 +1,6 @@
|
||||
switch("threads", "on")
|
||||
|
||||
# begin Nimble config (version 2)
|
||||
--noNimblePath
|
||||
when withDir(thisDir(), system.fileExists("nimble.paths")):
|
||||
include "nimble.paths"
|
||||
# end Nimble config
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
# those terms.
|
||||
|
||||
# This example creates a TCP port mapping via PCP, NAT-PMP, or UPnP-IGD,
|
||||
# prints the external address, and destroys the mapping before exiting.
|
||||
# prints the external address so you can verify it on the router,
|
||||
# then waits for Enter before removing the mapping.
|
||||
|
||||
import chronos
|
||||
import libplum/plum
|
||||
@ -20,6 +21,7 @@ proc main() {.async.} =
|
||||
|
||||
echo "Local address: ", getLocalAddress().valueOr("<unknown>")
|
||||
|
||||
# --- Add mapping ---
|
||||
let r = await createMapping(TCP, 8080)
|
||||
if r.isErr():
|
||||
echo "createMapping failed: ", r.error
|
||||
@ -28,11 +30,16 @@ proc main() {.async.} =
|
||||
|
||||
let res = r.value
|
||||
echo "Mapping created:"
|
||||
echo " external host: ", res.mapping.externalHost
|
||||
echo " external port: ", res.mapping.externalPort
|
||||
echo " external: ", res.mapping.externalHost, ":", res.mapping.externalPort
|
||||
echo " internal port: ", res.mapping.internalPort
|
||||
echo ""
|
||||
echo "Verify the mapping on your router, then press Enter to remove it."
|
||||
try: discard readLine(stdin)
|
||||
except IOError: discard
|
||||
|
||||
# --- Remove mapping ---
|
||||
destroyMapping(res.id)
|
||||
echo "Mapping removed."
|
||||
|
||||
let cleanupRes = cleanup()
|
||||
if cleanupRes.isErr():
|
||||
|
||||
@ -10,7 +10,8 @@ installDirs = @["libplum", "vendor"]
|
||||
### Dependencies
|
||||
requires "nim >= 1.6.0",
|
||||
"results >= 0.4.0",
|
||||
"chronos >= 4.2.0 & < 5.0.0"
|
||||
"chronos >= 4.2.0 & < 5.0.0",
|
||||
"unittest2"
|
||||
|
||||
proc compileStaticLibraries() =
|
||||
withDir "vendor/libplum":
|
||||
@ -22,6 +23,7 @@ task buildBundledLibs, "build bundled libraries":
|
||||
|
||||
task test, "run tests":
|
||||
compileStaticLibraries()
|
||||
exec("nimble setup")
|
||||
exec("nim c -r tests/test_plum.nim")
|
||||
|
||||
before install:
|
||||
|
||||
@ -66,9 +66,12 @@ type
|
||||
|
||||
# Define the config struct, passed by copy (usual for struct).
|
||||
plum_config_t* {.importc: "plum_config_t", header: "plum.h", bycopy.} = object
|
||||
log_level* {.importc: "log_level".}: plum_log_level_t
|
||||
log_callback* {.importc: "log_callback".}: plum_log_callback_t
|
||||
dummytls_domain* {.importc: "dummytls_domain".}: cstring
|
||||
log_level* {.importc: "log_level".}: plum_log_level_t
|
||||
log_callback* {.importc: "log_callback".}: plum_log_callback_t
|
||||
dummytls_domain* {.importc: "dummytls_domain".}: cstring
|
||||
discover_timeout* {.importc: "discover_timeout".}: cint # msecs, 0 means use default (10000)
|
||||
mapping_timeout* {.importc: "mapping_timeout".}: cint # msecs, 0 means use default (10000)
|
||||
recheck_period* {.importc: "recheck_period".}: cint # msecs, 0 means use default (300000)
|
||||
|
||||
# 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
|
||||
|
||||
@ -103,7 +103,7 @@ proc mappingCallback(id: cint, state: plum_state_t,
|
||||
protocol: PlumProtocol(raw[].protocol.int),
|
||||
internalPort: raw[].internal_port,
|
||||
externalPort: raw[].external_port,
|
||||
externalHost: $cast[cstring](unsafeAddr raw[].external_host[0])
|
||||
externalHost: $cast[cstring](addr raw[].external_host)
|
||||
)
|
||||
|
||||
if not handle.resolved.exchange(true):
|
||||
@ -118,13 +118,21 @@ proc mappingCallback(id: cint, state: plum_state_t,
|
||||
if not handle.onStateChange.isNil:
|
||||
handle.onStateChange(plumState, mapping)
|
||||
|
||||
proc init*(logLevel = PLUM_LOG_LEVEL_NONE): Result[void, string] {.raises: [].} =
|
||||
proc init*(
|
||||
logLevel: plum_log_level_t = PLUM_LOG_LEVEL_NONE,
|
||||
discoverTimeout: int = 0,
|
||||
mappingTimeout: int = 0,
|
||||
recheckPeriod: int = 0
|
||||
): Result[void, string] {.raises: [].} =
|
||||
## init MUST be called to setup internal plum thread (plum_init).
|
||||
|
||||
var config = plum_config_t(
|
||||
log_level: logLevel,
|
||||
log_callback: nil,
|
||||
dummytls_domain: nil
|
||||
dummytls_domain: nil,
|
||||
discover_timeout: discoverTimeout.cint,
|
||||
mapping_timeout: mappingTimeout.cint,
|
||||
recheck_period: recheckPeriod.cint
|
||||
)
|
||||
|
||||
let res = plum_init(addr config)
|
||||
@ -146,6 +154,7 @@ proc createMapping*(
|
||||
protocol: PlumProtocol,
|
||||
internalPort: uint16,
|
||||
externalPort: uint16 = 0,
|
||||
timeout: Duration = seconds(30),
|
||||
onStateChange: PlumStateCallback = nil
|
||||
): Future[Result[MappingResult, string]] {.async: (raises: [CancelledError]).} =
|
||||
let signal = ThreadSignalPtr.new().valueOr:
|
||||
@ -174,24 +183,23 @@ proc createMapping*(
|
||||
var completed = false
|
||||
try:
|
||||
# Wait for the callback to fireSync
|
||||
completed = await withTimeout(signal.wait(), seconds(30))
|
||||
completed = await withTimeout(signal.wait(), timeout)
|
||||
except CancelledError:
|
||||
# CancelledError skips the lines below and propagates after finally.
|
||||
raise
|
||||
finally:
|
||||
if not completed:
|
||||
# The signal reached timeout or was cancelled,
|
||||
# we cannot close it now otherwise the pending operation
|
||||
# might trigger a memory issue.
|
||||
# When entering into DESTROYING state, libplum will ignore
|
||||
# the pending operation so closing the signal in the DESTROYED
|
||||
# callback is safe.
|
||||
# Timeout or cancellation: we cannot close the signal here because
|
||||
# the C callback may fire later and call fireSync on it.
|
||||
# Mark the handle as abandoned so the DESTROYED callback closes it.
|
||||
handle.abandoned.store(true)
|
||||
discard plum_destroy_mapping(id)
|
||||
else:
|
||||
# The signal is completed, we can close it
|
||||
# Signal fired normally, safe to close.
|
||||
discard signal.close()
|
||||
|
||||
if not completed:
|
||||
|
||||
# Reached only when completed = true (CancelledError skips this).
|
||||
if not completed:
|
||||
return err("plum: mapping " & $id & " timed out")
|
||||
|
||||
if handle.resolvedState == Success:
|
||||
@ -204,11 +212,16 @@ proc destroyMapping*(id: cint) {.raises: [].} =
|
||||
## Must be called exactly once after a successful createMapping.
|
||||
discard plum_destroy_mapping(id)
|
||||
|
||||
proc hasMapping*(id: cint): bool {.raises: [].} =
|
||||
## Returns true if the mapping exists and has not been destroyed yet.
|
||||
withSafeLock:
|
||||
result = id in activeMappings
|
||||
|
||||
proc getLocalAddress*(): Result[string, string] {.raises: [].} =
|
||||
var buf = newString(PLUM_MAX_ADDRESS_LEN)
|
||||
let res = plum_get_local_address(buf.cstring, buf.len.csize_t)
|
||||
if res >= 0:
|
||||
buf.setLen(min(res, PLUM_MAX_ADDRESS_LEN))
|
||||
buf.setLen(min(res.int, PLUM_MAX_ADDRESS_LEN))
|
||||
ok(buf)
|
||||
else:
|
||||
err("plum_get_local_address failed: " & $res)
|
||||
|
||||
1
tests/nim.cfg
Normal file
1
tests/nim.cfg
Normal file
@ -0,0 +1 @@
|
||||
-p:".."
|
||||
@ -31,10 +31,17 @@ suite "plum":
|
||||
check r.value.len > 0
|
||||
discard cleanup()
|
||||
|
||||
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
|
||||
|
||||
discard init()
|
||||
let r = waitFor createMapping(UDP, 12345)
|
||||
let r = waitFor createMapping(UDP, 12345, timeout = seconds(5))
|
||||
check r.isErr()
|
||||
discard cleanup()
|
||||
|
||||
@ -44,30 +51,31 @@ suite "plum - NAT port mapping (requires NAT_TEST_PLUM=1)":
|
||||
skip()
|
||||
return
|
||||
|
||||
check init().isOk()
|
||||
check init(discoverTimeout = 15000).isOk()
|
||||
|
||||
let r = waitFor createMapping(TCP, 8101)
|
||||
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 res = r.value
|
||||
check res.mapping.externalPort > 0
|
||||
check res.mapping.externalHost.len > 0
|
||||
|
||||
destroyMapping(res.id)
|
||||
check cleanup().isOk()
|
||||
discard cleanup()
|
||||
|
||||
test "createMapping UDP":
|
||||
if getEnv("NAT_TEST_PLUM") != "1":
|
||||
skip()
|
||||
return
|
||||
|
||||
check init().isOk()
|
||||
check init(discoverTimeout = 2000).isOk()
|
||||
|
||||
let r = waitFor createMapping(UDP, 8090)
|
||||
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)
|
||||
|
||||
let res = r.value
|
||||
check res.mapping.externalPort > 0
|
||||
|
||||
destroyMapping(res.id)
|
||||
check cleanup().isOk()
|
||||
discard cleanup()
|
||||
|
||||
2
vendor/libplum
vendored
2
vendor/libplum
vendored
@ -1 +1 @@
|
||||
Subproject commit ff91874909882e0ba708a84cd9f1cf5761631869
|
||||
Subproject commit 7b3639bcf973ff9e3de554545f703f5a595a0536
|
||||
Loading…
x
Reference in New Issue
Block a user