mirror of
https://github.com/logos-storage/nim-libplum.git
synced 2026-06-07 01:29:27 +00:00
Initial commit
This commit is contained in:
commit
21e98b41a1
14
.github/workflows/ci.yml
vendored
Normal file
14
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: status-im/nimbus-common-workflow/.github/workflows/common.yml@main
|
||||
with:
|
||||
test-command: |
|
||||
nimble test
|
||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
nimcache/
|
||||
|
||||
# Compiled binaries
|
||||
*.so
|
||||
*.dylib
|
||||
*.a
|
||||
*.la
|
||||
*.exe
|
||||
*.dll
|
||||
|
||||
/examples/port_mapping
|
||||
/tests/test_plum
|
||||
|
||||
nimble.develop
|
||||
nimble.paths
|
||||
nimbledeps
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "vendor/libplum"]
|
||||
path = vendor/libplum
|
||||
url = file:///home/arnaud/Work/2-towns/libplum
|
||||
201
LICENSE-APACHEv2
Normal file
201
LICENSE-APACHEv2
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Status Research & Development GmbH
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
21
LICENSE-MIT
Normal file
21
LICENSE-MIT
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Status Research & Development GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
82
README.md
Normal file
82
README.md
Normal file
@ -0,0 +1,82 @@
|
||||
# nim-libplum
|
||||
|
||||
Nim binding for [libplum](https://github.com/paullouisageneau/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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```nim
|
||||
import chronos
|
||||
import libplum/plum
|
||||
|
||||
proc main() {.async.} =
|
||||
check init().isOk()
|
||||
|
||||
let r = await createMapping(TCP, 8080)
|
||||
if r.isErr():
|
||||
echo "failed: ", r.error
|
||||
return
|
||||
|
||||
let res = r.value
|
||||
echo "external: ", res.mapping.externalHost, ":", res.mapping.externalPort
|
||||
|
||||
destroyMapping(res.id)
|
||||
discard cleanup()
|
||||
|
||||
waitFor main()
|
||||
```
|
||||
|
||||
See the [examples](examples) directory for a complete example.
|
||||
|
||||
### Ongoing state changes
|
||||
|
||||
Pass an `onStateChange` callback to `createMapping` to be notified when the mapping is renewed or lost:
|
||||
|
||||
```nim
|
||||
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)
|
||||
```
|
||||
|
||||
### Configurable timeouts
|
||||
|
||||
The discovery and mapping timeouts can be configured via `init`:
|
||||
|
||||
```nim
|
||||
discard init(
|
||||
discoverTimeout = 5000, # ms, default 10000
|
||||
mappingTimeout = 5000, # ms, default 10000
|
||||
recheckPeriod = 60000, # ms, default 300000
|
||||
)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Licensed and distributed under either of
|
||||
|
||||
* MIT license: [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT
|
||||
* Apache License, Version 2.0: [LICENSE-APACHEv2](LICENSE-APACHEv2) or http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
at your option.
|
||||
7
config.nims
Normal file
7
config.nims
Normal file
@ -0,0 +1,7 @@
|
||||
switch("threads", "on")
|
||||
|
||||
# begin Nimble config (version 2)
|
||||
--noNimblePath
|
||||
when withDir(thisDir(), system.fileExists("nimble.paths")):
|
||||
include "nimble.paths"
|
||||
# end Nimble config
|
||||
1
examples/nim.cfg
Normal file
1
examples/nim.cfg
Normal file
@ -0,0 +1 @@
|
||||
-p:".."
|
||||
41
examples/port_mapping.nim
Normal file
41
examples/port_mapping.nim
Normal file
@ -0,0 +1,41 @@
|
||||
# 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.
|
||||
|
||||
# This example creates a TCP port mapping via PCP, NAT-PMP, or UPnP-IGD,
|
||||
# prints the external address, and destroys the mapping before exiting.
|
||||
|
||||
import chronos
|
||||
import libplum/plum
|
||||
|
||||
proc main() {.async.} =
|
||||
let initRes = init()
|
||||
if initRes.isErr():
|
||||
echo "init failed: ", initRes.error
|
||||
return
|
||||
|
||||
echo "Local address: ", getLocalAddress().valueOr("<unknown>")
|
||||
|
||||
let r = await createMapping(TCP, 8080)
|
||||
if r.isErr():
|
||||
echo "createMapping failed: ", r.error
|
||||
discard cleanup()
|
||||
return
|
||||
|
||||
let res = r.value
|
||||
echo "Mapping created:"
|
||||
echo " external host: ", res.mapping.externalHost
|
||||
echo " external port: ", res.mapping.externalPort
|
||||
echo " internal port: ", res.mapping.internalPort
|
||||
|
||||
destroyMapping(res.id)
|
||||
|
||||
let cleanupRes = cleanup()
|
||||
if cleanupRes.isErr():
|
||||
echo "cleanup failed: ", cleanupRes.error
|
||||
|
||||
waitFor main()
|
||||
28
libplum.nimble
Normal file
28
libplum.nimble
Normal file
@ -0,0 +1,28 @@
|
||||
mode = ScriptMode.Verbose
|
||||
|
||||
packageName = "libplum"
|
||||
version = "0.0.1"
|
||||
author = "Status Research & Development GmbH"
|
||||
description = "Nim binding for libplum (PCP, NAT-PMP, UPnP-IGD port mapping)"
|
||||
license = "Apache License 2.0 or MIT"
|
||||
installDirs = @["libplum", "vendor"]
|
||||
|
||||
### Dependencies
|
||||
requires "nim >= 1.6.0",
|
||||
"results >= 0.4.0",
|
||||
"chronos >= 4.2.0 & < 5.0.0"
|
||||
|
||||
proc compileStaticLibraries() =
|
||||
withDir "vendor/libplum":
|
||||
exec("cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF")
|
||||
exec("cmake --build build")
|
||||
|
||||
task buildBundledLibs, "build bundled libraries":
|
||||
compileStaticLibraries()
|
||||
|
||||
task test, "run tests":
|
||||
compileStaticLibraries()
|
||||
exec("nim c -r tests/test_plum.nim")
|
||||
|
||||
before install:
|
||||
compileStaticLibraries()
|
||||
106
libplum/libplum.nim
Normal file
106
libplum/libplum.nim
Normal file
@ -0,0 +1,106 @@
|
||||
# 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.
|
||||
|
||||
import std/[os, strutils]
|
||||
|
||||
const
|
||||
rootPath = currentSourcePath.parentDir().parentDir().replace('\\', '/')
|
||||
libplumPath = rootPath & "/vendor/libplum"
|
||||
includePath = libplumPath & "/include/plum"
|
||||
libraryPath = libplumPath & "/build/libplum.a"
|
||||
{.passc: "-I" & includePath & " -DPLUM_STATIC".}
|
||||
{.passl: libraryPath.}
|
||||
# libplum declares some parameters as `const T*` in C (read-only pointer).
|
||||
# Nim has no equivalent, so the generated C code drops the `const`, causing
|
||||
# a type mismatch warning in GCC 15+. This flag suppresses that warning.
|
||||
# It is safe because we never write through those pointers.
|
||||
{.passc: "-Wno-incompatible-pointer-types".}
|
||||
|
||||
when defined(windows):
|
||||
{.passl: "-lws2_32 -liphlpapi -lbcrypt".}
|
||||
else:
|
||||
{.passl: "-lpthread".}
|
||||
|
||||
const
|
||||
PLUM_ERR_SUCCESS* = cint(0)
|
||||
PLUM_ERR_INVALID* = cint(-1)
|
||||
PLUM_ERR_FAILED* = cint(-2)
|
||||
PLUM_ERR_NOT_AVAIL* = cint(-3)
|
||||
|
||||
PLUM_MAX_HOST_LEN* = 256
|
||||
PLUM_MAX_ADDRESS_LEN* = 64
|
||||
|
||||
# Import plum enums with cint size
|
||||
|
||||
type
|
||||
plum_log_level_t* {.importc: "plum_log_level_t", header: "plum.h",
|
||||
size: sizeof(cint).} = enum
|
||||
PLUM_LOG_LEVEL_VERBOSE = 0
|
||||
PLUM_LOG_LEVEL_DEBUG = 1
|
||||
PLUM_LOG_LEVEL_INFO = 2
|
||||
PLUM_LOG_LEVEL_WARN = 3
|
||||
PLUM_LOG_LEVEL_ERROR = 4
|
||||
PLUM_LOG_LEVEL_FATAL = 5
|
||||
PLUM_LOG_LEVEL_NONE = 6
|
||||
|
||||
plum_ip_protocol_t* {.importc: "plum_ip_protocol_t", header: "plum.h",
|
||||
size: sizeof(cint).} = enum
|
||||
PLUM_IP_PROTOCOL_TCP = 0
|
||||
PLUM_IP_PROTOCOL_UDP = 1
|
||||
|
||||
plum_state_t* {.importc: "plum_state_t", header: "plum.h",
|
||||
size: sizeof(cint).} = enum
|
||||
PLUM_STATE_DESTROYED = 0
|
||||
PLUM_STATE_PENDING = 1
|
||||
PLUM_STATE_SUCCESS = 2
|
||||
PLUM_STATE_FAILURE = 3
|
||||
PLUM_STATE_DESTROYING = 4
|
||||
|
||||
# Define the callback to receive the plum logs
|
||||
plum_log_callback_t* = proc(level: plum_log_level_t, message: cstring) {.cdecl.}
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Define the callback to receive the mapping result
|
||||
plum_mapping_callback_t* =
|
||||
proc(id: cint, state: plum_state_t, mapping: ptr plum_mapping_t) {.cdecl.}
|
||||
|
||||
# Import plum functions
|
||||
|
||||
proc plum_init*(config: ptr plum_config_t): cint
|
||||
{.importc: "plum_init", header: "plum.h".}
|
||||
|
||||
proc plum_cleanup*(): cint
|
||||
{.importc: "plum_cleanup", header: "plum.h".}
|
||||
|
||||
proc plum_create_mapping*(mapping: ptr plum_mapping_t,
|
||||
callback: plum_mapping_callback_t): cint
|
||||
{.importc: "plum_create_mapping", header: "plum.h".}
|
||||
|
||||
proc plum_query_mapping*(id: cint, state: ptr plum_state_t,
|
||||
mapping: ptr plum_mapping_t): cint
|
||||
{.importc: "plum_query_mapping", header: "plum.h".}
|
||||
|
||||
proc plum_destroy_mapping*(id: cint): cint
|
||||
{.importc: "plum_destroy_mapping", header: "plum.h".}
|
||||
|
||||
proc plum_get_local_address*(buffer: cstring, size: csize_t): cint
|
||||
{.importc: "plum_get_local_address", header: "plum.h".}
|
||||
214
libplum/plum.nim
Normal file
214
libplum/plum.nim
Normal file
@ -0,0 +1,214 @@
|
||||
# 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.
|
||||
|
||||
import std/[atomics, locks, tables]
|
||||
import chronos
|
||||
import chronos/threadsync
|
||||
import results
|
||||
import ./libplum
|
||||
|
||||
export results
|
||||
|
||||
{.pragma: callback, cdecl, raises: [], gcsafe.}
|
||||
|
||||
type
|
||||
PlumProtocol* = enum
|
||||
TCP = PLUM_IP_PROTOCOL_TCP.int
|
||||
UDP = PLUM_IP_PROTOCOL_UDP.int
|
||||
|
||||
PlumState* = enum
|
||||
Destroyed = PLUM_STATE_DESTROYED.int
|
||||
Pending = PLUM_STATE_PENDING.int
|
||||
Success = PLUM_STATE_SUCCESS.int
|
||||
Failure = PLUM_STATE_FAILURE.int
|
||||
Destroying = PLUM_STATE_DESTROYING.int
|
||||
|
||||
PlumMapping* = object
|
||||
protocol*: PlumProtocol
|
||||
internalPort*: uint16
|
||||
externalPort*: uint16
|
||||
externalHost*: string
|
||||
|
||||
MappingResult* = object
|
||||
id*: cint
|
||||
mapping*: PlumMapping
|
||||
|
||||
PlumStateCallback* =
|
||||
proc(state: PlumState, mapping: PlumMapping) {.callback.}
|
||||
|
||||
MappingHandle = ref object
|
||||
signal: ThreadSignalPtr
|
||||
# Indicate that the first call of the mapping handle is
|
||||
# done. In that case, resolvedState and resolvedMapping
|
||||
# will contain the result.
|
||||
# MappingHandle can be called multiple times after that,
|
||||
# but the result will be passed through the callback.
|
||||
resolved: Atomic[bool]
|
||||
resolvedState: PlumState
|
||||
resolvedMapping: PlumMapping
|
||||
# Use abandoned pattern for memory freeing
|
||||
abandoned: Atomic[bool]
|
||||
onStateChange: PlumStateCallback
|
||||
|
||||
# libplum calls mappingCallback from its own C thread. Under refc, any thread
|
||||
# that touches Nim objects must register with the GC first.
|
||||
template foreignThreadGc(body: untyped) =
|
||||
when declared(setupForeignThreadGc): setupForeignThreadGc()
|
||||
body
|
||||
when declared(tearDownForeignThreadGc): tearDownForeignThreadGc()
|
||||
|
||||
var activeMappingsLock: Lock
|
||||
var activeMappings {.guard: activeMappingsLock.}: Table[cint, MappingHandle]
|
||||
|
||||
initLock(activeMappingsLock)
|
||||
|
||||
# We can be confident that the pattern is GC Safe using
|
||||
# a lock.
|
||||
template withSafeLock(body: untyped) =
|
||||
{.cast(gcsafe).}:
|
||||
withLock activeMappingsLock:
|
||||
body
|
||||
|
||||
proc mappingCallback(id: cint, state: plum_state_t,
|
||||
raw: ptr plum_mapping_t) {.cdecl, raises: [].} =
|
||||
## Called from libplum's internal C thread on SUCCESS, FAILURE, and DESTROYED.
|
||||
|
||||
foreignThreadGc:
|
||||
let handle = cast[MappingHandle](raw[].user_ptr)
|
||||
if handle.isNil:
|
||||
return
|
||||
|
||||
let plumState = PlumState(state.int)
|
||||
if plumState == Destroyed:
|
||||
withSafeLock:
|
||||
activeMappings.del(id)
|
||||
# The handle can be abandoned after a timeout during a
|
||||
# mapping creation.
|
||||
# In that case, destroy is called internally and the
|
||||
# signal pointer can be closed.
|
||||
if handle.abandoned.load():
|
||||
discard handle.signal.close()
|
||||
return
|
||||
|
||||
# Skip states other than Success and Failure
|
||||
if plumState notin {Success, Failure}:
|
||||
return
|
||||
|
||||
let mapping = PlumMapping(
|
||||
protocol: PlumProtocol(raw[].protocol.int),
|
||||
internalPort: raw[].internal_port,
|
||||
externalPort: raw[].external_port,
|
||||
externalHost: $cast[cstring](unsafeAddr raw[].external_host[0])
|
||||
)
|
||||
|
||||
if not handle.resolved.exchange(true):
|
||||
# If this is the first time the handle mapping
|
||||
# is called, let's set the result in the handle values,
|
||||
# and fire the signal.
|
||||
handle.resolvedState = plumState
|
||||
handle.resolvedMapping = mapping
|
||||
discard handle.signal.fireSync()
|
||||
else:
|
||||
# Otherwise, just call the callback
|
||||
if not handle.onStateChange.isNil:
|
||||
handle.onStateChange(plumState, mapping)
|
||||
|
||||
proc init*(logLevel = PLUM_LOG_LEVEL_NONE): 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
|
||||
)
|
||||
|
||||
let res = plum_init(addr config)
|
||||
if res == PLUM_ERR_SUCCESS:
|
||||
ok()
|
||||
else:
|
||||
err("plum_init failed: " & $res)
|
||||
|
||||
proc cleanup*(): Result[void, string] {.raises: [].} =
|
||||
## cleanup MUST be called to stop the thread and clean the setup.
|
||||
|
||||
let res = plum_cleanup()
|
||||
if res == PLUM_ERR_SUCCESS:
|
||||
ok()
|
||||
else:
|
||||
err("plum_cleanup failed: " & $res)
|
||||
|
||||
proc createMapping*(
|
||||
protocol: PlumProtocol,
|
||||
internalPort: uint16,
|
||||
externalPort: uint16 = 0,
|
||||
onStateChange: PlumStateCallback = nil
|
||||
): Future[Result[MappingResult, string]] {.async: (raises: [CancelledError]).} =
|
||||
let signal = ThreadSignalPtr.new().valueOr:
|
||||
return err("plum: cannot create signal: " & $error)
|
||||
|
||||
let handle = MappingHandle(
|
||||
signal: signal,
|
||||
onStateChange: onStateChange
|
||||
)
|
||||
|
||||
var req = plum_mapping_t(
|
||||
protocol: plum_ip_protocol_t(protocol.int),
|
||||
internal_port: internalPort,
|
||||
external_port: externalPort,
|
||||
user_ptr: cast[pointer](handle)
|
||||
)
|
||||
|
||||
let id = plum_create_mapping(addr req, mappingCallback)
|
||||
if id < 0:
|
||||
discard signal.close()
|
||||
return err("plum_create_mapping failed: " & $id)
|
||||
|
||||
withSafeLock:
|
||||
activeMappings[id] = handle
|
||||
|
||||
var completed = false
|
||||
try:
|
||||
# Wait for the callback to fireSync
|
||||
completed = await withTimeout(signal.wait(), seconds(30))
|
||||
except CancelledError:
|
||||
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.
|
||||
handle.abandoned.store(true)
|
||||
discard plum_destroy_mapping(id)
|
||||
else:
|
||||
# The signal is completed, we can close it
|
||||
discard signal.close()
|
||||
|
||||
if not completed:
|
||||
return err("plum: mapping " & $id & " timed out")
|
||||
|
||||
if handle.resolvedState == Success:
|
||||
return ok(MappingResult(id: id, mapping: handle.resolvedMapping))
|
||||
else:
|
||||
discard plum_destroy_mapping(id)
|
||||
return err("plum: mapping " & $id & " failed")
|
||||
|
||||
proc destroyMapping*(id: cint) {.raises: [].} =
|
||||
## Must be called exactly once after a successful createMapping.
|
||||
discard plum_destroy_mapping(id)
|
||||
|
||||
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))
|
||||
ok(buf)
|
||||
else:
|
||||
err("plum_get_local_address failed: " & $res)
|
||||
73
tests/test_plum.nim
Normal file
73
tests/test_plum.nim
Normal file
@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
||||
import std/envvars
|
||||
import unittest2
|
||||
import chronos
|
||||
import libplum/plum
|
||||
|
||||
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()
|
||||
|
||||
test "createMapping fails without router":
|
||||
# In CI with no NAT device, expect Failure or timeout — both return err.
|
||||
discard init()
|
||||
let r = waitFor createMapping(UDP, 12345)
|
||||
check r.isErr()
|
||||
discard cleanup()
|
||||
|
||||
suite "plum - NAT port mapping (requires NAT_TEST_PLUM=1)":
|
||||
test "createMapping TCP and destroyMapping":
|
||||
if getEnv("NAT_TEST_PLUM") != "1":
|
||||
skip()
|
||||
return
|
||||
|
||||
check init().isOk()
|
||||
|
||||
let r = waitFor createMapping(TCP, 8101)
|
||||
check r.isOk()
|
||||
|
||||
let res = r.value
|
||||
check res.mapping.externalPort > 0
|
||||
check res.mapping.externalHost.len > 0
|
||||
|
||||
destroyMapping(res.id)
|
||||
check cleanup().isOk()
|
||||
|
||||
test "createMapping UDP":
|
||||
if getEnv("NAT_TEST_PLUM") != "1":
|
||||
skip()
|
||||
return
|
||||
|
||||
check init().isOk()
|
||||
|
||||
let r = waitFor createMapping(UDP, 8090)
|
||||
check r.isOk()
|
||||
|
||||
let res = r.value
|
||||
check res.mapping.externalPort > 0
|
||||
|
||||
destroyMapping(res.id)
|
||||
check cleanup().isOk()
|
||||
1
vendor/libplum
vendored
Submodule
1
vendor/libplum
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit ff91874909882e0ba708a84cd9f1cf5761631869
|
||||
Loading…
x
Reference in New Issue
Block a user