Skip to content

Releases: jjacke13/hyperdht-cpp

v0.4.0 — Windows port + CGNAT holepunch + ESP32 single-socket

07 May 22:36

Choose a tag to compare

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_CXX option for downstream C++ consumers that need internal classes; default-OFF keeps the shared library's public ABI tight (just the C FFI marked with HYPERDHT_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 pathexamples/cpp/client.cpp now correctly calls hyperdht_stream_close() after receiving the echo and tears the DHT down in on_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_ms in hyperdht_connect_opts_t work as before.
  • Build: new CMake option HYPERDHT_EMBEDDED is 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). BlindRelay is 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

02 May 12:41

Choose a tag to compare

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_string rejects non-hex characters
  • Version bumped across CMake + all Nix derivations

fixing around

27 Apr 14:54

Choose a tag to compare

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

21 Apr 19:58

Choose a tag to compare

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_t wakeup 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_t was freed with the PoolSocket while libuv still referenced it in the close queue. Heap-allocated now, freed in the uv_close callback. 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_connect are 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.cppconnect.cpp + dht_storage.cpp + dht_network.cpp; hyperdht_api.cppffi_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

19 Apr 16:53

Choose a tag to compare

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