Releases: jjacke13/hyperdht-cpp
v0.4.0 — Windows port + CGNAT holepunch + ESP32 single-socket
Highlights
Windows x86_64 platform support
First-class Windows build via vcpkg + MSVC, dedicated CI workflow (.github/workflows/windows.yml) that builds both static and shared variants on every push. libudx was patched for MSVC C11 alignas and a couple of Elvis-operator usages; Noise zero-sized arrays replaced with nullptr sentinels.
CGNAT holepunch fix (load-bearing for mobile)
Android-on-carrier-data was failing at HOLEPUNCH_FAILED (-5) while the same phone running JS hyperdht in Termux connected fine. Root cause: C++ client was sending the stale announce-time peer_address in Round 1's remoteAddress field; JS server's fast-mode punch trigger (server.js:530-538) checks hasSameAddr(p.nat.addresses, remoteAddress) against its currently sampled NAT addresses — stale port → no match → fast-mode skipped → fall through to slow probe loop that never lands on CGNAT.
Fix (6c2383b): capture the relay's fresh observation of where the server replied (hs.peerAddress) into HandshakeResult::server_address, and use it as the holepunch target. Reverse probe now arrives within ~100 ms of Round 1, stream OPEN ~150 ms after.
Plus 8 supporting parity fixes for the fallback path: NAT sampling concurrent with Round 1, retry Round 1 once on UNKNOWN firewall, retry PoolSocket::request 3x with shared adaptive timeout, require 4 NAT samples before classifying CONSISTENT, drop the 15s global cap, drop stale server announce from sampling targets, stop dispatching new handshakes after first success, cpp-reviewer follow-ups.
ESP32 single-socket (LAN regression fix)
After v0.3.0's dual-socket refactor, same-LAN connections to the ESP32 echo server stopped working — UDP packets reached server_socket_ (fd=49) at the lwIP shim layer but on_recv_server never fired. ESP32 also never goes persistent (always behind home WiFi NAT, always ephemeral). Both motivate single-socket mode under HYPERDHT_EMBEDDED.
Fix (e5a8507): collapse to one UDP socket on EMBEDDED. 7 #ifdef blocks across rpc.hpp + rpc.cpp (accessors, ctor, bind, port, check_persistent, close). Desktop builds completely unaffected.
Other changes
- nospoon example extracted to its own repo (nospoon). The
examples/cpp/nospoon/directory and its CI/Wintun scaffolding live there now. The C FFI surface stays here. - CMake
HYPERDHT_EXPORT_CXXoption for downstream C++ consumers that need internal classes; default-OFF keeps the shared library's public ABI tight (just the C FFI marked withHYPERDHT_API). - Android VpnService.protect() support — new FFI
hyperdht_get_socket_fd()exposes the UDP socket fds so VPN apps can route DHT traffic outside the tunnel and avoid recursion. - Example C++ client close path —
examples/cpp/client.cppnow correctly callshyperdht_stream_close()after receiving the echo and tears the DHT down inon_close. Process exits 0 in a couple seconds; previously the stream stayed half-open until UDX timed out, with the peer fielding TLP retransmissions during the wait. - Misc fixes: swallow second Ctrl+C during destroy drain, changeRemote docs clarification.
Verification
- Tests: 578/578 unit pass on Linux x86_64, ASAN/UBSan clean.
- Live tested: JS client → C++ server, JS client → ESP32 echo server (LAN + WAN), C++ client → ESP32 echo server (LAN-shortcut path) — all pass on the 0.4.0 build.
- Windows CI: green on every push.
Migration
- API: no breaking changes. C FFI ABI is unchanged.
relay_through/relay_keep_alive_msinhyperdht_connect_opts_twork as before. - Build: new CMake option
HYPERDHT_EMBEDDEDis OFF by default — ESP32-only, doesn't affect desktop builds. - Linker: desktop builds compile and link the same set of files as v0.3.1 except for the nospoon example (gone).
BlindRelayis fully present (linker dead-code elimination handles the unused server-side classes on EMBEDDED).
Full commit log
git log v0.3.1..v0.4.0 --oneline — 26 commits across 2 merged feature branches (fix/cgnat-holepunch, feat/windows-ci) plus feat/embedded-single-socket.
v0.3.1 — hardened & reusable
Security Audit
62 out of 64 findings fixed across 6 categories:
- Crypto: low-order point rejection in Noise DH, key material zeroing on all destructors and FFI boundaries, nonce exhaustion guard, unauthenticated holepunch fallback removed
- Input validation: safe pointer arithmetic in protomux, channel ID truncation checks, varint range validation, relay count cap, buffer decode cap, port 0 filtering
- Resource exhaustion: caps on pending queue, seen map, announce targets, value sizes, delayed timers, relay pairings (with 30s TTL), handshake sessions
- Lifecycle/UAF: weak_ptr sentinels in async callbacks, null guards on pool socket close, destructor guards, shared closed_flag for drain callbacks
- FFI/JNI: keypair zeroing on all 12 sites, atomic double-call protection, global ref tracking + cleanup, firewall fail-closed on exception
- Network: FIND_NODE and DOWN_HINT rate limiting (1/sec/IP)
reusableSocket
Server now advertises udx.reusableSocket in the Noise handshake payload. JS clients cache the UDX route after the first connection and skip holepunch on reconnect — reduces per-connection latency from 5-15s to <1s for web apps behind NAT.
- Configurable per-server via C++ / C FFI / Python
- Matches JS holesail's
createServer({ reusableSocket: true }) - Default: false (matching JS HyperDHT)
Other fixes
- Python wrapper:
destroy()uses UV_RUN_ONCE drain so Ctrl+C works - holesail-py: fast shutdown, disconnect logging, heartbeat diagnostics
- Nospoon coexistence: C++ and JS DHT instances work side-by-side
- IPv6
from_stringrejects non-hex characters - Version bumped across CMake + all Nix derivations
fixing around
Dual-socket architecture
client_socket_(ephemeral) +server_socket_(persistent) matching JS dht-rpc- Firewall probe: PING_NAT from client asking remote to reply to server; port-preservation check
- Re-announce + relay cleanup after persistent transition
- Routing table ID parity: BLAKE2b(host,port) matching JS
ESP32-S3 port
- libuv-esp32 shim for FreeRTOS
- Cross-compile full library, echo test on real hardware
External contributions (Luke Burns )
- PR #1: server UAF in blind-relay callback chain
- PR #2: LRU cache gc() leaked entries after get() promotion
- PR #3: datagram C FFI + NS_SEND constant fix
Android fixes
- JNI global ref leaks, lifecycle, threading (6 issues)
- Post-persistent echo: UV_RUN_NOWAIT flush for deferred UDX writes
- UI freeze: close DHT on background thread, not UI thread
- Debug instrumentation: DHT_LOG routes to logcat on Android
- CI builds debug libhyperdht.a with HYPERDHT_DEBUG=ON
Stats
35 src files, 31 headers, ~24k lines C++, 584 tests, 84 C FFI functions
v0.2.0 — Punching Around
Highlights
Android / Kotlin Wrapper
- Full Kotlin/JNI wrapper with coroutine API (
suspend fun connect(),Flow<Stream>) connectAndOpenStream— opens encrypted stream atomically inside the connect callback- Thread-safe stream operations via
uv_async_twakeup handle - Debug JNI build variant (
libhyperdht_jni_debug.so) with logcat logging - Android echo test app (
examples/android/) - CI job cross-compiles for Android ARM64 (NDK 26, API 26)
Bug Fixes
- fix: PoolSocket use-after-free — embedded
udx_socket_twas freed with the PoolSocket while libuv still referenced it in the close queue. Heap-allocated now, freed in theuv_closecallback. Crashed on GrapheneOS (hardened_malloc unmaps freed pages) but was silent on desktop (glibc leaves stale data intact). - fix: pool socket keepalive in rawStream firewall path — the firewall path used the pool socket without setting
socket_keepalive, so the socket could be freed while the stream was still using it. - fix: SecretStream message buffering — messages arriving before
on_connectare now queued (64 message cap) and replayed, matching JS behavior. - fix: echo server null userdata, stdout buffering, data before open
- fix: Python wrapper — ctypes GC crash (callback pointers stored on DHT instance), single uv_poll handle per fd, non-blocking write buffer
Infrastructure
- CI for x86_64, aarch64 (native ARM64 runner), macOS, Android ARM64
- Native ARM64 test binary for Android (
test_echo_native) + ASAN variant - File split:
dht.cpp→connect.cpp+dht_storage.cpp+dht_network.cpp;hyperdht_api.cpp→ffi_core.cpp+ffi_server.cpp+ffi_stream.cpp+ffi_storage.cpp - findPeer pipelining, relay address cache, route shortcut, drain callback (JS parity)
v0.1.0 — New Horizons
First public release of hyperdht-cpp — a C++ reimplementation of HyperDHT, wire-compatible with the JavaScript reference.
What it does: two devices on different networks find each other by public key and establish an end-to-end encrypted channel. No servers, no port forwarding.
What's included:
- Full HyperDHT protocol: DHT routing, NAT holepunching (4 strategies), Noise IK handshake, SecretStream encryption, blind relay fallback
- 76-function C FFI for cross-language bindings
- Python wrapper + holesail-compatible P2P tunnel
- 569 unit tests, ASAN/UBSan clean
- NixOS modules, Docker build, pkg-config
- Wire-compatible with JS
hyperdht@6.29.1— live-tested in both directions
Platforms: Linux (x86_64, aarch64). macOS builds but is untested.
Known current limitation: If you run a C++ server in the same machine with a JS server, a remote client can't connect to the C++ server
Get started: see BUILDING.md