feat: protocol filter (#15)

* Add Protocol filter

* Temporary point on fork

* Use ord instead of int
This commit is contained in:
Arnaud 2026-07-13 17:21:02 +04:00 committed by GitHub
parent de15c06943
commit fa8aadb591
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 104 additions and 43 deletions

6
.gitmodules vendored
View File

@ -1,4 +1,4 @@
[submodule "vendor/libplum"]
path = vendor/libplum
url = https://github.com/paullouisageneau/libplum.git
branch = master
path = vendor/libplum
url = https://github.com/2-towns/libplum.git
branch = feat/protocol-filter

11
api.md
View File

@ -26,6 +26,13 @@ type MappingProtocol* = enum
Direct ## no mapping needed, local address is already public
```
```nim
type ProtocolFilter* {.pure.} = enum
Any ## try all protocols (default)
PCP ## PCP/NAT-PMP only
UPnP ## UPnP only
```
```nim
type PlumMapping* = object
protocol*: PlumProtocol ## IP protocol (TCP/UDP)
@ -55,7 +62,8 @@ proc init*(
logLevel: plum_log_level_t = PLUM_LOG_LEVEL_NONE,
discoverTimeout: int = 0,
mappingTimeout: int = 0,
recheckPeriod: int = 0
recheckPeriod: int = 0,
protocol: ProtocolFilter = ProtocolFilter.Any
): Result[void, string]
```
@ -65,6 +73,7 @@ Initializes the library and starts the internal thread. Must be called before an
- `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)
- `protocol`: restrict discovery to a single family (`ProtocolFilter.PCP` for PCP/NAT-PMP, `ProtocolFilter.UPnP` for UPnP); `ProtocolFilter.Any` tries all (default)
### cleanup

View File

@ -52,35 +52,40 @@ 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_LOG_LEVEL_VERBOSE
PLUM_LOG_LEVEL_DEBUG
PLUM_LOG_LEVEL_INFO
PLUM_LOG_LEVEL_WARN
PLUM_LOG_LEVEL_ERROR
PLUM_LOG_LEVEL_FATAL
PLUM_LOG_LEVEL_NONE
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_IP_PROTOCOL_TCP
PLUM_IP_PROTOCOL_UDP
plum_protocol_t* {.importc: "plum_protocol_t", header: "plum.h", size: sizeof(cint).} = enum
PLUM_PROTOCOL_ANY
PLUM_PROTOCOL_PCP
PLUM_PROTOCOL_UPNP
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
PLUM_STATE_DESTROYED
PLUM_STATE_PENDING
PLUM_STATE_SUCCESS
PLUM_STATE_FAILURE
PLUM_STATE_DESTROYING
plum_mapping_protocol_t* {.
importc: "plum_mapping_protocol_t", header: "plum.h", size: sizeof(cint)
.} = enum
PLUM_MAPPING_PROTOCOL_UNKNOWN = 0
PLUM_MAPPING_PROTOCOL_PCP = 1
PLUM_MAPPING_PROTOCOL_NATPMP = 2
PLUM_MAPPING_PROTOCOL_UPNP = 3
PLUM_MAPPING_PROTOCOL_DIRECT = 4
PLUM_MAPPING_PROTOCOL_UNKNOWN
PLUM_MAPPING_PROTOCOL_PCP
PLUM_MAPPING_PROTOCOL_NATPMP
PLUM_MAPPING_PROTOCOL_UPNP
PLUM_MAPPING_PROTOCOL_DIRECT
# Define the callback to receive the plum logs
plum_log_callback_t* = proc(level: plum_log_level_t, message: cstring) {.cdecl.}
@ -96,6 +101,7 @@ type
# msecs, 0 means use default (10000)
recheck_period* {.importc: "recheck_period".}: cint
# msecs, 0 means use default (300000)
protocol* {.importc: "protocol".}: plum_protocol_t # PLUM_PROTOCOL_ANY means try all
# 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

View File

@ -34,31 +34,36 @@ export results
type
PlumProtocol* = enum
TCP = PLUM_IP_PROTOCOL_TCP.int
UDP = PLUM_IP_PROTOCOL_UDP.int
TCP = ord(PLUM_IP_PROTOCOL_TCP)
UDP = ord(PLUM_IP_PROTOCOL_UDP)
ProtocolFilter* {.pure.} = enum
Any = ord(PLUM_PROTOCOL_ANY)
PCP = ord(PLUM_PROTOCOL_PCP)
UPnP = ord(PLUM_PROTOCOL_UPNP)
PlumLogLevel* {.pure.} = enum
Verbose = PLUM_LOG_LEVEL_VERBOSE.int
Debug = PLUM_LOG_LEVEL_DEBUG.int
Info = PLUM_LOG_LEVEL_INFO.int
Warn = PLUM_LOG_LEVEL_WARN.int
Error = PLUM_LOG_LEVEL_ERROR.int
Fatal = PLUM_LOG_LEVEL_FATAL.int
None = PLUM_LOG_LEVEL_NONE.int
Verbose = ord(PLUM_LOG_LEVEL_VERBOSE)
Debug = ord(PLUM_LOG_LEVEL_DEBUG)
Info = ord(PLUM_LOG_LEVEL_INFO)
Warn = ord(PLUM_LOG_LEVEL_WARN)
Error = ord(PLUM_LOG_LEVEL_ERROR)
Fatal = ord(PLUM_LOG_LEVEL_FATAL)
None = ord(PLUM_LOG_LEVEL_NONE)
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
Destroyed = ord(PLUM_STATE_DESTROYED)
Pending = ord(PLUM_STATE_PENDING)
Success = ord(PLUM_STATE_SUCCESS)
Failure = ord(PLUM_STATE_FAILURE)
Destroying = ord(PLUM_STATE_DESTROYING)
MappingProtocol* = enum
Unknown = PLUM_MAPPING_PROTOCOL_UNKNOWN.int
PCP = PLUM_MAPPING_PROTOCOL_PCP.int
NatPmp = PLUM_MAPPING_PROTOCOL_NATPMP.int
UPnP = PLUM_MAPPING_PROTOCOL_UPNP.int
Direct = PLUM_MAPPING_PROTOCOL_DIRECT.int
Unknown = ord(PLUM_MAPPING_PROTOCOL_UNKNOWN)
PCP = ord(PLUM_MAPPING_PROTOCOL_PCP)
NatPmp = ord(PLUM_MAPPING_PROTOCOL_NATPMP)
UPnP = ord(PLUM_MAPPING_PROTOCOL_UPNP)
Direct = ord(PLUM_MAPPING_PROTOCOL_DIRECT)
PlumMapping* = object
protocol*: PlumProtocol
@ -161,8 +166,11 @@ proc init*(
discoverTimeout: int32 = 0,
mappingTimeout: int32 = 0,
recheckPeriod: int32 = 0,
protocol: ProtocolFilter = ProtocolFilter.Any,
): Result[void, string] =
## init MUST be called to setup internal plum thread (plum_init).
## protocol restricts discovery to a single family (PCP/NAT-PMP or UPnP);
## ProtocolFilter.Any tries all.
var config = plum_config_t(
log_level: plum_log_level_t(logLevel.int),
@ -171,6 +179,7 @@ proc init*(
discover_timeout: discoverTimeout.cint,
mapping_timeout: mappingTimeout.cint,
recheck_period: recheckPeriod.cint,
protocol: plum_protocol_t(protocol.int),
)
let res = plum_init(addr config)

View File

@ -210,3 +210,40 @@ when miniupnp_protocol != "":
# no destroyMapping on purpose: cleanup must release everything
check cleanup().isOk()
check activeMappingCount() == 0
# miniupnpd in the pcp container also serves UPnP, so a UPnP filter must
# override the default PCP-first selection.
when miniupnp_protocol == "pcp":
test "protocol filter selects UPnP even when PCP is available":
require init(
discoverTimeout = discoverMs.int32,
logLevel = logLevel,
protocol = ProtocolFilter.UPnP,
)
.isOk()
defer:
discard cleanup()
let r = waitFor createMapping(TCP, 8111, timeout = mappingTimeout)
require r.isOk()
let res = r.value
defer:
destroyMapping(res.id)
check res.mapping.mappingProtocol == UPnP
# miniupnpd in the upnp container has PCP/NAT-PMP disabled, so a PCP filter
# must not fall back to UPnP: the mapping cannot succeed.
when miniupnp_protocol == "upnp":
test "protocol filter PCP does not fall back to UPnP":
require init(
discoverTimeout = discoverMs.int32,
logLevel = logLevel,
protocol = ProtocolFilter.PCP,
)
.isOk()
defer:
discard cleanup()
let r = waitFor createMapping(TCP, 8112, timeout = mappingTimeout)
check r.isErr()

2
vendor/libplum vendored

@ -1 +1 @@
Subproject commit e4be508aad4297b190498c7e26552e0f495a9aa1
Subproject commit 3df6d2b2e37689c572b69af401a6922d9db67541