Compare commits

..
Author SHA1 Message Date
Adrien Beraud ccb0b4340e transport: make private network grants permanent 2026-11-01 22:18:02 -05:00
Adrien Beraud fa78704062 transport: test permanent grants and authority transitions 2026-11-01 22:12:28 -05:00
Adrien Beraud 625625a186 tests: isolate projection scheduling from transport handshakes 2026-10-29 23:35:22 -04:00
Adrien Beraud f0d039b4a0 tests: keep persisted transport paths as filesystem paths 2026-10-29 23:28:48 -04:00
Adrien Beraud d464214d73 tests: use portable paths for persisted transport keys 2026-10-29 23:28:48 -04:00
Adrien Beraud b69c66feed network: retain session authorization and budget credential checks 2026-10-29 23:28:48 -04:00
Adrien Beraud 3d9ee6a51a tests: verify cached private authorization expires and renews 2026-10-29 23:28:48 -04:00
Adrien Beraud f71697c81b tests: cover private client authorization and cached credential limits 2026-10-29 23:28:48 -04:00
Adrien Beraud b071022283 tests: fix transport test portability on GCC and Windows 2026-10-29 23:28:48 -04:00
Adrien Beraud 487bc2fce2 meson: add optional Rust bindings build 2026-10-29 23:28:48 -04:00
Adrien Beraud 758f280651 network: validate sessions before cache admission and isolate fragments 2026-10-29 23:28:48 -04:00
Adrien Beraud 20464e6f16 tests: cover rejected sessions and cross-sender fragments 2026-10-29 23:28:48 -04:00
Adrien Beraud d211de73e0 transport: erase temporary session keys and HMAC state 2026-10-29 23:28:48 -04:00
Adrien Beraud bde661a2d4 tests: verify session key destruction erases secrets 2026-10-29 23:28:48 -04:00
Adrien Beraud 49ef8ee84d docs: describe separate rate limits 2026-10-29 23:28:48 -04:00
Adrien Beraud 390528a1af tools: disable public-key rate limit with other limits 2026-10-29 23:28:48 -04:00
Adrien Beraud 97c9d58a5c network: separate rate limits for sessions, unauthenticated requests and public-key work 2026-10-29 23:28:48 -04:00
Adrien Beraud e5903d5c94 tests: check separate request, session and public-key rate limits 2026-10-29 23:28:48 -04:00
Adrien Beraud 65bbbead34 docs: describe control-only transport establishment 2026-10-29 23:28:48 -04:00
Adrien Beraud ffaf1e2ca3 dht: establish cold searches through target-aware routing probes 2026-10-29 23:28:48 -04:00
Adrien Beraud af7d5c2770 network: finish recovery only after accepted replies 2026-10-29 23:28:48 -04:00
Adrien Beraud c14858a82a tests: exercise private routing and unreachable search peers 2026-10-29 23:28:48 -04:00
Adrien Beraud 1010a4c90f tests: cover establishment failure and legacy fallback 2026-10-29 23:28:48 -04:00
Adrien Beraud ea6bacf0a1 network: establish sessions without spending withheld request attempts 2026-10-29 23:28:48 -04:00
Adrien Beraud 36f67747d7 tests: preserve routing progress during cold session establishment 2026-10-29 23:28:48 -04:00
Adrien Beraud b093dc0907 tests: cover cold establishment and delayed recovery replies 2026-10-29 23:28:48 -04:00
Adrien Beraud a06695734d network: require a session for keyed value requests 2026-10-29 23:28:48 -04:00
Adrien Beraud 35aa47029e tests: reject unvalidated transport value requests 2026-10-29 23:28:48 -04:00
Adrien Beraud 4af12a4c2c docs: describe per-peer session recovery 2026-10-29 23:28:48 -04:00
Adrien Beraud 3f77b6b820 network: recover lost sessions per peer 2026-10-29 23:28:48 -04:00
Adrien Beraud 858867dc56 tests: check that session recovery withholds every request to the peer 2026-10-29 23:28:48 -04:00
Adrien Beraud fcb0632a41 docs: describe session recovery without payloads in Initial envelopes 2026-10-29 23:28:48 -04:00
Adrien Beraud 3ba61e7e4e network: recover lost sessions with a ping before resending requests 2026-10-29 23:28:48 -04:00
Adrien Beraud 4ef2c21ce0 docs: describe fragment sizes per envelope 2026-10-29 23:28:48 -04:00
Adrien Beraud d98c66b2c0 network: size value fragments by the envelope in use 2026-10-29 23:28:48 -04:00
Adrien Beraud dc09983b6b tests: require session recovery before resending payloads, and envelope-sized fragments 2026-10-29 23:28:48 -04:00
Adrien Beraud da00246fe8 docs: describe the fragment size calculation 2026-10-29 23:28:48 -04:00
Adrien Beraud e092e4d89f network: size value fragments from the minimum MTU
Fragments carried 1280 bytes of value data, plus the msgpack framing and,
for encrypted peers, the envelope overhead: packets exceeded the 1280-byte
IPv6 minimum MTU. Derive both fragment sizes from the 1232-byte UDP payload
that fits in it, less an upper bound of the fragment framing.
2026-10-29 23:28:48 -04:00
Adrien Beraud ed8131a3c6 tests: require value fragments to fit the IPv6 minimum MTU 2026-10-29 23:28:48 -04:00
Adrien Beraud f29c77cadf docs: state the scope of each transport limitation 2026-10-29 23:28:48 -04:00
Adrien Beraud d90988cb6a transport: save key files through temporary replacements 2026-10-29 23:28:48 -04:00
Adrien Beraud 3cf24c89d4 tests: require atomic transport-key state persistence 2026-10-29 23:28:48 -04:00
Adrien Beraud 5e7bcd1189 node: use compact fixed-width transport badges 2026-10-29 23:28:48 -04:00
Adrien Beraud 8ac6baee22 tests: require fixed-width node transport badges 2026-10-29 23:28:48 -04:00
Adrien Beraud f9bb4bcb40 node: show transport authentication state in logs 2026-10-29 23:28:48 -04:00
Adrien Beraud 35f486e035 tests: cover node transport state formatting 2026-10-29 23:28:48 -04:00
Adrien Beraud 0d5e4c761a network: enforce transport trust and address validation boundaries 2026-10-29 23:28:48 -04:00
Adrien Beraud efc372dc3f tests: cover transport trust and address validation boundaries 2026-10-29 23:28:48 -04:00
Adrien Beraud ddfbca3106 docs: describe fragment binding to the envelope state 2026-10-29 23:28:48 -04:00
Adrien Beraud 353c3d4e11 network: bind fragmented message parts to their envelope state
Value data fragments carry no node ID and were merged into a partial
message by source address and transaction ID alone, first come first
served. Transaction IDs increment from a random start and show in any
plaintext packet of a pair, such as the first-contact ping. An attacker
spoofing the sender's address could thus pre-seed forged plaintext
fragments, or a plaintext header, for an encrypted reply: the real parts
were then dropped as overlapping, forging unsigned values or losing the
message.

Parts of a message must now all be plaintext, or all come in envelopes
from the same sender key as its first part. An encrypted part replaces
plaintext state, which anyone can forge from a spoofed address; plaintext
or foreign-key parts never join an encrypted message.
2026-10-29 23:28:48 -04:00
Adrien Beraud 51324683e8 tests: cover forged parts in encrypted fragmented messages
Value data fragments carry no node ID. Parts of a message received in
envelopes must all come under the sender key of the envelopes, so that
plaintext or foreign-key parts spoofed from the sender's address, which
would otherwise be merged first, are ignored.
2026-10-29 23:28:48 -04:00
Adrien Beraud 4633483e06 docs: describe address-validated session caching 2026-10-29 23:28:48 -04:00
Adrien Beraud 6aee7ceb5c network: cache sessions only for address-validated peers
A session was cached for an address whenever we answered a peer there,
including replies to Initial and Hello envelopes, whose source address is
not verified. A spoofed Initial or Hello therefore replaced the session of
the real owner of the address, whose Data envelopes were then dropped until
a retry, and spoofed Initials from many addresses filled the cache and
evicted legitimate sessions, which were picked by address order.

Only cache a session for a peer whose ephemeral key arrived in a Session
or Data envelope, i.e. opened with our key for its address. Replies to
Initial and Hello envelopes derive their keys without caching them, so a
node that sent an Initial only confirms the session, and switches to Data
envelopes, once the peer answered a Session envelope. Evict the least
recently used session when the cache is full.
2026-10-29 23:28:48 -04:00
Adrien Beraud 32709230c6 tests: cover session cache hijacking by unvalidated senders
Replies to Initial and Hello envelopes must not cache a session at the
source address, which can be spoofed, and the cache must evict the least
recently used session rather than the lowest address. A node that started
with an Initial envelope only switches to Data envelopes once the peer
answered a Session envelope, which is when the peer caches the session.
2026-10-29 23:28:48 -04:00
Adrien Beraud 7b526410dd docs: describe Data envelopes and session confirmation 2026-10-29 23:28:48 -04:00
Adrien Beraud fbff1eced4 network: forget the peer ephemeral key on plaintext key advertisement
A peer only advertises its key in plaintext when it doesn't know ours, e.g. after a restart: it lost our session, so answer with an Initial envelope.
2026-10-29 23:28:48 -04:00
Adrien Beraud d23cb42b27 tests: cover a peer restarting with the same key
A node restarting with the same key and contacting a peer by address gets answers encrypted with the ephemeral key of its previous run.
2026-10-29 23:28:48 -04:00
Adrien Beraud e78e54b8c5 transport: send keyless Data envelopes once the session is confirmed
Sessions are keyed by peer address, so the receiver finds the keys from the source address: Data envelopes drop both keys from the header (30 bytes of overhead instead of 94) and need no key derivation to reject. A session is confirmed when the peer answers our envelope request with the session keys, or sends Data itself. Credentials only travel in key-carrying envelopes, which replaces Peer::acked. A received Initial envelope unconfirms the session, and full session caches evict instead of refusing, so a peer that derived keys keeps them.
2026-10-29 23:28:48 -04:00
Adrien Beraud 40560eec4e tests: cover keyless Data envelopes and session confirmation
Steady-state traffic uses Data envelopes, without sender keys nor credential, once the peer answered a request. Data envelopes only open from the session address, also authorize writes, and survive the receiver's epoch rotation.
2026-10-29 23:28:48 -04:00
Adrien Beraud 853bb1eeed docs: describe address-bound ephemeral keys and write authorization 2026-10-29 23:28:48 -04:00
Adrien Beraud 22d87a0f0e transport: replace write tokens with address-bound sessions
Ephemeral keys derive from a per-epoch secret and the peer address, so only that address learns them. A Session envelope then proves return routability: encrypted put, refresh and listen are accepted without token, and messages to encrypted peers no longer carry one (40 bytes saved). Initial envelopes cannot write. Tokens for plaintext peers move from Dht to NetworkEngine and derive from the same epoch secret. Session caches are keyed by address, and cached keys of both epochs are tried before any derivation.
2026-10-29 23:28:48 -04:00
Adrien Beraud dfb66c5a23 tests: cover session-based write authorization and address-bound keys
Encrypted writes must come in a Session envelope, whose ephemeral key is bound to the sender address. Plaintext writes still need a token, now issued by the network engine.
2026-10-29 23:28:48 -04:00
Adrien Beraud 56fcc5b918 docs: describe forward-secret transport keys
Ephemeral keys, Initial and Session key schedules, the unauthenticated
hello and the retry rule, with their security properties.
2026-10-29 23:28:48 -04:00
Adrien Beraud 26b30d4162 transport: add forward secrecy with rotated ephemeral keys
Each node rotates an ephemeral X25519 key every 30 minutes and keeps the
previous one for another period. Envelopes carry the sender static and
ephemeral keys. Session envelopes use keys derived from all four DH
combinations of static and ephemeral keys, erased with the epoch.
Initial envelopes, sent before the peer's ephemeral key is known, mix
the sender ephemeral and static keys with the receiver static key.

A hello is unauthenticated: it is only answered, without adding or
moving its claimed node. A peer is acknowledged only by a reply to a
request sent in an envelope, and a request retry restarts with an
Initial envelope and our credential.
2026-10-29 23:28:48 -04:00
Adrien Beraud 791a667fe2 tests: cover ephemeral key rotation, peer restart and hello
The previous ephemeral key is accepted for one more period, then erased.
A request retry restarts with an Initial envelope and our credential.
A hello is only answered and doesn't change what is known of its sender.
2026-10-29 23:28:48 -04:00
Adrien Beraud b693e3028b tests: cover forward-secret transport keys and envelope header
Session keys are directional and mix both static and ephemeral keys,
Initial keys are used before the peer's ephemeral key is known.
2026-10-29 23:28:48 -04:00
Adrien Beraud 3e975d530a docs: describe the transport encryption design 2026-10-29 23:28:48 -04:00
Adrien Beraud a32acbfa08 dht: persist the generated transport key in the node state
With persist_path, a transport key generated by the node is saved in the
state (owner-only file permissions) and restored on start, keeping the
node ID across restarts. Configured keys are never written to the state.
2026-10-29 23:28:48 -04:00
Adrien Beraud 43147b8791 tests: check generated transport key persistence 2026-10-29 23:28:48 -04:00
Adrien Beraud c1762af7e9 tools: add transport encryption and private network options
--no-encryption, --transport-key, --network-authority and
--network-credential configure the node; --gen-network-authority and
--issue-credential manage a private network.
2026-10-29 23:28:48 -04:00
Adrien Beraud 68b5996ba9 tests: cover encrypted transport and private networks 2026-10-29 23:28:48 -04:00
Adrien Beraud 7bc287fe2f network_engine: encrypt the DHT protocol
Nodes using transport encryption derive their node ID from their X25519
transport key, advertise it in plaintext messages ("k") and in node
lists ("k4"/"k6"), and encrypt all traffic with nodes whose key is
known. Keys are bound to node IDs, so they can't be forged, and
plaintext messages never move a node known to support encryption.
Legacy nodes keep talking in plaintext.

Private networks (TransportConfig::authorities) only accept nodes with
a credential issued by one of the network authorities, never send or
accept plaintext, and introduce themselves to addresses with a Hello.
2026-10-29 23:28:48 -04:00
Adrien Beraud 523dda8b3d tests: add transport primitives tests 2026-10-29 23:28:48 -04:00
Adrien Beraud 1c32dd0efb transport: add protocol encryption and credential primitives
X25519 transport keys, node ID derivation from the public key,
HKDF-SHA256 key agreement, ChaCha20-Poly1305 envelopes that legacy
msgpack parsers reject, and Ed25519 network authority credentials.
2026-10-29 23:28:48 -04:00
Adrien Beraud 1c914d8977 BUILD.md: cleanup 2026-10-29 23:28:29 -04:00
40 changed files with 6040 additions and 545 deletions

No files matched your search

+3 -3
View File
@@ -4,9 +4,9 @@ env:
LLHTTP_VERSION: "v9.2.1"
RESTINIO_VERSION: "0.7.10"
EXPECTED_LITE_URL: "https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp"
UBUNTU_FULL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-cxx-dev libargon2-dev"
UBUNTU_MESON_DEPS: "ninja-build libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-dev python3-setuptools python3-pip libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-cxx-dev libargon2-dev"
UBUNTU_MINIMAL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libfmt-dev libgnutls28-dev libcppunit-dev libmsgpack-cxx-dev libargon2-dev"
UBUNTU_FULL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
UBUNTU_MESON_DEPS: "ninja-build libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-dev python3-setuptools python3-pip libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
UBUNTU_MINIMAL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libfmt-dev libgnutls28-dev libcppunit-dev libmsgpack-dev libargon2-dev"
DEBIAN13_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-cxx-dev libargon2-dev"
on: [push, pull_request]
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
sudo apt update && \
sudo apt install libncurses5-dev libreadline-dev nettle-dev \
libgnutls28-dev libuv1-dev python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev \
libfmt-dev libmsgpack-cxx-dev libargon2-dev libasio-dev \
libfmt-dev libmsgpack-dev libargon2-dev libasio-dev \
llvm llvm-dev clang clang-tools && \
sudo apt remove gcc g++
+3 -3
View File
@@ -64,7 +64,7 @@ jobs:
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: '22'
cache: npm
@@ -151,7 +151,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: '22'
- uses: actions/download-artifact@v8
@@ -194,7 +194,7 @@ jobs:
node: ['22', '24', '26']
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- uses: actions/download-artifact@v8
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
# Run locally with: pipx run cibuildwheel --platform linux python
- name: Build wheels
uses: pypa/cibuildwheel@v4.3.5
uses: pypa/cibuildwheel@v4.3.4
with:
package-dir: python
+1 -2
View File
@@ -599,7 +599,6 @@ jobs:
uses: softprops/action-gh-release@v3
with:
draft: true
prerelease: ${{ contains(github.ref_name, '-') }}
files: |
downloaded_artifacts/windows_msi/*.msi
downloaded_artifacts/static_dhtnode/**/*dhtnode.exe
@@ -617,7 +616,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: https://registry.npmjs.org/
+2 -46
View File
@@ -130,36 +130,8 @@ the assembled npm archive on Node 22, 24, and 26 before release attachment.
## C and Rust Bindings
Enable `OPENDHT_C=ON` to build the C ABI. With `BUILD_TESTING=ON`, CTest runs
`test_c`; Meson registers the same test as `CBindings` when `c` and `tests` are
enabled. Rust regression and callback-lifetime tests run with Cargo; see
[the Rust build guide](rust/BUILD.md) for build and library-path settings.
`dht_infohash` and `dht_pkid` have `uint32_t` alignment, matching the native hash
types, while retaining their 20-byte and 32-byte sizes. Rebuild consumers against
the updated header; foreign-language declarations must match this alignment.
C handles returned by constructors, imports, and reference functions are owned
and require their matching delete/unref function. Data views, string getters,
query fields, and callback arguments are borrowed: copy them or take a reference
before returning from the callback. `dht_sockaddr_array_delete` releases both the
public-address array and its elements.
Operation completion callbacks run once, including failure when pending work is
discarded on runner teardown. Listener release callbacks run after the native
registration is destroyed; deleting an operation token alone does not cancel its
listener. Cancel first, then delete the consumed token. Startup-context release
callbacks run after all native status/store callback captures are released, also
on startup failure. Certificate-store callbacks must invoke the supplied emitter
synchronously with borrowed certificates. Query-result views are callback-scoped.
`dht_runner_loop` drives non-threaded nodes. `dht_runner_join` waits for callbacks
and allows restart; never call it from a DHT callback. For persisted shutdown,
wait for the shutdown callback before joining. Push tokens can be polled without
blocking and must be deleted separately. `dht_push_token_poll` returns `EAGAIN`
while pending, 0 with the native outcome when ready, and an error code otherwise;
consume a successful result only once. Native push outcomes are 0 through 3 for
refresh/value events, 128 through 131 for ignored events, and 132 for errors.
Enable `OPENDHT_C=ON` to build the C ABI.
See [the Rust build guide](rust/BUILD.md) for build instructions.
## Windows/MSVC
@@ -181,19 +153,3 @@ cd opendht
cmake --preset=vcpkg_manifest .
cmake --build .
```
### CI
Windows CI uses [lukka/run-vcpkg](https://github.com/lukka/run-vcpkg) to acquire
and bootstrap the revision pinned by `default-registry.baseline` in
`vcpkg-configuration.json`. CMake uses the action's `VCPKG_ROOT` and installs
manifest dependencies on every run. Dependency-preparation jobs warm the binary
cache before the Windows build matrix starts.
Only ABI-keyed binary package archives are cached, not installed trees. The
supported [files provider](https://learn.microsoft.com/en-us/vcpkg/reference/binarycaching#files-provider)
is persisted with `actions/cache`; it does not use the removed `x-gha` provider
or require package-feed credentials. Cache keys include the target triplet,
manifests, overlay ports, run ID, and attempt, with restore prefixes for reuse.
Each run can save newly built archives without overwriting an immutable cache;
vcpkg validates package ABIs before reuse.
+11 -4
View File
@@ -10,10 +10,10 @@ if(POLICY CMP0167)
endif()
set (opendht_VERSION_MAJOR 4)
set (opendht_VERSION_MINOR 5.0)
set (opendht_VERSION_MINOR 4.99)
set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
set (PACKAGE_VERSION "${opendht_VERSION}-rc.1")
set (VERSION "${PACKAGE_VERSION}")
set (PACKAGE_VERSION ${opendht_VERSION})
set (VERSION "${opendht_VERSION}")
project (opendht
VERSION ${opendht_VERSION}
DESCRIPTION "OpenDHT: A lightweight C++17 Distributed Hash Table library."
@@ -117,6 +117,7 @@ if (NOT MSVC)
find_package (PkgConfig REQUIRED)
pkg_search_module (GnuTLS REQUIRED IMPORTED_TARGET gnutls)
pkg_search_module (Nettle REQUIRED IMPORTED_TARGET nettle)
pkg_search_module (Hogweed REQUIRED IMPORTED_TARGET hogweed>=3.1)
pkg_search_module (argon2 REQUIRED IMPORTED_TARGET libargon2)
set(argon2_lib ", libargon2")
pkg_search_module (Jsoncpp IMPORTED_TARGET jsoncpp)
@@ -421,7 +422,7 @@ else ()
endif ()
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMSGPACK_NO_BOOST -DMSGPACK_DISABLE_LEGACY_NIL -DMSGPACK_DISABLE_LEGACY_CONVERT")
add_definitions(-DPACKAGE_VERSION="${PACKAGE_VERSION}")
add_definitions(-DPACKAGE_VERSION="${opendht_VERSION}")
if (ASIO_INCLUDE_DIR)
include_directories (SYSTEM "${ASIO_INCLUDE_DIR}")
@@ -467,6 +468,7 @@ list (APPEND opendht_SOURCES
src/routing_table.cpp
src/node_cache.cpp
src/network_engine.cpp
src/transport.cpp
src/securedht.cpp
src/dhtrunner.cpp
src/log.cpp
@@ -491,6 +493,7 @@ list (APPEND opendht_HEADERS
include/opendht/routing_table.h
include/opendht/node_cache.h
include/opendht/network_engine.h
include/opendht/transport.h
include/opendht/scheduler.h
include/opendht/rate_limiter.h
include/opendht/securedht.h
@@ -613,6 +616,7 @@ else()
PRIVATE
PkgConfig::argon2
PkgConfig::Nettle
PkgConfig::Hogweed
${SIMDUTF_LIBRARIES}
PUBLIC
${CMAKE_THREAD_LIBS_INIT}
@@ -637,6 +641,7 @@ else()
PRIVATE
argon2
nettle
hogweed
PUBLIC
${CMAKE_THREAD_LIBS_INIT}
${GNUTLS_LIBRARIES}
@@ -869,6 +874,8 @@ if (BUILD_TESTING)
tests/test_value.cpp
tests/test_crypto.h
tests/test_crypto.cpp
tests/test_transport.h
tests/test_transport.cpp
tests/test_dhtrunner.h
tests/test_dhtrunner.cpp
tests/test_storage.h
+59 -1
View File
@@ -27,6 +27,19 @@
[\fB\-y\fR \fIendpoint\fR]
[\fB\-z\fR \fIkey\fR]
[\fB\-u\fR \fIbundleid\fR]
[\fB\-\-no\-encryption\fR]
[\fB\-\-transport\-key\fR \fIfile\fR]
[\fB\-\-network\-authority\fR \fIkey\fR]
[\fB\-\-network\-credential\fR \fIfile\fR]
.br
.B dhtnode
\fB\-\-gen\-network\-authority\fR \fIprefix\fR
.br
.B dhtnode
\fB\-\-issue\-credential\fR \fIfile\fR
\fB\-\-authority\-key\fR \fIfile\fR
\fB\-\-transport\-key\fR \fIfile\fR
[\fB\-\-validity\fR \fIdays\fR]
.SH DESCRIPTION
Runs an OpenDHT node, with an interactive CLI (default) or as a daemon (with \fB'-d'\fP or \fB'-s'\fP).
Commands available in the interactive shell are:
@@ -122,7 +135,9 @@ Disable rate limiting (allows unlimited requests and storage).
Enable public stable mode.
.TP
\fB\-f\fP \fIpath\fP, \fB\-\-persist\fP \fIpath\fP
Specify a file path to persist/load the node state (routing table and values).
Specify a file path to persist/load the node state (routing table, values and
generated transport key). State is written to a temporary file and renamed
over the destination only after the write succeeds. Save failures are logged.
.TP
\fB\-n\fP \fInetwork_id\fP, \fB\-\-net\fP \fInetwork_id\fP
Specify the network id. This lets you connect to distinct networks and prevents
@@ -164,6 +179,40 @@ Device key for push notifications.
.TP
\fB\-u\fP \fIbundleid\fP, \fB\-\-bundleid\fP \fIbundleid\fP
Bundle ID for push notifications.
.SH TRANSPORT ENCRYPTION
The DHT protocol is encrypted with nodes supporting it, and stays in plaintext
with older nodes. The node ID derives from the transport key.
.TP
\fB\-\-no\-encryption\fP
Disable protocol encryption, using a random node ID like older versions.
.TP
\fB\-\-transport\-key\fP \fIfile\fP
Load the transport key from \fIfile\fP, or create it if it doesn't exist.
Keeps the same node ID across restarts. Otherwise, a key is generated and kept
in the node state if \fB\-\-persist\fP is used, or generated on each start.
.TP
\fB\-\-network\-authority\fP \fIkey\fP
Join a private network: only accept nodes with a credential issued by the
authority with public key \fIkey\fP (hexadecimal). Can be repeated.
Requires \fB\-\-network\-credential\fP and \fB\-\-transport\-key\fP.
.TP
\fB\-\-network\-credential\fP \fIfile\fP
Credential of our transport key, issued by a network authority.
.TP
\fB\-\-gen\-network\-authority\fP \fIprefix\fP
Generate a network authority, save its secret key to \fIprefix\fP.key and its
public key to \fIprefix\fP.pub, and exit. Refuses to overwrite either existing
output path, including a dangling symlink.
.TP
\fB\-\-issue\-credential\fP \fIfile\fP
Issue a credential for the key in \fB\-\-transport\-key\fP (created if needed),
signed with \fB\-\-authority\-key\fP, save it to \fIfile\fP and exit.
.TP
\fB\-\-authority\-key\fP \fIfile\fP
Network authority secret key used to issue credentials.
.TP
\fB\-\-validity\fP \fIdays\fP
Validity of issued credentials (default: 365 days).
.SH EXAMPLES
Run a node using bootstrap.jami.net as a bootstrap:
.PP
@@ -188,6 +237,15 @@ Use a proxy client to connect to a proxy server:
.EX
dhtnode --proxyclient dhtproxy.jami.net:80
.EE
.PP
Create a private network and run one of its nodes:
.PP
.EX
dhtnode --gen-network-authority net
dhtnode --issue-credential node1.cred --authority-key net.key --transport-key node1.key
dhtnode --transport-key node1.key --network-credential node1.cred \\
--network-authority $(cat net.pub) -b 192.0.2.1:4222
.EE
.SH AUTHORS
.TP
Program written by
+564
View File
@@ -0,0 +1,564 @@
# Transport encryption: cryptographic design
This document specifies how OpenDHT encrypts and authenticates the UDP DHT protocol between nodes (the "transport"), and how private networks restrict participation with a small public-key infrastructure.
Implementation: [include/opendht/transport.h](../include/opendht/transport.h), [src/transport.cpp](../src/transport.cpp), and the send/receive paths in [src/network_engine.cpp](../src/network_engine.cpp).
## 1. Scope
### Goals
- **Confidentiality and integrity** of DHT protocol messages exchanged between two nodes that both support encryption.
- **Authenticated node IDs**: a node can only send encrypted messages under the node ID that derives from its own key.
- **Backward compatibility**: nodes without this feature keep working with new nodes, in plaintext.
- **Optional private networks**: when configured, a node only talks to nodes holding a credential signed by a trusted authority.
- **Permanent membership**: grants do not expire. Without a compromise or an intentional membership change, no grant renewal, authority rotation, or operator maintenance is required.
- **Forward secrecy**: recorded traffic stays confidential after a node's long-term key is compromised, once the ephemeral keys used for it are erased (at most one hour later). The first packet to a peer has weaker guarantees ([§5.2](#52-key-schedule)).
- **Address validation**: a Session or Data envelope proves that its sender receives packets at its source address. It replaces the legacy write tokens between encrypted nodes ([§6.6](#66-write-authorization)).
- **Low cost**: routing discovery also establishes the session, with a few X25519 operations per peer every 30 minutes. Once a session is established, 30 bytes of overhead per packet, minus the 40-byte token in messages that carried one.
### Non-goals
- **End-to-end protection of values.** Encryption is hop-by-hop. Nodes storing a value see it in clear. Use `SecureDht` (signed or encrypted values) for end-to-end protection. `SecureDht` identities are unrelated to transport keys.
- **Hiding metadata**: packet sizes, timing and addresses are visible, and so are the sender's public keys until a session is established.
- The REST proxy (`DhtProxyServer`/`DhtProxyClient`) and local peer discovery are not covered.
## 2. Notation
| Notation | Meaning |
|---|---|
| `a ‖ b` | Concatenation of byte strings |
| `"text"` | ASCII bytes, without terminator |
| `u32be(x)` | Unsigned integer, big-endian, 4 bytes |
| `sk_A`, `pk_A` | Static transport secret and public key of node A |
| `esk_A`, `epk_A` | Ephemeral secret and public key of node A for the address of its peer |
| `DH(x, Y)` | `X25519(x, Y)` |
| `N` | Network ID (`Config::network`, 32-bit, `0` by default) |
| `K_A→B` | Key encrypting envelopes from A to B |
All keys are 32 bytes. Integers on the wire are big-endian.
## 3. Primitives
| Purpose | Primitive | Reference |
|---|---|---|
| Key agreement | X25519 | RFC 7748 |
| Key derivation | HKDF-SHA256 | RFC 5869 |
| Packet encryption | ChaCha20-Poly1305 AEAD, 96-bit nonce, 128-bit tag | RFC 8439 |
| Node ID | SHA-256, truncated to 160 bits | FIPS 180-4 |
| Credential signature | Ed25519 | RFC 8032 |
| Randomness | `gnutls_rnd` (`GNUTLS_RND_KEY` for keys, `GNUTLS_RND_NONCE` for nonces) | |
Primitives come from Nettle (libnettle and libhogweed) and GnuTLS, which OpenDHT already depends on. All primitives are used through their standard interfaces with no modification. The authentication tag is compared in constant time. Secret keys are erased with `gnutls_memset`.
## 4. Keys and identities
### 4.1 Transport key
Each node has a static X25519 key pair, its long-term identity:
```
sk = 32 random bytes
pk = X25519(sk, 9)
```
The key is either given in `Config::transport.key`, or generated on start. A generated key is saved in the node state when `Config::persist_path` is set (see [§8](#8-key-persistence)). Otherwise, it changes on every restart, and so does the node ID.
The transport key is used only for the transport. It is independent of the `SecureDht` identity (an RSA certificate).
### 4.2 Node ID
When transport encryption is enabled, the node ID is derived from the public key:
```
nodeId(pk) = SHA-256("opendht-node-id-v1" ‖ pk)[0..20)
```
`Config::node_id` is ignored in that case. This binding is what authenticates node IDs: a receiver checks that the ID claimed inside a decrypted message equals `nodeId(pk)` of the static key the envelope was authenticated with (see [§6.5](#65-receiving)). Holding the ID therefore requires holding `sk`.
When encryption is disabled (`TransportConfig::enabled = false`), the node uses the legacy behavior: a random ID, or `Config::node_id`.
### 4.3 Network authority (private networks)
An authority is an Ed25519 key pair. Its secret is the 32-byte Ed25519 seed. Its public key identifies the private network, and nodes are configured with a list of trusted authority public keys (`TransportConfig::authorities`). A network is private if and only if this list is non-empty.
Any one trusted authority can grant membership. A node can trust multiple authorities simultaneously, allowing old and replacement grants to coexist during an explicit transition. There is no scheduled authority rotation.
### 4.4 Credential (private networks)
A membership grant, called a credential in the API and CLI, is an authority's signature over a transport public key. It never expires and contains no timestamp. It is 97 bytes:
```
offset size field
0 1 version = 0x02
1 32 pk transport public key of the node
33 64 signature Ed25519 signature by the authority
```
The signature covers:
```
"opendht-credential-v2" ‖ 0x02 ‖ pk
```
A credential is **valid** when its signature verifies under at least one trusted authority. Admission does not depend on a system clock. The same grant can be reused across restarts and session-key rotations without an update.
Version 1 expiring credentials are rejected, not reinterpreted as permanent grants. Migrating from that format requires issuing a version 2 grant once.
Credentials are not bound to a network ID. An authority key therefore defines a network: use distinct authorities for distinct networks.
## 5. Key agreement
### 5.1 Ephemeral keys
Each node has an epoch secret `S`: 32 random bytes, replaced every 30 minutes (an *epoch*). Its ephemeral key for a peer at address `addr` derives from it:
```
addr = IP (4 bytes for IPv4, including IPv4-mapped IPv6, else 16 bytes) ‖ port (2 bytes, network order)
esk(addr) = HMAC-SHA256(key = S, "opendht-address-key-v1" ‖ addr)
epk(addr) = X25519(esk(addr), 9)
```
- The node always sends to `addr` with `epk(addr)` from its **current** epoch.
- It still accepts envelopes addressed to its key from the **previous** epoch, so peers have one epoch to learn the new key.
- On rotation, the previous epoch secret and every key derived from it are erased.
An ephemeral secret therefore lives at most one hour. Epoch secrets are never persisted, and a restart starts with a new one.
Nodes learn each other's ephemeral key from the header of every envelope ([§6.1](#61-envelope)). A node records a peer's ephemeral key only from an envelope it authenticated.
`epk(addr)` is only ever sent to `addr`. Learning it therefore requires receiving packets at `addr`, or observing the path to it. This is what makes a Session envelope a proof of address ([§6.6](#66-write-authorization)). Deriving keys per address costs one more X25519 base-point multiplication per new address, and nothing per packet.
### 5.2 Key schedule
Each envelope from a sender A to a receiver B is encrypted with a key `K_A→B`, derived with HKDF-SHA256 (RFC 5869):
```
prk = HMAC-SHA256(key = "opendht-transport-v2", ikm)
K_A→B = HMAC-SHA256(key = prk, info ‖ 0x01)
```
The input keying material `ikm` and `info` depend on what A knows about B.
**Session**: A knows B's ephemeral key `epk_B`:
```
ikm = DH(esk_A, epk_B) ‖ DH(esk_A, pk_B) ‖ DH(sk_A, epk_B) ‖ DH(sk_A, pk_B)
info = u32be(N) ‖ 0x02 ‖ pk_A ‖ epk_A ‖ pk_B ‖ epk_B
```
**Initial**: A doesn't know B's ephemeral key, typically on first contact:
```
ikm = DH(esk_A, pk_B) ‖ DH(sk_A, pk_B)
info = u32be(N) ‖ 0x01 ‖ pk_A ‖ epk_A ‖ pk_B
```
B computes the same values with its own secrets, since `DH(x_A, Y_B) = DH(y_B, X_A)`. Any DH result equal to zero (a low-order public key) is rejected.
Properties:
- **Directional keys.** `info` orders the keys as sender, then receiver, so `K_A→B ≠ K_B→A`. Each node derives both from the same four DH values.
- **Authentication.** Every key includes `DH(sk_A, pk_B)`, so only the holders of `sk_A` or `sk_B` can produce or read the envelope. Every envelope thus proves possession of the sender's static key.
- **Forward secrecy.** A Session key includes `DH(esk_A, epk_B)`. Once both ephemeral secrets are erased, the key cannot be recomputed, even with both static secrets.
- **Key-compromise impersonation resistance.** With `sk_B`, an attacker still cannot impersonate A to B in a Session envelope: it cannot compute `DH(sk_A, epk_B)` without `sk_A` or `esk_B`.
- **Initial keys are weaker**, as for the first message of Noise IK. Without `epk_B`, an Initial key is only forward-secret against a later compromise of `sk_A`, not of `sk_B`. It also allows impersonating A to B with `sk_B`. Initial envelopes are only sent until A learns `epk_B`, usually for a single packet.
- **Binding.** `info` includes `N`, the envelope type and all public keys involved. Nodes with different network IDs cannot decrypt each other's packets.
- **Cost.** A Session key costs four X25519 operations, plus one for our ephemeral key. It is cached in the epoch, so it is derived once per peer address and ephemeral key pair, once the peer has proven to receive at that address ([§6.5](#65-receiving)). An Initial key costs two X25519 operations, plus one for our ephemeral key, and is not cached. Each epoch caches at most 16384 sessions, evicting the least recently used; beyond that, keys are derived for each packet.
## 6. Wire protocol
### 6.1 Envelope
Initial and Session envelopes carry the sender keys:
```
offset size field
0 1 marker = 0xC1
1 1 type: 1 = Initial, 2 = Session, 3 = Hello; | 0x80 if a credential is included
2 32 pk_sender
34 32 epk_sender
66 12 nonce (random)
78 len ciphertext
78+len 16 Poly1305 tag
```
Once the session is confirmed ([§6.4](#64-sending)), Data envelopes leave them out. The receiver finds the session keys from the source address:
```
offset size field
0 1 marker = 0xC1
1 1 type = 4 (Data), never with a credential
2 12 nonce (random)
14 len ciphertext
14+len 16 Poly1305 tag
```
- The ciphertext is `ChaCha20-Poly1305(K_sender→receiver, nonce, plaintext, aad = header)`.
- The header is bytes `[0, 66)`, or `[0, 2)` for Data: marker, type, and the sender keys. Because it is the associated data, an attacker cannot change the type, the credential flag or the sender keys without the tag check failing.
- The plaintext is the msgpack-encoded DHT message. With the credential flag, it is preceded by the sender's 97-byte credential.
- The overhead is 30 bytes for Data, 94 bytes for Initial and Session, or 191 bytes with a credential.
- Nonces are 96 bits from the system CSPRNG. For `n` packets under one key, the probability of a nonce collision is about $n^2 / 2^{97}$. Keys change at least every epoch, so this is negligible.
`0xC1` is the one byte value that msgpack never uses. Every legacy OpenDHT message is a msgpack map, which starts with `0x80`–`0x8F` or `0xDE`–`0xDF`. This marker therefore unambiguously separates envelopes from plaintext messages:
- legacy nodes fail to parse an envelope and drop it, without blacklisting the sender;
- new nodes still parse plaintext messages normally.
### 6.2 Hello (private networks only)
`Hello` introduces a node to a peer whose key is unknown. This happens when pinging a bare address, typically a bootstrap node. A Hello is **not encrypted**:
```
0xC1 ‖ 0x03 ‖ pk_sender ‖ epk_sender ‖ credential (97 bytes) ‖ msgpack ping
```
The sender cannot encrypt to a key it doesn't know, and X25519 keys cannot sign, so **a Hello proves nothing about its sender**. Its credential travels in clear and can be copied. The receiver therefore:
- answers only a ping, and only if the credential is valid for `pk_sender`;
- answers with a Session envelope to `pk_sender` and `epk_sender`, so only the holder of `sk_sender` can read the answer;
- keeps no state from the Hello: it does not add, move or authorize the claimed node.
The sender proves possession of its key with its next envelope.
### 6.3 Key advertisement in plaintext messages
New nodes add fields to plaintext msgpack messages. Legacy nodes ignore unknown fields.
| Field | Location | Content |
|---|---|---|
| `k` | top level | Sender's transport public key (bin, 32 bytes) |
| `k4` | reply body, next to `n4` | Concatenated public keys of the listed IPv4 nodes, 32 bytes per node, all-zero if unknown |
| `k6` | reply body, next to `n6` | Same for IPv6 |
Rules for using these fields:
- **`k` is sent only when it is needed**: in public mode, in plaintext messages to nodes whose key is not yet known. Encrypted messages do not need it, because the sender key is in the envelope header.
- **Listed keys are checked before use.** A key from `k`, `k4` or `k6` is attached to a node only if `nodeId(key)` equals the node's ID. Otherwise it is ignored.
- **The binding limits what a dishonest node can do with a node list.** It cannot attach a forged key to an existing node ID. It can still list a wrong address, which at worst causes encrypted packets to be sent to the wrong host. That host cannot read them.
- `k4` and `k6` are omitted if no listed node has a known key. They are ignored if their length is not exactly 32 × the number of listed nodes.
### 6.4 Sending
To send a message to node B:
1. If transport encryption is disabled, send plaintext.
2. If B's static key is known (B has a peer record), send an envelope with our ephemeral key for B's address:
- a Data envelope if our session with B's address is **confirmed**;
- otherwise a Session envelope if B's ephemeral key is known, or else an Initial envelope. In a private network, both include our credential.
3. Else, in a private network:
- if B's ID is unknown (a ping to a bare address), send a `Hello`;
- otherwise refuse to send (`EPERM`).
4. Else (public network, key unknown), send plaintext, including `k`.
Once a node's key is known, nothing is ever sent to it in plaintext. Failed key agreement, including a low-order public key, refuses the send (`EPERM`); it never falls back to plaintext.
**Confirmation.** A session with an address is confirmed when B shows that it holds the session keys and accepted us:
- B answers, in a Session or Data envelope, a request we sent to its node ID **in a Session or Data envelope**. B cached the session when it opened our envelope ([§6.5](#65-receiving)), and answered with keys of our current epoch. Requests sent in an Initial envelope or a Hello are excluded: B answered them without caching a session, so our next request is a Session envelope, and its answer confirms;
- or we receive a Data envelope under our current epoch. B only sends one after we confirmed it the same way.
The confirmation is lost when the session keys change: on epoch rotation, or when B's ephemeral key changes through accepted traffic. An Initial request leaves existing state untouched and is answered without reusing a cached session or its confirmation.
**Establishment.** Only `ping` and `find_node` requests may precede address validation. Application requests wait until B's ephemeral key is known, or a successful control reply identifies a genuinely keyless legacy peer. Learning the ephemeral from an accepted Initial reply is enough to send a Session: neither address-indexed cache admission nor Data confirmation requires another handshake. Direct NetworkEngine application requests share one engine-owned ping; canceling a caller's ping cannot strand them. Private bare-address bootstrap remains a ping-only Hello.
**Recovery.** At the first response timeout, a request forgets B's ephemeral key and starts recovering the session, for example after B restarts. Application requests to B are withheld, including new requests, while one internal ping retries in Initial envelopes (with our credential in a private network). `find_node` remains available and its successful reply can also release the waiters. An accepted reply completes its own request before held work is released, so a delayed original reply is not retransmitted during attachment. Released requests receive fresh response timers; withholding consumes no transmission attempts. The internal probe's first timeout notifies waiting operations so searches can replace unresponsive peers promptly. Probe exhaustion or rejection terminates the waiters. Their payloads never enter Initial envelopes, and no `401` challenge/retry exchange is used.
**Value searches.** A cold peer first receives `find_node(search_target)`. Its reply supplies its ephemeral and closer candidates in the same exchange. The existing distance-ordered search dispatch contacts closer candidates immediately, within the existing four-active-peer limit; it does not wait for a SELECT reply from the farther responder. This routing probe has separate state: it cannot satisfy a GET, pagination, or an empty-result callback. Responders remain eligible for subsequent value retrieval. A cold holder typically needs three exchanges (`find_node`, SELECT IDs, GET by ID), while a session-ready holder retains its usual two. A routing probe's first timeout frees its active search slot while protocol retries continue, rather than waiting for final expiration.
Value data is fragmented so that each packet fits in the 1280-byte IPv6 minimum MTU: `1280 − 40 − 8 = 1232` bytes of UDP payload, less 48 bytes reserved for the msgpack framing of a fragment, less the overhead of the envelope the fragments are sent in. Fragments carry `1184` bytes of value data in plaintext, `1154` in Data envelopes, `1090` in Initial or Session envelopes, and `993` with a credential.
This sizes value fragments only. Complete replies and node lists are not yet subject to a whole-datagram MTU budget; node-list trimming and packet-size enforcement remain separate work.
### 6.5 Receiving
For each received packet, a node applies the following checks in order:
1. **Address filters.** Drop the packet if it comes from a martian or blacklisted address. This is unchanged.
2. **Envelopes.** If the packet starts with `0xC1`:
1. If encryption is disabled, drop it.
2. Drop it if its type is unknown, or if `pk_sender` is our own key. This rejects reflected packets.
3. **Hello:**
- Drop it outside private networks, or if it carries no message.
- Apply the public-key limit, then verify the credential: its key must equal `pk_sender` and it must be valid. Drop the packet on failure.
- Continue with steps 4 and 5, then answer as described in [§6.2](#62-hello-private-networks-only). The Hello is not processed any further.
4. **Initial:** apply the public-key limit, derive the Initial key from `pk_sender` and `epk_sender`, and decrypt.
5. **Session:** first try the keys cached in each epoch for (source address, `pk_sender`, `epk_sender`). Otherwise, apply the public-key limit, then for our current epoch then the previous one: derive the keys with our ephemeral key for the source address, and decrypt. Keep newly derived keys pending until the packet passes authorization and message validation.
6. **Data:** decrypt with the session cached for the source address in our current epoch, then in the previous one. Its `pk_sender` and `epk_sender` are those of the session. This needs no key derivation and no public-key limit.
7. Drop the packet if it was not decrypted.
8. **Credential flag:** strip the 97-byte credential. In a private network, verify it as for Hello, unless the peer is already authorized; on success, record the peer as authorized, without an expiry.
**No credential:** in a private network, drop the packet unless the peer is already authorized.
9. Record `epk_sender` on a temporary peer record. It is **validated for the source address** if the envelope was a Session or Data envelope: opening it required our ephemeral key for that address, which was only ever sent there. Existing peer state is updated only after parsing, ID binding and the processing checks below.
**Session cache.** The epoch caches one session per source address, including its verified admission in private networks. Authorization therefore survives the release of a client-mode peer's node record. Newly derived Session keys enter the cache only after credential authorization, parsing, network and node ID checks, and basic fragment validation. Replies must match a pending request or listener. Admission is per packet, so subsequent Data fragments can decrypt before the whole message is assembled. Sending to the exact address for which the peer's ephemeral key is validated can also cache a session. Answers to Initial and Hello envelopes never use or change cached sessions: their source address may be spoofed. If both the peer record and cached admission are lost, the same permanent credential is verified again; no replacement grant is needed.
3. **Plaintext in a private network.** Drop any packet that is not an envelope.
4. **Parsing.** Parse the msgpack message and apply the network ID check. This is unchanged.
5. **Envelope ID binding.** For an envelope, the message's node ID must equal `nodeId(pk_sender)`, otherwise drop the message. Value data fragments are exempt because they carry no node ID. Fragment buffers are keyed by source address, transaction ID and sender key, with a separate keyless buffer for plaintext. Only parts from the same sender can join a header, and a response header must match a pending request or listener. Foreign-key or plaintext parts cannot reserve the expected sender's buffer, even when they arrive first. Initial envelopes carrying value fragments or fragmented-message headers are dropped: encrypted value data requires a Session or Data envelope.
6. **Plaintext with `k`.** For a plaintext message carrying `k`, attach the key to the sender if it matches the sender's ID.
7. **Processing.**
- A Session or Data envelope can move the sender's node record to the packet's source address.
- Initial requests use transient node records. Only `ping` and `find_node` receive successful replies; GET (including SELECT) and writes receive a short `401` error, and value updates are dropped. Rejected GETs never invoke storage or serialize values. These requests do not add or move an established node, count as activity, or change its ephemeral key or session confirmation.
- Initial replies and errors are accepted only for a pending request at the expected source address, never for a listener. They cannot move a known keyed node to a different address.
- Replies and Data envelopes can confirm the session ([§6.4](#64-sending)).
A **plaintext** message claiming the ID of a node whose key is known is untrusted, even from that node's recorded address:
- it does not move the node record and does not count as activity from that node;
- replies, errors, value updates and writes are dropped;
- `ping` and `find_node` receive control replies using a transient node record and an Initial envelope to the known key; GET receives a short `401`, even when the plaintext packet is the first advertisement of a matching `k`. Truly keyless legacy GETs remain unchanged.
**Rate limits.** A node keeps separate budgets, each with a global quota and a quota per source address (per /64 prefix for IPv6):
- **Session requests:** requests in Session or Data envelopes, whose source address is validated. `Config::max_req_per_sec`, by default 8192 per second, and 1024 per address.
- **Other requests:** plaintext requests and requests in Initial or Hello envelopes, whose source address can be spoofed. `Config::max_unauth_req_per_sec`, with the same defaults. A flood of them does not affect requests from established peers.
- **Public-key work:** one unit per packet needing a key agreement or a signature verification, charged before that work. `Config::max_crypto_per_sec`, by default 512 per second, and 64 per address. A unit covers the packet and its answer, including credential verification:
- each Initial envelope (two X25519 operations, and five more to answer a request);
- each Session envelope from an uncached (address, `pk_sender`, `epk_sender`) triple (five X25519 operations per epoch tried);
- each cached Session envelope whose peer has not yet been authorized and needs credential verification (one charge, not an additional charge for packets already admitted for key agreement);
- each Hello (one Ed25519 verification, and the key derivation of its answer);
- each plaintext request from a sender whose key is known or advertised, answered in an Initial envelope (three X25519 operations).
Initial and uncached Session envelopes from an address where we await an answer, from the sender's node record or from a request sent by address, use a separate budget of the same size. Our own key exchanges thus complete during a flood.
Packets from peers with cached session keys cost one AEAD operation, except when a private-network credential needs verification. Each epoch caches at most 16384 sessions, evicting the least recently used one to store a new one. An evicted peer recovers after one retransmission. Only a peer that received a packet from us at its address can take a cache slot, so a slot costs the attacker a reachable address and a round trip.
### 6.6 Write authorization
`put`, `refresh` and `listen` create state on the receiver, and `listen` makes it send value updates to the requester's address. The receiver therefore requires proof that the requester receives packets at its source address. Without it, an attacker could spoof a victim's address to fill its storage quota, or direct update traffic to it.
- **Encrypted requests** must arrive in a **Session** or **Data** envelope. Our ephemeral key for the source address is only sent to that address ([§5.1](#51-ephemeral-keys)), so opening the envelope proves the sender received one of our packets there. Requests in Initial envelopes are refused with error `401`; normal senders establish the session through `ping` or `find_node` before sending application requests.
- **Plaintext requests** must carry a token, as in the legacy protocol. The token is sent in answers to `find`, `get` and `listen`, and in value updates, to the address of the node record:
```
token(addr) = HMAC-SHA256(key = S, "opendht-address-token-v1" ‖ addr)
```
A token is accepted in the epoch it was issued and the next one. Legacy nodes treat tokens as opaque, so changing their derivation keeps compatibility.
Messages to and from encrypted nodes carry no token. This saves 40 bytes in each answer to `find`, `get` and `listen`, and in each `put`, `refresh`, `listen` and value update. A node that receives an encrypted answer may write to its sender.
## 7. Message flows
### 7.1 Public network
Encryption is opportunistic, and keys travel with node IDs.
```mermaid
sequenceDiagram
participant A as A (new)
participant B as B (new)
participant L as L (legacy)
A->>B: plaintext ping + k = pk_A (B's key unknown)
Note over B: attaches pk_A to A (nodeId check)
B->>A: Initial envelope (pong)
Note over A: decrypts, checks ID = nodeId(pk_B), learns epk_B
A->>B: Session envelope (request)
B->>A: Session envelope (reply)
Note over A: session confirmed
A->>B: Data envelope (request)
Note over B: session confirmed
B->>A: Data envelope (all further traffic)
A->>L: plaintext + k (ignored by L)
L->>A: plaintext
```
- The first request to a node with an unknown key, typically a bootstrap ping, is in plaintext. Its reply is an Initial envelope. The next request and its reply use Session envelopes, and all later traffic uses Data envelopes.
- Nodes learned through `k4`/`k6` get an Initial envelope as the first packet. Its answer is a Session envelope, but the answerer caches nothing yet: the next request is a Session envelope, and its answer confirms the session. Data envelopes follow.
- Legacy nodes see plaintext from new nodes and are unaffected.
### 7.2 Private network
Only nodes with a credential participate. Nodes never send or accept plaintext.
```mermaid
sequenceDiagram
participant A
participant B as B (bootstrap)
A->>B: Hello(pk_A, epk_A, cred_A, ping)
Note over B: verifies cred_A, keeps no state
B->>A: Session(cred_B, pong)
Note over A: verifies cred_B, authorizes B
A->>B: Session(cred_A, request)
Note over B: verifies cred_A, authorizes A, caches the session
B->>A: Session(cred_B, reply)
Note over A: session confirmed
A->>B: Data (without credential)
```
- **Credential exchange.** Each side attaches its credential to its Initial and Session envelopes. Data envelopes, sent once the peer has accepted us, never carry it.
- **Authorization lifetime.** Admission does not expire. Session keys still rotate for forward secrecy, independently of membership. A lost admission record is recovered using the same grant, without operator action.
- **Node lists.** Listed nodes without a valid matching key are discarded.
- **Startup checks.** The node refuses to start (`DhtException`) if:
- encryption is disabled while authorities are configured;
- the configured credential does not match the transport key;
- the credential is not valid for the configured authorities.
The credential and trusted authorities are read on start. Restart with the updated configuration when explicitly changing a grant or the trust set; this also discards cached admissions.
## 8. Key persistence
If the following conditions all hold, the static secret key is saved in the state file as the msgpack field `transport_key` (32 bytes):
- encryption is enabled;
- no key was configured;
- `Config::persist_path` is set.
When the key is saved:
- the state is written to a randomly named temporary file in the destination directory;
- owner-only read/write permissions are requested before any secret data is written;
- the temporary file is closed successfully before it is renamed over the state file;
- a save failure is logged, the temporary file is removed, and the previous state file is left intact;
- on start, the key is restored before any packet is exchanged, so the node keeps its ID across restarts.
The node also stores its ID in the state file, so older versions reading this state keep the same ID. A configured key is never written to the state file. Ephemeral keys and session keys are never persisted.
`dhtnode --transport-key <file>` loads the raw 32-byte secret from `<file>`. If the file does not exist, `dhtnode` creates it using the same temporary-file and permission handling. Unreadable or invalid existing keys are not replaced.
File operations use portable C++ filesystem APIs. Permission semantics depend on the platform; exclusive file creation and power-loss durability are not guaranteed. Use an owner-controlled directory and a single writer for each path.
## 9. Security analysis
### 9.1 Properties
Against an attacker who controls the network but holds no node's secret key:
- **Confidentiality and integrity.** Traffic between two nodes that know each other's keys is confidential and integrity-protected. The protection is IND-CCA2 and INT-CTXT, from ChaCha20-Poly1305 under keys derived from X25519.
- **ID authentication.** An encrypted message under node ID `X` can only be produced by the holder of the static secret key behind `X`.
- Producing a key for a given ID requires a SHA-256 preimage on 160 bits.
- Choosing the first `b` bits of one's own ID costs about $2^b$ key generations. Legacy nodes choose their ID freely.
- **Forward secrecy.** Session envelopes recorded by the attacker stay confidential if static keys are compromised later, once both ephemeral secrets are erased (at most one hour after use). Initial envelopes stay confidential if the sender's static key is compromised, but not the receiver's.
- **Key-compromise impersonation.** With a node's static secret, an attacker can decrypt the Initial envelopes sent to it and impersonate other nodes to it in Initial envelopes, but not in Session envelopes.
- **Address stickiness.** Spoofed plaintext, Hellos and replayed Initial envelopes cannot redirect a node record whose key is known. Moving it requires a Session or Data envelope from the new address (see [§6.2](#62-hello-private-networks-only) and [§6.5](#65-receiving)).
- **Fragment integrity.** Once a fragmented message arrives in envelopes, only parts from the same sender key are merged. Spoofed plaintext or foreign-key parts cannot alter or block it ([§6.5](#65-receiving), step 5).
- **Address validation.** Session and Data envelopes only open from the address our ephemeral key was sent to. An off-path attacker cannot send one from a spoofed address, even with its own valid static key, so it cannot `put`, `refresh` or `listen` on behalf of another address.
- **Private-network admission.** Without a valid credential and its matching secret key, a node:
- cannot read any traffic;
- cannot get answers it can decrypt;
- is never listed by honest nodes.
- **Network separation.** Different network IDs derive different keys.
- **DoS cost.** An unauthenticated Data envelope costs a lookup and at most two AEAD operations. Other envelopes pass the public-key limit before any key derivation or signature verification.
### 9.2 Limitations
Each item states who it applies to, what that party can do, and what it cannot do.
- **Encryption is hop-by-hop.** Nodes that store values read them. Only `SecureDht` protects values end to end ([§1](#1-scope)).
- **Public mode is trust on first use.** Applies in public networks, to an attacker who modifies traffic on the path between two nodes A and B before A knows B's key.
- A node learns a peer's key from the first plaintext message that carries it with the peer's ID: `k` in a message from the peer, or `k4`/`k6` in a reply from any node. An attacker that removes these fields from every such message keeps A and B in plaintext, and reads and modifies their traffic. The downgrade ends with the first message carrying a key that the attacker misses: the node that receives it then sends only envelopes to that peer, which the attacker cannot alter, and treats plaintext claiming the peer's ID as untrusted ([§6.5](#65-receiving), step 7). The attacker can still drop their traffic.
- The attacker cannot bind its own key to B's ID. It can rewrite B's plaintext messages under an ID and key of its own, so that A talks to the attacker's node at B's address, and the attacker relays to B. A then knows the attacker's node at that address, not B.
- Until A knows a key for an ID, any plaintext sender can claim that ID from any address. The claim lasts until A learns the key: plaintext claiming the ID is untrusted from then on, and the first Session or Data envelope under it moves the node record to its sender's address.
- A key received in an encrypted reply cannot be stripped. One received in a plaintext reply can, by an attacker on that reply's path.
- The first request to a node with an unknown key, for example a bootstrap ping, is in plaintext.
- Private networks are not affected: they never send or accept plaintext, and credentials authenticate keys.
- **Compromise of a static secret key.** Applies to whoever obtains `sk_B`, for example from B's key file or state file, for as long as B keeps that key.
- It can act as B from any address, and talk to any node under B's ID.
- It can read every Initial envelope ever sent to B, including recorded ones. Initial keys depend only on `sk_B` and the sender's public keys.
- It can send Initial envelopes to B under any other node's ID. B answers `ping` and `find_node` in them, under keys the attacker cannot derive, and changes no state ([§6.5](#65-receiving), step 7). GET and writes are refused with `401`.
- It cannot read Session or Data envelopes between B and other nodes, recorded or live, nor impersonate another node to B in them. That requires B's ephemeral secret, or the other node's static secret.
- In a private network, it stays admitted for as long as B's granting authority is trusted. The credential is not secret: Hellos carry it in clear.
- Compromise of a running node also exposes its epoch secrets, and with them all sessions of its current and previous epoch.
Recover by changing the key. This changes the node ID, and in a private network needs a new credential. Changing B's key alone does not exclude the compromised key: its grant remains valid until its authority is distrusted, as described below. The old key cannot be revoked in a public network.
- **Initial envelopes are not forward-secret against the receiver.** An Initial envelope stays confidential if the sender's static key is compromised later, but not if the receiver's is. Initial envelopes carry:
- `ping` or `find_node` to a node whose ephemeral key is unknown, including their retransmissions;
- control replies to plaintext requests advertising a key. Application requests wait for a Session ([§6.4](#64-sending)).
Routing targets and control traffic remain exposed to a later compromise of the receiver's static key. Application GET/SELECT and write payloads do not enter Initial envelopes. Answers to Initial envelopes are Session envelopes, and are forward-secret.
- **No replay protection.** Nodes keep no record of received nonces. A captured envelope is accepted again as long as the receiver holds the keys that open it:
- **Initial envelopes** open from any address, for as long as the receiver's static key is unchanged. A replayed `ping` or `find_node` is answered again, to the replayer's source address, under keys the replayer cannot derive. GET and writes are refused with `401`, value updates are dropped, and replies and errors are accepted only for a request pending toward that address ([§6.5](#65-receiving), step 7). Reflection remains for control replies: anyone holding a captured Initial request, or a Hello, can make the receiver send an encrypted answer to an address of its choice. In a private network this includes outsiders, because the captured packet carries its sender's credential. Rate limits bound request frequency, not reply size; node-list packet-size limits remain separate work.
- **Session and Data envelopes** open only from their original source address, which the replayer must spoof, and only until the epoch that opened them is erased: at most one hour. A replayed request is processed again as its sender, including a write the sender did make: a repeated `put`, `refresh` or `listen` extends the life of a value or of a listener. A replayed value update is accepted while its listen is open, and repeats it: the values it lists are added or refreshed again, and the expirations it announces are signaled again. A replayed reply or error is accepted only if its transaction ID is pending. Transaction IDs are 32-bit counters per peer with a random start, so an old ID is pending again only after $2^{32}$ requests to that peer.
- **On-path attackers get write authorization for the addresses they observe.** Applies to an attacker that reads the packets we send to an address `addr`. Our ephemeral key `epk(addr)` is in the clear header of every Initial and Session envelope we send there: at each first contact, retransmission and epoch rotation. Data envelopes do not carry it.
- With its own key pair, the attacker can send us Session envelopes with source address `addr`, and they open. It then has what the owner of `addr` has: write authorization ([§6.6](#66-write-authorization)). Its `put` and `refresh` consume the storage quota of `addr`, and its `listen` directs value updates to `addr`, all under the attacker's own node ID. Each such envelope also replaces the session cached for `addr`, so Data envelopes from the real owner are dropped until its next retransmission establishes a new session ([§6.4](#64-sending), recovery).
- It cannot impersonate the node at `addr`, nor read or alter its traffic. That requires the static secret of that node.
- A captured `epk(addr)` is usable until the epoch that produced it is erased: at most one hour.
- In a private network, the attacker's key must also hold a valid credential.
This is the same exposure as legacy tokens, which are sent in clear to the address they authorize. Address validation defends against off-path spoofing only.
- **Address changes.** Our ephemeral key for a peer is derived from its address. When a peer's address or port changes, for example after a NAT rebinding, its Session and Data envelopes to us no longer open, and ours no longer reach it. Its next retransmission is a ping in an Initial envelope, which starts a new session for the new address, and the request follows in a Session envelope ([§6.4](#64-sending), recovery).
- **No Sybil resistance.** An identity costs one key generation. Encryption prevents choosing a node ID ([§9.1](#91-properties)), not creating many of them. In public networks, legacy nodes still choose their IDs freely.
- **Denial of service.** Public-key operations on unauthenticated packets are bounded by the public-key limit ([§6.5](#65-receiving)): by default 512 packets per second globally, and 64 per source address or IPv6 /64 prefix.
- An attacker spending that budget from many or spoofed addresses drops Initial, Hello and uncached Session envelopes from new peers, and plaintext requests from keyed senders. Answers to our own requests have a separate budget, which only an attacker spoofing the addresses of the peers we contact can spend. Data envelopes, cached sessions and requests from established peers are unaffected.
- Likewise, a flood of plaintext or Initial requests drops other requests without a session, as in the legacy protocol, but not those of established peers.
- A unit stands for three to ten X25519 operations, so the default allows about 5000 per second. `Config::max_crypto_per_sec` adapts it to the CPU available.
- An attacker reachable at many addresses, for example an IPv6 prefix, can fill the session cache of an epoch: one slot costs it one round trip. Evicted peers have their Data envelopes dropped until their next retransmission, which starts a new session.
- **No per-grant revocation.** Applies to private networks. A grant is permanent: a compromised or excluded node can participate for as long as any authority that signed a grant for its key remains trusted. Compromise of an authority allows issuing arbitrary new grants. Exclusion requires every remaining node to remove the affected authority from its trust set and restart, discarding cached admissions. This also excludes every member relying only on that authority; retained members need grants from another trusted authority. Multiple accepted authorities provide a transition path, but do not neutralize a compromised authority while it is still trusted. There is no clock-based fallback, revocation list, or automatic expiry.
- **Metadata.** Anyone observing the traffic sees:
- in Initial and Session envelopes, the sender's static key, hence its node ID, and its ephemeral key: who talks to whom, at each first contact, retransmission and epoch rotation;
- in Data envelopes, only the type; the sender is still identified by its address, from the Session envelopes that preceded;
- in every envelope, the type, the credential flag, the size and the timing, which can reveal the kind of message, for example fragmented value data;
- Hello messages entirely, including the credential, which reveals the node's key. A copied credential is useless without the matching secret key;
- in public networks, plaintext messages entirely, including node lists.
Node addresses are not hidden: they are in node lists, and on the packets themselves.
- **Node state.** The saved node list holds IDs and addresses, but no keys. In a public network, saved nodes are contacted again in plaintext, with the first-contact exposure above. In a private network, a saved node with an ID but no key cannot be contacted ([§6.4](#64-sending), step 3): a restarted node needs a bootstrap address to rejoin, and reuses saved nodes once it learns their keys from node lists.
## 10. Operating a private network
With `dhtnode`:
```sh
# Once: create the authority (net.key = secret seed, net.pub = hex public key)
dhtnode --gen-network-authority net
# Once per node: create its transport key and permanent membership grant
dhtnode --transport-key node1.key --issue-credential node1.cred --authority-key net.key
# Run a node
dhtnode --transport-key node1.key --network-credential node1.cred --network-authority $(cat net.pub)
```
Authority generation refuses to run if either output path already exists, including a dangling symlink.
Keep the authority secret offline. Distribute only `net.pub` and the per-node credentials. Without a compromise or an intentional membership change, keep these keys, grants, and trust sets unchanged: there is no scheduled renewal or maintenance.
`--network-authority` can be repeated to trust several authorities during an explicit transition:
```sh
dhtnode --transport-key node1.key --network-credential node1.cred \
--network-authority $(cat net.pub) --network-authority $(cat replacement.pub)
```
To move away from an authority:
1. Create a replacement authority and configure retained members to trust both old and replacement public keys. Restart them with this overlapping trust set.
2. Issue replacement grants only to retained, uncompromised transport keys and restart those nodes with their new grants. Old and replacement grants interoperate while both authorities are trusted. A compromised transport key must be replaced as well.
3. Remove the old authority from every retained node's configuration and restart them, clearing prior admissions. Only grants from the remaining trusted authorities are then accepted.
A compromised authority or a compromised key's old grant remains usable wherever the old authority is still trusted. The overlap is a transition mechanism, not immediate exclusion; it ends only when all retained nodes stop trusting the affected authority. Uncompromised grants from other authorities do not need updating.
From the library, set `Config::transport` before starting:
- `authorities`;
- `key`, which must be the key the credential was issued for;
- `credential`.
## 11. Constants
| Name | Value |
|---|---|
| Envelope marker | `0xC1` |
| Envelope types | `Initial = 1`, `Session = 2`, `Hello = 3`, `Data = 4` |
| Credential flag | `0x80` |
| Key, nonce, tag sizes | 32, 12, 16 bytes |
| Envelope header | 66 bytes, 2 for Data |
| Envelope overhead | 30 bytes for Data, 94 for Initial and Session (191 with credential) |
| Epoch (ephemeral keys and tokens) | 30 minutes, previous epoch accepted for one more period |
| Cached sessions per epoch | 16384, least recently used evicted |
| Credential size, version | 97 bytes, `0x02` |
| Node ID context | `"opendht-node-id-v1"` |
| HKDF salt | `"opendht-transport-v2"` |
| Credential signature context | `"opendht-credential-v2"` |
| Ephemeral key, token contexts | `"opendht-address-key-v1"`, `"opendht-address-token-v1"` |
| Token size | 32 bytes |
| Value fragment size | 1184 bytes, less the envelope overhead (1154 to 993) |
+19 -1
View File
@@ -4,6 +4,7 @@
#include "infohash.h"
#include "value.h"
#include "transport.h"
#include <vector>
#include <memory>
@@ -97,7 +98,7 @@ struct OPENDHT_PUBLIC NodeInfo
*/
struct OPENDHT_PUBLIC Config
{
/** DHT node ID */
/** DHT node ID. Ignored if transport encryption is enabled: the node ID derives from the transport key. */
InfoHash node_id {};
/**
@@ -122,6 +123,20 @@ struct OPENDHT_PUBLIC Config
/** If non-0, overrides the default per-IP address rate-limit. -1 means no limit. */
ssize_t max_peer_req_per_sec {0};
/**
* If non-0, override the rate-limits of requests from senders without an encrypted session
* (default: same as above). -1 means no limit.
*/
ssize_t max_unauth_req_per_sec {0};
ssize_t max_peer_unauth_req_per_sec {0};
/**
* If non-0, override the rate-limits of packets needing a transport key agreement
* or credential check. -1 means no limit.
*/
ssize_t max_crypto_per_sec {0};
ssize_t max_peer_crypto_per_sec {0};
/* If non-0, overrides the default maximum number of searches. -1 means no limit. */
ssize_t max_searches {0};
@@ -143,6 +158,9 @@ struct OPENDHT_PUBLIC Config
/* Client mode, node will not be used by other nodes to store data. */
bool client_mode {false};
/** Encryption of the DHT protocol, and optional private network */
TransportConfig transport {};
};
/**
+10 -16
View File
@@ -277,9 +277,6 @@ public:
std::vector<ValuesExport> exportValues() const override;
void importValues(const std::vector<ValuesExport>&) override;
void saveState(const std::string& path) const;
void loadState(const std::string& path);
NodeStats getNodesStats(sa_family_t af) const override;
std::string getStorageLog() const override;
@@ -367,8 +364,6 @@ private:
static constexpr std::chrono::seconds BOOTSTRAP_PERIOD {10};
static constexpr size_t TOKEN_SIZE {32};
// internal structures
struct SearchNode;
struct Get;
@@ -379,13 +374,14 @@ private:
Dht(const Dht&) = delete;
Dht& operator=(const Dht&) = delete;
void saveState(const std::string& path) const;
/* Only called on construction: may replace the transport key */
void loadState(const std::string& path);
std::mt19937_64 rd;
InfoHash myid {};
uint64_t secret {};
uint64_t oldsecret {};
// registred types
TypeStore types;
@@ -438,6 +434,8 @@ private:
net::NetworkEngine network_engine;
std::string persistPath;
/* The transport key was generated: keep it in the persisted state */
bool persistTransportKey {false};
// are we a bootstrap node ?
// note: Any running node can be used as a bootstrap node.
@@ -452,11 +450,6 @@ private:
return public_stable ? LISTEN_EXPIRE_TIME_PUBLIC : LISTEN_EXPIRE_TIME;
}
void rotateSecrets();
Blob makeToken(const SockAddr&, bool old) const;
bool tokenMatch(const Blob& token, const SockAddr&) const;
void reportedAddr(const SockAddr&);
// Storage
@@ -576,6 +569,8 @@ private:
* @param query The query sent to the node.
*/
void searchNodeGetExpired(const net::Request& status, bool over, std::weak_ptr<Search> ws, Sp<Query> query);
void searchNodeRoutingDone(const net::Request& req, net::RequestAnswer&& answer, std::weak_ptr<Search> ws);
void searchNodeRoutingExpired(const net::Request& req, bool over, std::weak_ptr<Search> ws);
/**
* This method recovers sends individual request for values per id.
@@ -627,15 +622,14 @@ private:
void onGetValuesDone(const Sp<Node>& status, net::RequestAnswer& a, Sp<Search>& sr, const Sp<Query>& orig_query);
/* when we receive a listen request */
net::RequestAnswer onListen(
Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version = 0);
Sp<Node> node, const InfoHash& hash, size_t socket_id, const Query& query, int version = 0);
void onListenDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr);
/* when we receive an announce request */
net::RequestAnswer onAnnounce(Sp<Node> node,
const InfoHash& hash,
const Blob& token,
const std::vector<Sp<Value>>& v,
const time_point& created);
net::RequestAnswer onRefresh(Sp<Node> node, const InfoHash& hash, const Blob& token, const Value::Id& vid);
net::RequestAnswer onRefresh(Sp<Node> node, const InfoHash& hash, const Value::Id& vid);
void onAnnounceDone(const Sp<Node>& status, net::RequestAnswer& a, Sp<Search>& sr);
};
+190 -37
View File
@@ -12,13 +12,16 @@
#include "rate_limiter.h"
#include "logger.h"
#include "network_utils.h"
#include "transport.h"
#include <vector>
#include <string>
#include <functional>
#include <algorithm>
#include <list>
#include <memory>
#include <queue>
#include <tuple>
namespace dht {
namespace net {
@@ -34,9 +37,18 @@ struct TransId;
struct NetworkConfig
{
NetId network {0};
/* Requests in Session or Data envelopes, from a validated address */
ssize_t max_req_per_sec {0};
ssize_t max_peer_req_per_sec {0};
/* Other requests. 0: same as above */
ssize_t max_unauth_req_per_sec {0};
ssize_t max_peer_unauth_req_per_sec {0};
/* Packets needing a key agreement or signature check. 0: same as max_req_per_sec */
ssize_t max_crypto_per_sec {0};
ssize_t max_peer_crypto_per_sec {0};
bool is_client {false};
/** Transport encryption is used only if enabled and a key is set */
TransportConfig transport {};
};
class DhtProtocolException : public DhtException
@@ -83,7 +95,8 @@ struct ParsedMessage;
*/
struct RequestAnswer
{
Blob ntoken {};
/* Write token of the node; empty if it needs none (encrypted), nullopt if not granted. */
std::optional<Blob> ntoken {};
Value::Id vid {};
std::vector<Sp<Value>> values {};
std::vector<Value::Id> refreshed_values {};
@@ -160,34 +173,31 @@ private:
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, want_t, const Query&)> onGetValues {};
/**
* Called on listen request.
* Called on listen request, from a node proven to receive at its address.
*
* @param node (type: Sp<Node>) the requesting node.
* @param h (type: InfoHash) hash of the value of interest.
* @param token (type: Blob) security token.
* @param rid (type: uint16_t) request id.
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int)> onListen {};
std::function<RequestAnswer(Sp<Node>, const InfoHash&, Tid, const Query&, int)> onListen {};
/**
* Called on announce request.
* Called on announce request, from a node proven to receive at its address.
*
* @param node (type: Sp<Node>) the requesting node.
* @param h (type: InfoHash) hash of the value of interest.
* @param token (type: Blob) security token.
* @param values (type: std::vector<Sp<Value>>) values to store.
* @param created (type: time_point) time when the value was created.
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, const std::vector<Sp<Value>>&, const time_point&)>
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const std::vector<Sp<Value>>&, const time_point&)>
onAnnounce {};
/**
* Called on refresh request.
* Called on refresh request, from a node proven to receive at its address.
*
* @param node (type: Sp<Node>) the requesting node.
* @param h (type: InfoHash) hash of the value of interest.
* @param token (type: Blob) security token.
* @param vid (type: Value::id) the value id.
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, const Value::Id&)> onRefresh {};
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Value::Id&)> onRefresh {};
public:
using RequestCb = std::function<void(const Request&, RequestAnswer&&)>;
@@ -214,6 +224,15 @@ public:
net::DatagramSocket* getSocket() const { return dht_socket.get(); };
/** Our transport key, or nullptr if transport encryption is disabled */
const transport::KeyPair* getTransportKey() const { return transport_ ? &config.transport.key : nullptr; }
/** Replaces our transport key. Only valid before any message is exchanged. */
void setTransportKey(const transport::KeyPair& key)
{
if (transport_)
config.transport.key = key;
}
void clear();
/**
@@ -225,7 +244,6 @@ public:
* @param socket_id The tid to use to write to the request socket.
* @param hash The hash key of the value.
* @param want Wether to send ipv4 and/or ipv6 nodes.
* @param ntoken Listen security token.
* @param nodes The ipv4 closest nodes.
* @param nodes6 The ipv6 closest nodes.
* @param values The values to send.
@@ -235,7 +253,6 @@ public:
Tid socket_id,
const InfoHash& hash,
want_t want,
const Blob& ntoken,
std::vector<Sp<Node>>&& nodes,
std::vector<Sp<Node>>&& nodes6,
std::vector<Sp<Value>>&& values,
@@ -245,13 +262,11 @@ public:
void tellListenerRefreshed(const Sp<Node>& n,
Tid socket_id,
const InfoHash& hash,
const Blob& ntoken,
const std::vector<Value::Id>& values,
int version);
void tellListenerExpired(const Sp<Node>& n,
Tid socket_id,
const InfoHash& hash,
const Blob& ntoken,
const std::vector<Value::Id>& values,
int version);
@@ -264,6 +279,8 @@ public:
* Requests *
**************/
bool needsSession(const Node& node) const;
/**
* Send a "ping" request to a given node.
*
@@ -461,7 +478,10 @@ public:
size_t getNodeCacheSize() const { return cache.size(); }
size_t getNodeCacheSize(sa_family_t af) const { return cache.size(af); }
size_t getRateLimiterSize() const { return address_rate_limiter.size(); }
size_t getRateLimiterSize() const
{
return requestLimiter_.size() + unauthLimiter_.size() + cryptoLimiter_.size() + replyCryptoLimiter_.size();
}
size_t getPartialCount() const { return partial_messages.size(); }
@@ -486,22 +506,136 @@ private:
reason to increase this value. */
static constexpr unsigned BLACKLISTED_MAX {10};
static constexpr size_t MTU {1280};
/* IPv6 minimum MTU, less the IPv6 and UDP headers */
static constexpr size_t MAX_PACKET_SIZE {1280 - 40 - 8};
/* Upper bound of the msgpack framing around a value fragment */
static constexpr size_t FRAGMENT_HEADER_SIZE {48};
/* Value fragment sizes keeping each packet within the minimum MTU, less the envelope */
static constexpr size_t FRAGMENT_SIZE {MAX_PACKET_SIZE - FRAGMENT_HEADER_SIZE};
static constexpr size_t ENCRYPTED_FRAGMENT_SIZE {FRAGMENT_SIZE - transport::MAX_ENVELOPE_OVERHEAD};
static constexpr size_t MAX_PACKET_VALUE_SIZE {600};
static constexpr size_t MAX_MESSAGE_VALUE_SIZE {56 * 1024};
static constexpr size_t RX_MAX_EARLY_BYTES {MAX_MESSAGE_VALUE_SIZE + MAX_VALUE_SIZE + 32};
static constexpr size_t RX_MAX_EARLY_PARTS {RX_MAX_EARLY_BYTES / MTU + 2};
static constexpr size_t RX_MAX_EARLY_PARTS {RX_MAX_EARLY_BYTES / ENCRYPTED_FRAGMENT_SIZE + 2};
/* Lifetime of ephemeral transport keys and write tokens: in use for one period, then accepted for another */
static constexpr std::chrono::minutes EPOCH_DURATION {30};
/* Cached session keys per epoch */
static constexpr size_t MAX_SESSIONS {16 * 1024};
static constexpr size_t TOKEN_SIZE {transport::KEY_SIZE};
struct Session
{
/* our ephemeral key for the peer address */
transport::Key ephemeral;
transport::Key peer;
transport::Key peerEphemeral;
transport::SessionKeys keys;
/* the peer has these keys and accepted us: send Data envelopes */
bool confirmed {false};
bool authorized {false};
};
/* The secret from which ephemeral keys and tokens derive, and session keys, all erased together.
* A session is cached for an address only once its peer proved to receive there: it opened an envelope
* encrypted with our key for that address. Replies to Initial and Hello envelopes never cache one,
* since their source address can be spoofed. */
struct Epoch
{
struct Entry
{
Session session;
std::list<SockAddr>::iterator used;
};
transport::Key secret {};
std::map<SockAddr, Entry> sessions {};
/* most recently used first */
std::list<SockAddr> used {};
explicit operator bool() const { return secret != transport::Key {}; }
transport::KeyPair ephemeral(const SockAddr& addr) const;
Blob token(const SockAddr& addr) const;
/* Marks the session as used */
Session* find(const SockAddr& addr);
Session* find(const SockAddr& addr, const transport::Key& peer, const transport::Key& peerEphemeral);
/* Evicts the least recently used session when full */
void store(const SockAddr& addr, const Session& session);
void wipe();
};
struct PendingSession
{
Epoch* epoch;
Session session;
};
void rotateEpoch();
/* Session with the peer at `addr`, or nullopt for an invalid peer key. Cached if the peer ephemeral key is validated. */
std::optional<Session> session(const transport::Peer& peer, const SockAddr& addr);
std::optional<Blob> openSession(const transport::Header& header,
const uint8_t* buf,
size_t buflen,
const SockAddr& from,
std::optional<PendingSession>& pending,
Session*& cached);
/* Fills the sender keys of `header` from the session */
std::optional<Blob> openData(
transport::Header& header, const uint8_t* buf, size_t buflen, const SockAddr& from, Session*& cached);
struct OpenedEnvelope
{
Sp<transport::Peer> peer;
Blob payload;
/* unauthenticated */
bool hello {false};
/* Session or Data envelope: the sender got our ephemeral key for its address */
bool session {false};
std::optional<PendingSession> pending {};
Session* cached {};
};
std::optional<OpenedEnvelope> openEnvelope(const uint8_t* buf, size_t buflen, const SockAddr& from);
bool authorize(transport::Peer& peer, const uint8_t* credential, size_t size) const;
void attachPeer(Node& node, const Sp<transport::Peer>& peer);
Sp<transport::Peer> makePeer(const InfoHash& id, const transport::Key& key) const;
void processHello(const ParsedMessage& msg, const Sp<transport::Peer>& peer, const SockAddr& from);
/* Advertise our transport key in plaintext messages to nodes not known to support encryption */
bool advertiseKey(const Node& node) const
{
return transport_ and not config.transport.isPrivate() and not node.getTransport();
}
/* Messages to the node are encrypted */
bool encrypts(const Node& node) const { return transport_ and node.getTransport(); }
/* Token granting writes to a plaintext node. Encrypted nodes need none. */
Blob writeToken(const Node& node) const { return encrypts(node) ? Blob {} : epoch_.token(node.getAddr()); }
bool tokenMatch(const Blob& token, const SockAddr& addr) const;
/* Writes need proof that the sender receives at its address: a Session envelope, or else a token */
void authorizeWrite(const ParsedMessage& msg, const Node& node, const std::string& error) const;
/* Number of optional top-level fields packed by packOptionalFields */
unsigned optionalFields(bool key) const
{
return (config.network ? 1 : 0) + (config.is_client ? 1 : 0) + (key ? 1 : 0);
}
void packOptionalFields(msgpack::packer<msgpack::sbuffer>& pk, bool key) const;
void process(std::unique_ptr<ParsedMessage>&&, const SockAddr& from);
bool isExpectedReply(const ParsedMessage&, const SockAddr& from);
bool tryCompleteMessage(PartialMessage&, const SockAddr& from);
bool rateLimit(const SockAddr& addr);
/* Admits a request, unless `charged` already: Session and Data senders have their own budget.
* A plaintext request whose answer takes an Initial envelope also needs crypto budget. */
bool limitRequest(const ParsedMessage& msg, const SockAddr& from, bool charged = false);
/* Admits a packet needing a key agreement, from a separate budget if we await a reply from there */
bool limitCrypto(const transport::Key& sender, const SockAddr& from);
bool expectsReply(const transport::Key& sender, const SockAddr& from);
static bool isMartian(const SockAddr& addr);
bool isNodeBlacklisted(const SockAddr& addr) const;
void requestStep(Sp<Request> req);
void transmitRequest(const Sp<Request>& req);
void notifyRequestTimeout(Request& req);
void completeSession(Node& node);
/* Re-establishes the session with a ping in an Initial envelope, unless one is pending */
void recoverSession(const Sp<Node>& node);
/* Sends the requests withheld during the recovery, under the new session */
void resumeRequests(Node& node);
/**
* Sends a request to a node. Request::MAX_ATTEMPT_COUNT attempts will
@@ -521,9 +655,13 @@ private:
};
// basic wrapper for socket sendto function
int send(const SockAddr& addr, const char* buf, size_t len, bool confirmed = false);
int sendRaw(const SockAddr& addr, const uint8_t* buf, size_t len, bool confirmed = false);
// send to a node, encrypted if supported
int send(const Node& node, const SockAddr& addr, const char* buf, size_t len, bool confirmed = false);
/* Bytes added by the envelope that send() uses for the node at `addr` */
size_t envelopeOverhead(const Node& node, const SockAddr& addr);
void sendValueParts(Tid tid, const std::vector<Blob>& svals, const SockAddr& addr);
void sendValueParts(Tid tid, const std::vector<Blob>& svals, const Node& node, const SockAddr& addr);
std::vector<Blob> packValueHeader(msgpack::sbuffer&,
std::vector<Sp<Value>>::const_iterator,
std::vector<Sp<Value>>::const_iterator) const;
@@ -531,50 +669,65 @@ private:
{
return packValueHeader(buf, values.begin(), values.end());
}
using PartialMessageKey = std::pair<SockAddr, Tid>;
using PartialMessageKey = std::tuple<SockAddr, Tid, transport::Key>;
void maintainRxBuffer(const PartialMessageKey& key);
/*************
* Answers *
*************/
/* answer to a ping request */
void sendPong(const SockAddr& addr, Tid tid);
void sendPong(const Node& node, const SockAddr& addr, Tid tid);
struct BufferedNodes
{
Blob nodes4, nodes6;
/* transport keys, aligned with nodes, zeros if unknown. Empty if none is known. */
Blob keys4, keys6;
};
/* answer to findnodes/getvalues request */
void sendNodesValues(const SockAddr& addr,
void sendNodesValues(const Node& node,
const SockAddr& addr,
Tid tid,
const Blob& nodes,
const Blob& nodes6,
const BufferedNodes& nodes,
const std::vector<Sp<Value>>& st,
const Query& query,
const Blob& token);
Blob bufferNodes(sa_family_t af, const InfoHash& id, std::vector<Sp<Node>>& nodes);
std::pair<Blob, Blob> bufferNodes(sa_family_t af, const InfoHash& id, std::vector<Sp<Node>>& nodes);
std::pair<Blob, Blob> bufferNodes(
BufferedNodes bufferNodes(
sa_family_t af, const InfoHash& id, want_t want, std::vector<Sp<Node>>& nodes, std::vector<Sp<Node>>& nodes6);
/* answer to a listen request */
void sendListenConfirmation(const SockAddr& addr, Tid tid, const Blob& token = {});
void sendListenConfirmation(const Node& node, const SockAddr& addr, Tid tid, const Blob& token = {});
/* answer to put request */
void sendValueAnnounced(const SockAddr& addr, Tid, Value::Id);
void sendValueAnnounced(const Node& node, const SockAddr& addr, Tid, Value::Id);
/* answer in case of error */
void sendError(const SockAddr& addr, Tid tid, uint16_t code, const std::string& message, bool include_id = false);
void sendError(const Node& node,
const SockAddr& addr,
Tid tid,
uint16_t code,
const std::string& message,
bool include_id = false);
void deserializeNodes(ParsedMessage& msg, const SockAddr& from);
/* DHT info */
const InfoHash& myid;
const NetworkConfig config {};
NetworkConfig config {};
const bool transport_ {false};
const Blob credential_ {};
Epoch epoch_ {};
Epoch previousEpoch_ {};
Sp<Scheduler::Job> epochRotation_ {};
const std::unique_ptr<DatagramSocket> dht_socket;
Sp<Logger> logger_;
std::mt19937_64& rd;
NodeCache cache;
// global limiting should be triggered by at least 8 different IPs
using IpLimiter = RateLimiter;
using IpLimiterMap = std::map<SockAddr, IpLimiter, SockAddr::ipCmp>;
IpLimiterMap address_rate_limiter;
RateLimiter rate_limiter;
ssize_t limiter_maintenance {0};
/* Requests from Session/Data senders, other requests, public-key work for unexpected and expected senders */
AddressRateLimiter requestLimiter_;
AddressRateLimiter unauthLimiter_;
AddressRateLimiter cryptoLimiter_;
AddressRateLimiter replyCryptoLimiter_;
// requests handling
std::map<Tid, Sp<Request>> requests {};
+9
View File
@@ -18,6 +18,9 @@ struct Request;
struct Socket;
struct RequestAnswer;
} /* namespace net */
namespace transport {
struct Peer;
}
using Tid = uint32_t;
using SocketCb = std::function<void(const Sp<Node>&, net::RequestAnswer&&)>;
@@ -46,6 +49,10 @@ struct Node
bool isClient() const { return is_client; }
bool isIncoming() { return time > reply_time; }
/** Transport encryption state, if the node is known to support it */
const Sp<transport::Peer>& getTransport() const { return transport_; }
void setTransport(const Sp<transport::Peer>& peer) { transport_ = peer; }
const time_point& getTime() const { return time; }
const time_point& getReplyTime() const { return reply_time; }
void setTime(const time_point& t) { time = t; }
@@ -84,6 +91,7 @@ struct Node
void requested(const Sp<net::Request>& req);
void received(time_point now, const Sp<net::Request>& req);
Sp<net::Request> getRequest(Tid tid);
const std::map<Tid, Sp<net::Request>>& getRequests() const { return requests_; }
void cancelRequest(const Sp<net::Request>& req);
void setExpired();
@@ -155,6 +163,7 @@ private:
std::map<Tid, Sp<net::Request>> requests_ {};
std::map<Tid, Sp<Socket>> sockets_;
Sp<transport::Peer> transport_ {};
};
} // namespace dht
+1
View File
@@ -24,6 +24,7 @@ struct NodeCache
* To use in case of connectivity change etc.
*/
void clearBadNodes(sa_family_t family = 0);
void clear();
NodeCache(std::mt19937_64& r)
: rd(r) {};
+56
View File
@@ -3,6 +3,9 @@
#pragma once
#include "utils.h"
#include "sockaddr.h"
#include <map>
#include <queue>
namespace dht {
@@ -23,6 +26,11 @@ public:
records.pop();
return records.size();
}
/** Whether quota remains, without using it */
bool available(const time_point& now)
{
return quota_ == std::numeric_limits<size_t>::max() or maintain(now) < quota_;
}
/** Return false if quota is reached, insert record and return true otherwise. */
bool limit(const time_point& now)
{
@@ -41,4 +49,52 @@ private:
std::queue<time_point> records {};
};
/**
* A global quota and a quota per address. IPv6 addresses share the quota of their /64 prefix.
* Negative quotas are unlimited.
*/
class AddressRateLimiter
{
public:
AddressRateLimiter(ssize_t quota, ssize_t peerQuota, const duration& period = std::chrono::seconds(1))
: global_(quota, period)
, peerQuota_(peerQuota)
, period_(period)
{}
bool limit(const SockAddr& addr, const time_point& now)
{
// Checked first so that dropped packets create no per-address record
if (not global_.available(now))
return false;
if (peerQuota_ >= 0) {
maintain(now);
auto it = peers_.find(addr);
if (it == peers_.end())
it = peers_.emplace(addr, RateLimiter(peerQuota_, period_)).first;
if (not it->second.limit(now))
return false;
}
return global_.limit(now);
}
/** Number of addresses with a record */
size_t size() const { return peers_.size(); }
private:
void maintain(const time_point& now)
{
if (now < nextMaintenance_)
return;
for (auto it = peers_.begin(); it != peers_.end();)
it = it->second.maintain(now) ? std::next(it) : peers_.erase(it);
nextMaintenance_ = now + period_;
}
RateLimiter global_;
const ssize_t peerQuota_;
const duration period_;
std::map<SockAddr, RateLimiter, SockAddr::ipCmp> peers_ {};
time_point nextMaintenance_ {time_point::min()};
};
} // namespace dht
+219
View File
@@ -0,0 +1,219 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include "def.h"
#include "infohash.h"
#include "sockaddr.h"
#include "utils.h"
#include <array>
#include <optional>
#include <vector>
namespace dht {
namespace transport {
constexpr size_t KEY_SIZE {32};
constexpr size_t SIGNATURE_SIZE {64};
using Key = std::array<uint8_t, KEY_SIZE>;
using Signature = std::array<uint8_t, SIGNATURE_SIZE>;
/** Erases secret key material. */
OPENDHT_PUBLIC void wipe(Key& key);
/**
* X25519 key pair used to encrypt the DHT protocol: the static key of a node, from which its node ID derives,
* or a periodically rotated ephemeral key.
*/
struct OPENDHT_PUBLIC KeyPair
{
Key secret {};
Key pub {};
static KeyPair generate();
static KeyPair fromSecret(const Key& secret);
explicit operator bool() const { return pub != Key {}; }
};
/** Node ID of a node using the transport public key `pub`. */
OPENDHT_PUBLIC InfoHash nodeId(const Key& pub);
/** Secret from the system CSPRNG. */
OPENDHT_PUBLIC Key randomSecret();
/** Ephemeral key pair used with the peer at `address` (IP and port), derived from an epoch secret. */
OPENDHT_PUBLIC KeyPair addressKey(const Key& epochSecret, const uint8_t* address, size_t size);
/** Write token of the plaintext peer at `address` (IP and port), derived from an epoch secret. */
OPENDHT_PUBLIC Key addressToken(const Key& epochSecret, const uint8_t* address, size_t size);
/** Non-expiring authority-signed grant binding a transport public key to a private network. */
struct OPENDHT_PUBLIC Credential
{
static constexpr size_t PACKED_SIZE {1 + KEY_SIZE + SIGNATURE_SIZE};
Key key {};
Signature signature {};
Blob pack() const;
/** @throws std::invalid_argument if the data is not a packed credential */
static Credential unpack(const uint8_t* data, size_t size);
static Credential unpack(const Blob& b) { return unpack(b.data(), b.size()); }
/** True if signed by one of the trusted authorities. */
bool verify(const std::vector<Key>& authorities) const;
explicit operator bool() const { return key != Key {}; }
};
/** Ed25519 key of a private network authority, issuing node credentials. */
struct OPENDHT_PUBLIC Authority
{
Key secret {};
Key pub {};
static Authority generate();
static Authority fromSecret(const Key& secret);
Credential issue(const Key& transportKey) const;
};
/** Transport state of a remote node, attached to its Node. */
struct Peer
{
explicit Peer(const Key& k)
: key(k)
{}
const Key key;
/** Last authenticated ephemeral key of the peer */
std::optional<Key> ephemeral {};
/** Address proven by the Session or Data envelope carrying the ephemeral key */
SockAddr validatedAddress {};
/** Private networks: the peer presented a grant from a trusted authority */
bool authorized {false};
/** Our session was lost: requests other than pings wait for a new ephemeral key */
bool recovering {false};
void setEphemeral(const Key& k, const SockAddr& address = {})
{
ephemeral = k;
validatedAddress = address;
}
void resetEphemeral()
{
ephemeral.reset();
validatedAddress = {};
}
};
/*
* Envelope: marker | type | sender static key | sender ephemeral key | nonce | ChaCha20-Poly1305(payload) | tag
* Data envelope: marker | type | nonce | ChaCha20-Poly1305(payload) | tag, keys found from the source address.
* The marker is never a valid msgpack first byte, so legacy nodes drop envelopes.
* Hello (private networks only, plaintext): marker | type | sender static key | sender ephemeral key | credential | message
*/
constexpr uint8_t ENVELOPE_MARKER {0xC1};
/** Initial: the sender doesn't know the receiver ephemeral key. Session: it does. Data: the receiver has the session. */
enum class EnvelopeType : uint8_t { Initial = 1, Session = 2, Hello = 3, Data = 4 };
/** Type flag: the payload starts with the sender credential */
constexpr uint8_t CREDENTIAL_FLAG {0x80};
constexpr size_t NONCE_SIZE {12};
constexpr size_t TAG_SIZE {16};
constexpr size_t HEADER_SIZE {2 + 2 * KEY_SIZE};
constexpr size_t DATA_HEADER_SIZE {2};
constexpr size_t ENVELOPE_OVERHEAD {HEADER_SIZE + NONCE_SIZE + TAG_SIZE};
constexpr size_t DATA_ENVELOPE_OVERHEAD {DATA_HEADER_SIZE + NONCE_SIZE + TAG_SIZE};
constexpr size_t MAX_ENVELOPE_OVERHEAD {ENVELOPE_OVERHEAD + Credential::PACKED_SIZE};
inline bool
isEnvelope(const uint8_t* data, size_t size)
{
return size >= DATA_HEADER_SIZE and data[0] == ENVELOPE_MARKER;
}
struct Header
{
EnvelopeType type;
bool credential;
/* not in Data envelopes */
Key sender;
Key ephemeral;
};
/** Envelope header, or nullopt if not an envelope of a known type. */
OPENDHT_PUBLIC std::optional<Header> parseHeader(const uint8_t* data, size_t size);
/** Keys to send to and receive from a peer. */
struct SessionKeys
{
Key tx;
Key rx;
/** Erases secret key material. */
void wipe()
{
transport::wipe(tx);
transport::wipe(rx);
}
/** Destructor that wipes secret key material. */
~SessionKeys()
{
wipe();
}
};
/** Session keys with a peer whose ephemeral key is known, or nullopt for an invalid peer key. */
OPENDHT_PUBLIC std::optional<SessionKeys> sessionKeys(
const KeyPair& own, const KeyPair& ephemeral, const Key& peer, const Key& peerEphemeral, NetId network);
/** Key of an Initial envelope to `peer`, or nullopt for an invalid peer key. */
OPENDHT_PUBLIC std::optional<Key> initialSendKey(const KeyPair& own,
const KeyPair& ephemeral,
const Key& peer,
NetId network);
/** Key of an Initial envelope from `peer`, or nullopt for an invalid peer key. */
OPENDHT_PUBLIC std::optional<Key> initialReceiveKey(const KeyPair& own,
const Key& peer,
const Key& peerEphemeral,
NetId network);
/** Encrypts `credential || data`, authenticating the envelope header. */
OPENDHT_PUBLIC Blob seal(const Key& key,
EnvelopeType type,
const Key& sender,
const Key& ephemeral,
const uint8_t* data,
size_t size,
const Blob& credential = {});
/** Encrypts `data` in a Data envelope. */
OPENDHT_PUBLIC Blob sealData(const Key& key, const uint8_t* data, size_t size);
/** Decrypted payload, or nullopt if the envelope is not authentic. */
OPENDHT_PUBLIC std::optional<Blob> open(const Key& key, const uint8_t* data, size_t size);
OPENDHT_PUBLIC Blob
hello(const Key& sender, const Key& ephemeral, const Blob& credential, const uint8_t* data, size_t size);
} // namespace transport
/**
* Encryption of the DHT protocol. Peers that don't support it keep talking in plaintext,
* unless the node is part of a private network.
*/
struct TransportConfig
{
/** Advertise our key and encrypt traffic with nodes supporting it. Disabling uses a legacy random node ID. */
bool enabled {true};
/** Transport key. If empty, generated on start, and kept in the persisted state if Config::persist_path is set. */
transport::KeyPair key {};
/** Network authorities. If set, only nodes with a credential issued by one of them are accepted. */
std::vector<transport::Key> authorities {};
/** Credential of `key`, required for private networks. */
transport::Credential credential {};
bool isPrivate() const { return not authorities.empty(); }
};
} // namespace dht
+55 -7
View File
@@ -5,13 +5,14 @@ project(
'cython',
license: 'MIT',
license_files: 'LICENSE',
version: '4.5.0-rc.1',
version: '4.4.99',
meson_version: '>=1.1.0',
default_options: ['cpp_std=c++20', 'warning_level=3'],
)
gnutls = dependency('gnutls')
nettle = dependency('nettle')
hogweed = dependency('hogweed', version: '>=3.1')
msgpack = dependency('msgpack-cxx', required: false)
argon2 = dependency('libargon2')
fmt = dependency('fmt')
@@ -38,6 +39,7 @@ deps = [
fmt,
gnutls,
nettle,
hogweed,
msgpack,
argon2,
openssl,
@@ -55,12 +57,7 @@ if host_machine.system() == 'linux' and io_uring.found()
endif
conf_data = configuration_data()
add_project_arguments(
'-DMSGPACK_NO_BOOST',
'-DASIO_STANDALONE',
'-DPACKAGE_VERSION="' + meson.project_version() + '"',
language: 'cpp',
)
add_project_arguments('-DMSGPACK_NO_BOOST', '-DASIO_STANDALONE', language: 'cpp')
if io_uring.found()
add_project_arguments('-DASIO_HAS_IO_URING', '-DASIO_DISABLE_EPOLL', language: 'cpp')
conf_data.set('iouring_lib', ', liburing')
@@ -89,6 +86,7 @@ opendht_src = [
'src/routing_table.cpp',
'src/node_cache.cpp',
'src/network_engine.cpp',
'src/transport.cpp',
'src/securedht.cpp',
'src/dhtrunner.cpp',
'src/log.cpp',
@@ -210,6 +208,48 @@ if get_option('c').enabled()
endif
endif
rust_option = get_option('rust').require(
get_option('c').enabled(),
error_message: 'rust requires c',
).require(
get_option('default_library') != 'static',
error_message: 'rust requires shared libraries',
)
if rust_option.allowed()
cargo = find_program('cargo', required: rust_option)
if cargo.found()
rust_env = environment()
rust_env.prepend('LIBRARY_PATH', meson.current_build_dir())
rust_env.prepend('LD_LIBRARY_PATH', meson.current_build_dir())
rust_env.prepend('DYLD_LIBRARY_PATH', meson.current_build_dir())
rust_env.prepend('PATH', meson.current_build_dir())
cargo_args = [
'--manifest-path', meson.current_source_dir() / 'rust/Cargo.toml',
'--target-dir', meson.current_build_dir() / 'rust',
]
rust_build = custom_target(
'rust-bindings',
output: 'rust-build.json',
command: [cargo, 'build', '--lib', '--examples', '--message-format=json-render-diagnostics'] + cargo_args,
env: rust_env,
depends: opendht_c,
capture: true,
build_by_default: true,
build_always_stale: true,
)
if get_option('tests').enabled()
test(
'Rust',
cargo,
args: ['test'] + cargo_args,
env: rust_env,
depends: rust_build,
timeout: 120,
)
endif
endif
endif
if get_option('python').require(
get_option('indexation').enabled(),
error_message: 'python requires indexation',
@@ -340,6 +380,14 @@ if get_option('tests').enabled()
)
test('Crypto', test_crypto, timeout: 60)
test_transport = executable(
'test_transport',
'tests/test_transport.cpp',
'tests/tests_runner.cpp',
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
)
test('Transport', test_transport)
test_dhtrunner = executable(
'test_dhtrunner',
'tests/test_dhtrunner.cpp',
+1
View File
@@ -4,6 +4,7 @@ option('push_notifications', type : 'feature', value : 'disabled')
option('peer_discovery', type : 'feature', value : 'enabled')
option('tools', type : 'feature', value : 'enabled')
option('c', type : 'feature', value : 'enabled')
option('rust', type : 'feature', value : 'disabled')
option('indexation', type : 'feature', value : 'enabled')
option('python', type : 'feature', value : 'enabled')
option('tests', type : 'feature', value : 'enabled')
+1 -1
View File
@@ -8,5 +8,5 @@ Version: @VERSION@
Libs: -L${libdir} -lopendht -lfmt
Libs.private: @http_lib@ -pthread
Requires: gnutls >= 3.3@jsoncpp_lib@@openssl_lib@
Requires.private: nettle >= 2.4@argon2_lib@@llhttp_lib@@iouring_lib@@simdutf_lib@
Requires.private: nettle >= 2.4, hogweed >= 3.1@argon2_lib@@llhttp_lib@@iouring_lib@@simdutf_lib@
Cflags: -I${includedir}@opendht_public_cflags@
+3
View File
@@ -866,6 +866,9 @@ cdef class DhtConfig(object):
def setRateLimit(self, ssize_t max_req_per_sec, ssize_t max_peer_req_per_sec):
self._config.dht_config.node_config.max_req_per_sec = max_req_per_sec
self._config.dht_config.node_config.max_peer_req_per_sec = max_peer_req_per_sec
def setCryptoRateLimit(self, ssize_t max_crypto_per_sec, ssize_t max_peer_crypto_per_sec):
self._config.dht_config.node_config.max_crypto_per_sec = max_crypto_per_sec
self._config.dht_config.node_config.max_peer_crypto_per_sec = max_peer_crypto_per_sec
def setProxyInfo(self, str proxy_server, str push_node_id="", str push_token="", str push_topic="", str push_platform="", str user_agent="", Certificate server_ca = Certificate()):
self._config.proxy_server = proxy_server.encode()
self._config.push_node_id = push_node_id.encode()
+2
View File
@@ -283,6 +283,8 @@ cdef extern from "opendht/callbacks.h" namespace "dht":
string persist_path
size_t max_req_per_sec
size_t max_peer_req_per_sec
ssize_t max_crypto_per_sec
ssize_t max_peer_crypto_per_sec
cppclass SecureDhtConfig:
Config node_config
Identity id
+1
View File
@@ -11,6 +11,7 @@ from opendht import aio as dht
async def main():
config = dht.DhtConfig()
config.setRateLimit(-1, -1)
config.setCryptoRateLimit(-1, -1)
ping_node = dht.DhtRunner()
pong_node = dht.DhtRunner()
+123 -113
View File
@@ -10,6 +10,7 @@
#include <msgpack.hpp>
#include <algorithm>
#include <filesystem>
#include <random>
#include <sstream>
#include <fstream>
@@ -27,6 +28,7 @@ constexpr duration Dht::LISTEN_EXPIRE_TIME;
constexpr duration Dht::LISTEN_EXPIRE_TIME_PUBLIC;
constexpr duration Dht::REANNOUNCE_MARGIN;
static constexpr size_t MAX_REQUESTS_PER_SEC {8 * 1024};
static constexpr size_t MAX_CRYPTO_PER_SEC {512};
static constexpr duration BOOTSTRAP_PERIOD_MAX {std::chrono::hours(24)};
NodeStatus
@@ -333,6 +335,30 @@ Dht::searchNodeGetExpired(const net::Request& status, bool over, std::weak_ptr<S
}
}
void
Dht::searchNodeRoutingDone(const net::Request& req, net::RequestAnswer&& answer, std::weak_ptr<Search> ws)
{
if (auto sr = ws.lock()) {
if (auto sn = sr->getNode(req.node); sn and sn->routingProbe.get() == &req) {
sr->insertNode(req.node, scheduler.time(), answer.ntoken);
scheduler.edit(sr->nextSearchStep, scheduler.time());
}
}
}
void
Dht::searchNodeRoutingExpired(const net::Request& req, bool over, std::weak_ptr<Search> ws)
{
if (auto sr = ws.lock()) {
if (auto sn = sr->getNode(req.node); sn and sn->routingProbe.get() == &req) {
sn->candidate = not over;
if (over)
sn->routingProbe.reset();
scheduler.edit(sr->nextSearchStep, scheduler.time());
}
}
}
void
Dht::paginate(std::weak_ptr<Search> ws, Sp<Query> query, SearchNode* n)
{
@@ -438,6 +464,16 @@ Dht::searchSendGetValues(Sp<Search> sr, SearchNode* pn, bool update)
if (not n)
continue;
if (network_engine.needsSession(*n->node)) {
n->routingProbe = network_engine
.sendFindNode(n->node,
sr->id,
-1,
std::bind(&Dht::searchNodeRoutingDone, this, _1, _2, ws),
std::bind(&Dht::searchNodeRoutingExpired, this, _1, _2, ws));
return n;
}
if (query and not query->select.empty()) {
/* The request contains a select. No need to paginate… */
/* if (logger_)
@@ -530,8 +566,8 @@ Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel)
sn->node->toString(),
a.value->id);
auto created = a.permanent ? time_point::max() : a.created;
acked = {network_engine
.sendAnnounceValue(sn->node, sr->id, a.value, created, sn->token, onDone, onExpired),
acked = {network_engine.sendAnnounceValue(
sn->node, sr->id, a.value, created, sn->token.value_or(Blob {}), onDone, onExpired),
next_refresh_time};
} else if (hasValue and a.permanent) {
if (logger_)
@@ -543,7 +579,7 @@ Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel)
sn->node,
sr->id,
a.value->id,
sn->token,
sn->token.value_or(Blob {}),
onDone,
[this,
ws,
@@ -563,7 +599,8 @@ Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel)
sr->id,
v,
created,
sn->token,
sn->token.value_or(
Blob {}),
onDone,
onExpired),
next_refresh_time};
@@ -622,8 +659,13 @@ Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel)
sr->id.toString(),
n.node->toString(),
a.value->id);
n.acked[a.value->id] = {network_engine.sendAnnounceValue(
n.node, sr->id, a.value, a.created, n.token, onDone, onExpired),
n.acked[a.value->id] = {network_engine.sendAnnounceValue(n.node,
sr->id,
a.value,
a.created,
n.token.value_or(Blob {}),
onDone,
onExpired),
now + getType(a.value->type).expiration};
}
}
@@ -663,8 +705,8 @@ Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
if (auto sr = ws.lock()) {
scheduler.edit(sr->nextSearchStep, scheduler.time());
if (auto sn = sr->getNode(node)) {
if (not answer.ntoken.empty())
sn->refreshToken(answer.ntoken);
if (answer.ntoken)
sn->refreshToken(*answer.ntoken);
sn->onValues(query, std::move(answer), types, scheduler);
}
}
@@ -698,7 +740,7 @@ Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
n.node,
sr->id,
*query,
n.token,
n.token.value_or(Blob {}),
r->second.socketId,
[this, ws, query](const net::Request& req, net::RequestAnswer&& answer) mutable {
/* on done */
@@ -1405,12 +1447,10 @@ Dht::storageChanged(const InfoHash& id, Storage& st, const Sp<Value>& v, bool ne
node_listeners.first->toString());
std::vector<Sp<Value>> vals;
vals.push_back(v);
Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
network_engine.tellListener(node_listeners.first,
l.first,
id,
0,
ntoken,
{},
{},
std::move(vals),
@@ -1507,7 +1547,6 @@ Dht::storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t socket_
socket_id,
id,
WANT4 | WANT6,
makeToken(node->getAddr(), false),
dht4.buckets.findClosestNodes(id, now, TARGET_NODES),
dht6.buckets.findClosestNodes(id, now, TARGET_NODES),
std::move(vals),
@@ -1562,8 +1601,7 @@ Dht::storageRemoved(const InfoHash& id, Storage& st, const std::vector<Sp<Value>
logger_->warn("[store {}] [node {}] Sending expired",
id.to_view(),
node_listeners.first->toString());
Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
network_engine.tellListenerExpired(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
network_engine.tellListenerExpired(node_listeners.first, l.first, id, ids, l.second.version);
}
}
}
@@ -1645,59 +1683,6 @@ Dht::connectivityChanged(sa_family_t af)
startBootstrap(); // will only happen if disconnected
}
void
Dht::rotateSecrets()
{
oldsecret = secret;
secret = std::uniform_int_distribution<uint64_t> {}(rd);
uniform_duration_distribution<> time_dist(std::chrono::minutes(15), std::chrono::minutes(45));
auto rotate_secrets_time = scheduler.time() + time_dist(rd);
scheduler.add(rotate_secrets_time, std::bind(&Dht::rotateSecrets, this));
}
Blob
Dht::makeToken(const SockAddr& addr, bool old) const
{
const void* ip;
size_t iplen;
in_port_t port;
auto family = addr.getFamily();
if (family == AF_INET) {
const auto& sin = addr.getIPv4();
ip = &sin.sin_addr;
iplen = 4;
port = sin.sin_port;
} else if (family == AF_INET6) {
const auto& sin6 = addr.getIPv6();
ip = &sin6.sin6_addr;
iplen = 16;
port = sin6.sin6_port;
} else {
return {};
}
const auto& c1 = old ? oldsecret : secret;
Blob data;
data.reserve(sizeof(secret) + sizeof(in_port_t) + iplen);
data.insert(data.end(), (uint8_t*) &c1, ((uint8_t*) &c1) + sizeof(c1));
data.insert(data.end(), (uint8_t*) ip, (uint8_t*) ip + iplen);
data.insert(data.end(), (uint8_t*) &port, ((uint8_t*) &port) + sizeof(in_port_t));
return crypto::hash(data, TOKEN_SIZE);
}
bool
Dht::tokenMatch(const Blob& token, const SockAddr& addr) const
{
if (not addr or token.size() != TOKEN_SIZE)
return false;
if (token == makeToken(addr, false))
return true;
if (token == makeToken(addr, true))
return true;
return false;
}
NodeStats
Dht::getNodesStats(sa_family_t af) const
{
@@ -1983,10 +1968,33 @@ fromDhtConfig(const Config& config)
{
net::NetworkConfig netConf;
netConf.network = config.network;
// global limiting should be triggered by at least 8 different IPs
auto peerLimit = [](ssize_t global, ssize_t peer) -> ssize_t {
return peer ? peer : (global < 0 ? -1 : global / 8);
};
netConf.max_req_per_sec = config.max_req_per_sec ? config.max_req_per_sec : MAX_REQUESTS_PER_SEC;
netConf.max_peer_req_per_sec = config.max_peer_req_per_sec ? config.max_peer_req_per_sec
: netConf.max_req_per_sec / 8;
netConf.max_peer_req_per_sec = peerLimit(netConf.max_req_per_sec, config.max_peer_req_per_sec);
// left at 0, the engine uses the limits above
netConf.max_unauth_req_per_sec = config.max_unauth_req_per_sec;
netConf.max_peer_unauth_req_per_sec = config.max_unauth_req_per_sec
? peerLimit(config.max_unauth_req_per_sec,
config.max_peer_unauth_req_per_sec)
: config.max_peer_unauth_req_per_sec;
netConf.max_crypto_per_sec = config.max_crypto_per_sec ? config.max_crypto_per_sec : MAX_CRYPTO_PER_SEC;
netConf.max_peer_crypto_per_sec = peerLimit(netConf.max_crypto_per_sec, config.max_peer_crypto_per_sec);
netConf.is_client = config.client_mode;
netConf.transport = config.transport;
auto& transport = netConf.transport;
if (transport.isPrivate() and not transport.enabled)
throw DhtException("Private networks require transport encryption");
if (transport.enabled and not transport.key)
transport.key = transport::KeyPair::generate();
if (transport.isPrivate()) {
if (transport.credential.key != transport.key.pub)
throw DhtException("Transport credential doesn't match the transport key");
if (not transport.credential.verify(transport.authorities))
throw DhtException("Invalid or expired transport credential");
}
return netConf;
}
@@ -2016,14 +2024,17 @@ Dht::Dht(std::unique_ptr<net::DatagramSocket>&& sock,
std::bind(&Dht::onPing, this, _1),
std::bind(&Dht::onFindNode, this, _1, _2, _3),
std::bind(&Dht::onGetValues, this, _1, _2, _3, _4),
std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5, _6),
std::bind(&Dht::onAnnounce, this, _1, _2, _3, _4, _5),
std::bind(&Dht::onRefresh, this, _1, _2, _3, _4))
std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5),
std::bind(&Dht::onAnnounce, this, _1, _2, _3, _4),
std::bind(&Dht::onRefresh, this, _1, _2, _3))
, persistPath(config.persist_path)
, is_bootstrap(config.is_bootstrap)
, maintain_storage(config.maintain_storage)
, public_stable(config.public_stable)
{
if (auto key = network_engine.getTransportKey())
myid = transport::nodeId(key->pub);
persistTransportKey = config.transport.enabled and not config.transport.key and not persistPath.empty();
scheduler.syncTime();
auto s = network_engine.getSocket();
if (not s or (not s->hasIPv4() and not s->hasIPv6()))
@@ -2042,10 +2053,6 @@ Dht::Dht(std::unique_ptr<net::DatagramSocket>&& sock,
uniform_duration_distribution<> time_dis {std::chrono::seconds(3), std::chrono::seconds(5)};
nextNodesConfirmation = scheduler.add(scheduler.time() + time_dis(rd), std::bind(&Dht::confirmNodes, this));
// Fill old secret
secret = std::uniform_int_distribution<uint64_t> {}(rd);
rotateSecrets();
if (not persistPath.empty())
loadState(persistPath);
@@ -2562,7 +2569,7 @@ Dht::onError(Sp<net::Request> req, net::DhtProtocolException e)
for (auto& n : sr->nodes) {
if (n->node != node)
continue;
n->token.clear();
n->token.reset();
n->last_get_reply = time_point::min();
for (auto& listen : n->listenStatus) {
if (listen.second.req and listen.second.req->pending())
@@ -2601,11 +2608,10 @@ Dht::onPing(Sp<Node>)
}
net::RequestAnswer
Dht::onFindNode(Sp<Node> node, const InfoHash& target, want_t want)
Dht::onFindNode(Sp<Node>, const InfoHash& target, want_t want)
{
const auto& now = scheduler.time();
net::RequestAnswer answer;
answer.ntoken = makeToken(node->getAddr(), false);
if (want & WANT4)
answer.nodes4 = dht4.buckets.findClosestNodes(target, now, TARGET_NODES);
if (want & WANT6)
@@ -2625,7 +2631,6 @@ Dht::onGetValues(Sp<Node> node, const InfoHash& hash, want_t, const Query& query
const auto& now = scheduler.time();
net::RequestAnswer answer {};
auto st = store.find(hash);
answer.ntoken = makeToken(node->getAddr(), false);
answer.nodes4 = dht4.buckets.findClosestNodes(hash, now, TARGET_NODES);
answer.nodes6 = dht6.buckets.findClosestNodes(hash, now, TARGET_NODES);
if (st != store.end() && not st->second.empty()) {
@@ -2649,7 +2654,7 @@ Dht::onGetValuesDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr
logger_->d(sr->id, "[search %s] [node %s] Got reply to 'get' with %u nodes",
sr->id.toString().c_str(), node->toString().c_str(), a.nodes4.size()+a.nodes6.size());*/
if (not a.ntoken.empty()) {
if (a.ntoken) {
if (not a.values.empty() or not a.fields.empty()) {
if (logger_)
logger_->debug("[search {}] [node {}] Found {} values",
@@ -2715,7 +2720,7 @@ Dht::onGetValuesDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr
}
net::RequestAnswer
Dht::onListen(Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version)
Dht::onListen(Sp<Node> node, const InfoHash& hash, size_t socket_id, const Query& query, int version)
{
if (not hash) {
if (logger_)
@@ -2723,17 +2728,9 @@ Dht::onListen(Sp<Node> node, const InfoHash& hash, const Blob& token, size_t soc
throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
net::DhtProtocolException::LISTEN_NO_INFOHASH};
}
if (not tokenMatch(token, node->getAddr())) {
if (logger_)
logger_->warn("[node {}] Incorrect token {} for 'listen'", node->toString(), hash.toString());
throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED,
net::DhtProtocolException::LISTEN_WRONG_TOKEN};
}
Query q = query;
storageAddListener(hash, node, socket_id, std::move(q), version);
net::RequestAnswer answer {};
answer.ntoken = makeToken(node->getAddr(), false);
return answer;
return {};
}
void
@@ -2745,9 +2742,9 @@ Dht::onListenDone(const Sp<Node>& node, net::RequestAnswer& answer, Sp<Search>&
if (not sr->done) {
const auto& now = scheduler.time();
if (not answer.ntoken.empty())
if (answer.ntoken)
if (auto sn = sr->getNode(node))
sn->refreshToken(answer.ntoken);
sn->refreshToken(*answer.ntoken);
searchSendGetValues(sr);
scheduler.edit(sr->nextSearchStep, now);
}
@@ -2756,7 +2753,6 @@ Dht::onListenDone(const Sp<Node>& node, net::RequestAnswer& answer, Sp<Search>&
net::RequestAnswer
Dht::onAnnounce(Sp<Node> n,
const InfoHash& hash,
const Blob& token,
const std::vector<Sp<Value>>& values,
const time_point& creation_date)
{
@@ -2767,12 +2763,6 @@ Dht::onAnnounce(Sp<Node> n,
throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
net::DhtProtocolException::PUT_NO_INFOHASH};
}
if (!tokenMatch(token, node.getAddr())) {
if (logger_)
logger_->warn("[node {}] Incorrect token {} for 'put'", node.toString(), hash.toString());
throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED,
net::DhtProtocolException::PUT_WRONG_TOKEN};
}
{
// We store a value only if we think we're part of the
// SEARCH_NODES nodes around the target id.
@@ -2833,15 +2823,10 @@ Dht::onAnnounce(Sp<Node> n,
}
net::RequestAnswer
Dht::onRefresh(Sp<Node> node, const InfoHash& hash, const Blob& token, const Value::Id& vid)
Dht::onRefresh(Sp<Node> node, const InfoHash& hash, const Value::Id& vid)
{
using namespace net;
if (not tokenMatch(token, node->getAddr())) {
if (logger_)
logger_->warn("[node {}] Incorrect token {} for 'put'", node->toString(), hash.toString());
throw DhtProtocolException {DhtProtocolException::UNAUTHORIZED, DhtProtocolException::PUT_WRONG_TOKEN};
}
if (storageRefresh(hash, vid)) {
if (logger_)
logger_->debug("[store {}] [node {}] Refreshed value {:016x}", hash.toString(), node->toString(), vid);
@@ -2872,9 +2857,7 @@ Dht::storageRefresh(const InfoHash& id, Value::Id vid)
logger_->warn("[store {}] [node {}] Sending refresh",
id.toString(),
node_listeners.first->toString());
Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
network_engine
.tellListenerRefreshed(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
network_engine.tellListenerRefreshed(node_listeners.first, l.first, id, ids, l.second.version);
}
}
}
@@ -2907,8 +2890,10 @@ struct DhtState
InfoHash id;
std::vector<NodeExport> nodes;
std::vector<ValuesExport> values;
/* secret transport key, if generated by the node */
Blob transport_key;
MSGPACK_DEFINE_MAP(v, id, nodes, values)
MSGPACK_DEFINE_MAP(v, id, nodes, values, transport_key)
};
void
@@ -2918,8 +2903,26 @@ Dht::saveState(const std::string& path) const
state.id = myid;
state.nodes = exportNodes();
state.values = exportValues();
std::ofstream file(path);
msgpack::pack(file, state);
auto key = persistTransportKey ? network_engine.getTransportKey() : nullptr;
if (key)
state.transport_key.assign(key->secret.begin(), key->secret.end());
auto temporary = path + "." + InfoHash::getRandom().toString() + ".tmp";
try {
std::ofstream file;
file.exceptions(std::ios::failbit | std::ios::badbit);
file.open(temporary, std::ios::binary | std::ios::trunc);
if (key)
std::filesystem::permissions(temporary,
std::filesystem::perms::owner_read | std::filesystem::perms::owner_write);
msgpack::pack(file, state);
file.close();
std::filesystem::rename(temporary, path);
} catch (const std::exception& e) {
std::error_code ec;
std::filesystem::remove(temporary, ec);
if (logger_)
logger_->warn("Error saving state to {}: {}", path, e.what());
}
}
void
@@ -2948,8 +2951,15 @@ Dht::loadState(const std::string& path)
auto state = oh.get().as<DhtState>();
if (logger_)
logger_->debug("Importing {} nodes", state.nodes.size());
if (state.id)
if (persistTransportKey and state.transport_key.size() == transport::KEY_SIZE) {
transport::Key secret;
std::copy(state.transport_key.begin(), state.transport_key.end(), secret.begin());
auto key = transport::KeyPair::fromSecret(secret);
network_engine.setTransportKey(key);
myid = transport::nodeId(key.pub);
} else if (state.id and not network_engine.getTransportKey()) {
myid = state.id;
}
std::vector<Sp<Node>> tmpNodes;
tmpNodes.reserve(state.nodes.size());
for (const auto& node : state.nodes)
+881 -264
View File
File diff suppressed because it is too large. Load diff
+10 -3
View File
@@ -4,6 +4,7 @@
#include "node.h"
#include "request.h"
#include "rng.h"
#include "transport.h"
#include <sstream>
@@ -147,14 +148,20 @@ Node::closeSocket(Tid id)
std::string
Node::toString() const
{
return fmt::format("{} {:<21}", id, addr.toString());
auto state = 'P';
auto authorization = '-';
if (transport_) {
state = transport_->ephemeral && transport_->validatedAddress == addr ? 'A' : 'K';
if (transport_->authorized)
authorization = '+';
}
return fmt::format("{} {:<21} [{}{}]", id, addr.toString(), state, authorization);
}
std::ostream&
operator<<(std::ostream& s, const Node& h)
{
s << h.id << " " << h.addr.toString();
return s;
return s << h.toString();
}
void
+6
View File
@@ -9,6 +9,12 @@ constexpr size_t CLEANUP_MAX_NODES {4 * 1024};
constexpr size_t CLEANUP_FREQ {4 * 1024};
NodeCache::~NodeCache()
{
clear();
}
void
NodeCache::clear()
{
cache_4.setExpired();
cache_6.setExpired();
+22
View File
@@ -4,6 +4,7 @@
#include "infohash.h"
#include "sockaddr.h"
#include "transport.h"
#include "net.h"
#include <algorithm>
@@ -26,6 +27,7 @@ static constexpr auto KEY_NETID = "n"sv;
static constexpr auto KEY_ISCLIENT = "s"sv;
static constexpr auto KEY_Q = "q"sv;
static constexpr auto KEY_A = "a"sv;
static constexpr auto KEY_TRANSPORT = "k"sv;
static constexpr auto KEY_REQ_SID = "sid"sv;
static constexpr auto KEY_REQ_ID = "id"sv;
@@ -36,6 +38,8 @@ static constexpr auto KEY_REQ_TOKEN = "token"sv;
static constexpr auto KEY_REQ_VALUE_ID = "vid"sv;
static constexpr auto KEY_REQ_NODES4 = "n4"sv;
static constexpr auto KEY_REQ_NODES6 = "n6"sv;
static constexpr auto KEY_REQ_KEYS4 = "k4"sv;
static constexpr auto KEY_REQ_KEYS6 = "k6"sv;
static constexpr auto KEY_REQ_CREATION = "c"sv;
static constexpr auto KEY_REQ_ADDRESS = "sa"sv;
static constexpr auto KEY_REQ_VALUES = "values"sv;
@@ -93,6 +97,8 @@ struct ParsedMessage
time_point created {time_point::max()};
/* IPv4 nodes in response to a 'find' request */
Blob nodes4_raw, nodes6_raw;
/* transport keys of the nodes, or zeros if unknown */
Blob nodes4_keys, nodes6_keys;
std::vector<Sp<Node>> nodes4, nodes6;
/* values to store or retreive request */
std::vector<Sp<Value>> values;
@@ -129,6 +135,13 @@ struct ParsedMessage
std::string ua;
int version {0};
SockAddr addr;
/* transport key advertised by the sender in plaintext */
std::optional<transport::Key> transport_key;
/* transport state of the sender, if authenticated */
Sp<transport::Peer> peer;
bool encrypted {false};
/* received in a Session envelope: the sender receives at its address */
bool session {false};
void msgpack_unpack(const msgpack::object& o);
bool append(const ParsedMessage& block);
@@ -229,6 +242,11 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
parsed.q = o.val.as<std::string_view>();
else if (key == KEY_A)
parsed.a = &o.val;
else if (key == KEY_TRANSPORT and o.val.type == msgpack::type::BIN
and o.val.via.bin.size == transport::KEY_SIZE) {
transport_key.emplace();
std::copy_n((const uint8_t*) o.val.via.bin.ptr, transport::KEY_SIZE, transport_key->begin());
}
}
if (parsed.e)
@@ -321,6 +339,10 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
nodes4_raw = unpackBlob(o.val);
else if (key == KEY_REQ_NODES6)
nodes6_raw = unpackBlob(o.val);
else if (key == KEY_REQ_KEYS4)
nodes4_keys = unpackBlob(o.val);
else if (key == KEY_REQ_KEYS6)
nodes6_keys = unpackBlob(o.val);
else if (key == KEY_REQ_ADDRESS)
parsedReq.sa = &o.val;
else if (key == KEY_REQ_CREATION)
+11
View File
@@ -3,6 +3,7 @@
#pragma once
#include "net.h"
#include "scheduler.h"
#include "value.h"
namespace dht {
@@ -133,6 +134,9 @@ private:
void clear()
{
if (retryJob)
retryJob->cancel();
retryJob.reset();
on_done = {};
on_error = {};
on_expired = {};
@@ -155,6 +159,13 @@ private:
Blob msg {}; /* the serialized message. */
std::vector<Blob> parts;
/* the last attempt was an Initial envelope, answered without caching our session */
bool initial {false};
/* waiting for session establishment or recovery, without an application retry timer */
bool withheld {false};
bool sessionProbe {false};
bool timeoutNotified {false};
Sp<Scheduler::Job> retryJob;
};
} /* namespace net */
+18 -11
View File
@@ -104,10 +104,12 @@ struct Dht::SearchNode
std::map<Sp<Query>, std::vector<Sp<Query>>> pagination_queries {};
SyncStatus getStatus {}; /* get/sync status */
Sp<net::Request> routingProbe;
NodeListenerStatus listenStatus {}; /* listen status */
AnnounceStatusMap acked {}; /* announcement status for a given value id */
Blob token {}; /* last token the node sent to us after a get request */
/* write token the node sent us after a get request: empty if it needs none (encrypted) */
std::optional<Blob> token {};
time_point last_get_reply {time_point::min()}; /* last time received valid token */
Sp<Scheduler::Job> syncJob {};
bool candidate {false}; /* A search node is candidate if the search is/was synced and this
@@ -138,12 +140,12 @@ struct Dht::SearchNode
*/
bool isSynced(const time_point& now) const
{
return not node->isExpired() and not token.empty() and last_get_reply >= now - Node::NODE_EXPIRE_TIME;
return not node->isExpired() and token and last_get_reply >= now - Node::NODE_EXPIRE_TIME;
}
time_point getSyncTime(const time_point& now) const
{
if (node->isExpired() or token.empty())
if (node->isExpired() or not token)
return now;
return last_get_reply + Node::NODE_EXPIRE_TIME;
}
@@ -154,7 +156,7 @@ struct Dht::SearchNode
*/
void refreshToken(const Blob& tok)
{
if (not tok.empty() and tok.size() <= 64)
if (tok.size() <= 64)
token = tok;
}
@@ -186,7 +188,7 @@ struct Dht::SearchNode
*/
bool canGet(time_point now, time_point update, const Get& get) const
{
if (node->isExpired())
if (node->isExpired() or (routingProbe and routingProbe->pending()))
return false;
bool is_pending {false}, completed_sq_status {false}, pending_sq_status {false};
@@ -241,6 +243,8 @@ struct Dht::SearchNode
*/
bool isDone(const Get& get) const
{
if (routingProbe and routingProbe->pending())
return false;
if (hasStartedPagination(get.query)) {
const auto& pqs = pagination_queries.find(get.query);
auto paginationPending = std::find_if(pqs->second.cbegin(),
@@ -260,6 +264,9 @@ struct Dht::SearchNode
void cancelGet()
{
if (routingProbe)
node->cancelRequest(routingProbe);
routingProbe.reset();
for (const auto& status : getStatus) {
if (status.second->pending()) {
node->cancelRequest(status.second);
@@ -514,7 +521,7 @@ struct Dht::Search
/**
* @returns true if the node was not present and added to the search
*/
bool insertNode(const Sp<Node>& n, time_point now, const Blob& token = {});
bool insertNode(const Sp<Node>& n, time_point now, const std::optional<Blob>& token = {});
SearchNode* getNode(const Sp<Node>& n)
{
@@ -529,7 +536,7 @@ struct Dht::Search
{
unsigned count = 0;
for (const auto& n : nodes)
if (not n->isBad() and n->pendingGet())
if (not n->isBad() and (n->pendingGet() or (n->routingProbe and n->routingProbe->pending())))
count++;
return count;
}
@@ -585,7 +592,7 @@ struct Dht::Search
void setDone()
{
for (auto& n : nodes) {
n->getStatus.clear();
n->cancelGet();
n->listenStatus.clear();
n->acked.clear();
}
@@ -869,7 +876,7 @@ struct Dht::Search
target. We just got a new candidate, insert it at the right spot or
discard it. */
bool
Dht::Search::insertNode(const Sp<Node>& snode, time_point now, const Blob& token)
Dht::Search::insertNode(const Sp<Node>& snode, time_point now, const std::optional<Blob>& token)
{
auto& node = *snode;
const auto& nid = node.id;
@@ -948,10 +955,10 @@ Dht::Search::insertNode(const Sp<Node>& snode, time_point now, const Blob& token
}
if (n == nodes.end() or not *n)
return false;
if (not token.empty()) {
if (token) {
(*n)->candidate = false;
(*n)->last_get_reply = now;
if (token.size() <= 64)
if (token->size() <= 64)
(*n)->token = token;
expired = false;
}
+454
View File
@@ -0,0 +1,454 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "transport.h"
#include <gnutls/crypto.h>
#include <nettle/chacha-poly1305.h>
#include <nettle/curve25519.h>
#include <nettle/eddsa.h>
#include <nettle/hmac.h>
#include <nettle/memops.h>
#include <nettle/sha2.h>
#include <nettle/version.h>
#include <algorithm>
#include <cstring>
#include <stdexcept>
#include <string_view>
namespace dht {
namespace transport {
using namespace std::literals;
namespace {
constexpr auto NODE_ID_CONTEXT = "opendht-node-id-v1"sv;
constexpr auto KDF_SALT = "opendht-transport-v2"sv;
constexpr auto CREDENTIAL_CONTEXT = "opendht-credential-v2"sv;
constexpr auto ADDRESS_KEY_CONTEXT = "opendht-address-key-v1"sv;
constexpr auto ADDRESS_TOKEN_CONTEXT = "opendht-address-token-v1"sv;
constexpr uint8_t CREDENTIAL_VERSION {2};
void
randomBytes(uint8_t* data, size_t size, gnutls_rnd_level_t level)
{
if (gnutls_rnd(level, data, size) != 0)
throw std::runtime_error("Unable to generate random data");
}
Key
hmacSha256(const uint8_t* key, size_t keyLen, const uint8_t* data, size_t dataLen)
{
struct hmac_sha256_ctx ctx;
hmac_sha256_set_key(&ctx, keyLen, key);
hmac_sha256_update(&ctx, dataLen, data);
Key out;
#if NETTLE_VERSION_MAJOR >= 4
hmac_sha256_digest(&ctx, out.data());
#else
hmac_sha256_digest(&ctx, out.size(), out.data());
#endif
gnutls_memset(&ctx, 0, sizeof(ctx));
return out;
}
/* HMAC-SHA256(secret, context || address) */
Key
addressPrf(const Key& secret, std::string_view context, const uint8_t* address, size_t size)
{
struct hmac_sha256_ctx ctx;
hmac_sha256_set_key(&ctx, secret.size(), secret.data());
hmac_sha256_update(&ctx, context.size(), (const uint8_t*) context.data());
hmac_sha256_update(&ctx, size, address);
Key out;
#if NETTLE_VERSION_MAJOR >= 4
hmac_sha256_digest(&ctx, out.data());
#else
hmac_sha256_digest(&ctx, out.size(), out.data());
#endif
gnutls_memset(&ctx, 0, sizeof(ctx));
return out;
}
void
chachaDigest(struct chacha_poly1305_ctx* ctx, uint8_t* tag)
{
#if NETTLE_VERSION_MAJOR >= 4
chacha_poly1305_digest(ctx, tag);
#else
chacha_poly1305_digest(ctx, TAG_SIZE, tag);
#endif
}
constexpr size_t SIGNED_SIZE {CREDENTIAL_CONTEXT.size() + 1 + KEY_SIZE};
std::array<uint8_t, SIGNED_SIZE>
signedData(const Key& key)
{
std::array<uint8_t, SIGNED_SIZE> data;
auto position = std::copy(CREDENTIAL_CONTEXT.begin(), CREDENTIAL_CONTEXT.end(), data.begin());
*position++ = CREDENTIAL_VERSION;
std::copy(key.begin(), key.end(), position);
return data;
}
std::optional<Key>
dh(const Key& secret, const Key& pub)
{
Key out;
curve25519_mul(out.data(), secret.data(), pub.data());
if (out == Key {})
return std::nullopt;
return out;
}
/* Key derivation input, wiped on destruction */
struct KdfBuffer
{
std::array<uint8_t, 4 + 1 + 4 * KEY_SIZE + 1> data;
size_t size {0};
KdfBuffer& add(const uint8_t* d, size_t len)
{
std::memcpy(data.data() + size, d, len);
size += len;
return *this;
}
KdfBuffer& add(const Key& k) { return add(k.data(), k.size()); }
KdfBuffer& add(uint8_t b) { return add(&b, 1); }
KdfBuffer& add(NetId network)
{
for (int i = 0; i < 4; ++i)
add(static_cast<uint8_t>((network >> (24 - 8 * i)) & 0xFF));
return *this;
}
~KdfBuffer() { gnutls_memset(data.data(), 0, data.size()); }
};
/* HKDF-SHA256 with a single output block */
Key
hkdf(const KdfBuffer& ikm, KdfBuffer info)
{
auto prk = hmacSha256((const uint8_t*) KDF_SALT.data(), KDF_SALT.size(), ikm.data.data(), ikm.size);
info.add(uint8_t {1});
auto out = hmacSha256(prk.data(), prk.size(), info.data.data(), info.size);
wipe(prk);
return out;
}
KdfBuffer
kdfInfo(NetId network, EnvelopeType type, const Key& sender, const Key& senderEphemeral, const Key& receiver)
{
KdfBuffer info;
info.add(network).add(static_cast<uint8_t>(type)).add(sender).add(senderEphemeral).add(receiver);
return info;
}
void
wipe(std::optional<Key>& k)
{
if (k)
transport::wipe(*k);
}
} // namespace
KeyPair
KeyPair::generate()
{
Key secret = randomSecret();
auto kp = fromSecret(secret);
wipe(secret);
return kp;
}
KeyPair
KeyPair::fromSecret(const Key& secret)
{
KeyPair kp;
kp.secret = secret;
curve25519_mul_g(kp.pub.data(), secret.data());
return kp;
}
InfoHash
nodeId(const Key& pub)
{
struct sha256_ctx ctx;
sha256_init(&ctx);
sha256_update(&ctx, NODE_ID_CONTEXT.size(), (const uint8_t*) NODE_ID_CONTEXT.data());
sha256_update(&ctx, pub.size(), pub.data());
std::array<uint8_t, SHA256_DIGEST_SIZE> digest;
#if NETTLE_VERSION_MAJOR >= 4
sha256_digest(&ctx, digest.data());
#else
sha256_digest(&ctx, digest.size(), digest.data());
#endif
return InfoHash(digest.data(), HASH_LEN);
}
Key
randomSecret()
{
Key secret;
randomBytes(secret.data(), secret.size(), GNUTLS_RND_KEY);
return secret;
}
KeyPair
addressKey(const Key& epochSecret, const uint8_t* address, size_t size)
{
auto secret = addressPrf(epochSecret, ADDRESS_KEY_CONTEXT, address, size);
auto kp = KeyPair::fromSecret(secret);
wipe(secret);
return kp;
}
Key
addressToken(const Key& epochSecret, const uint8_t* address, size_t size)
{
return addressPrf(epochSecret, ADDRESS_TOKEN_CONTEXT, address, size);
}
Blob
Credential::pack() const
{
Blob b(PACKED_SIZE);
b[0] = CREDENTIAL_VERSION;
std::copy(key.begin(), key.end(), b.begin() + 1);
std::copy(signature.begin(), signature.end(), b.begin() + 1 + KEY_SIZE);
return b;
}
Credential
Credential::unpack(const uint8_t* data, size_t size)
{
if (size != PACKED_SIZE or data[0] != CREDENTIAL_VERSION)
throw std::invalid_argument("Invalid transport credential");
Credential c;
std::copy_n(data + 1, KEY_SIZE, c.key.begin());
std::copy_n(data + 1 + KEY_SIZE, SIGNATURE_SIZE, c.signature.begin());
return c;
}
bool
Credential::verify(const std::vector<Key>& authorities) const
{
auto data = signedData(key);
return std::any_of(authorities.begin(), authorities.end(), [&](const Key& authority) {
return ed25519_sha512_verify(authority.data(), data.size(), data.data(), signature.data()) == 1;
});
}
Authority
Authority::generate()
{
Key secret;
randomBytes(secret.data(), secret.size(), GNUTLS_RND_KEY);
auto a = fromSecret(secret);
wipe(secret);
return a;
}
Authority
Authority::fromSecret(const Key& secret)
{
Authority a;
a.secret = secret;
ed25519_sha512_public_key(a.pub.data(), secret.data());
return a;
}
Credential
Authority::issue(const Key& transportKey) const
{
Credential c;
c.key = transportKey;
auto data = signedData(c.key);
ed25519_sha512_sign(pub.data(), secret.data(), data.size(), data.data(), c.signature.data());
return c;
}
std::optional<SessionKeys>
sessionKeys(const KeyPair& own, const KeyPair& ephemeral, const Key& peer, const Key& peerEphemeral, NetId network)
{
auto ee = dh(ephemeral.secret, peerEphemeral);
auto es = dh(ephemeral.secret, peer);
auto se = dh(own.secret, peerEphemeral);
auto ss = dh(own.secret, peer);
std::optional<SessionKeys> keys;
if (ee and es and se and ss) {
// The DH(sender, receiver) terms in order ee, es, se, ss, seen from each side
KdfBuffer tx, rx;
tx.add(*ee).add(*es).add(*se).add(*ss);
rx.add(*ee).add(*se).add(*es).add(*ss);
keys = SessionKeys {
hkdf(tx, kdfInfo(network, EnvelopeType::Session, own.pub, ephemeral.pub, peer).add(peerEphemeral)),
hkdf(rx, kdfInfo(network, EnvelopeType::Session, peer, peerEphemeral, own.pub).add(ephemeral.pub))};
}
for (auto* k : {&ee, &es, &se, &ss})
wipe(*k);
return keys;
}
std::optional<Key>
initialSendKey(const KeyPair& own, const KeyPair& ephemeral, const Key& peer, NetId network)
{
auto es = dh(ephemeral.secret, peer);
auto ss = dh(own.secret, peer);
std::optional<Key> key;
if (es and ss) {
KdfBuffer ikm;
ikm.add(*es).add(*ss);
key = hkdf(ikm, kdfInfo(network, EnvelopeType::Initial, own.pub, ephemeral.pub, peer));
}
wipe(es);
wipe(ss);
return key;
}
std::optional<Key>
initialReceiveKey(const KeyPair& own, const Key& peer, const Key& peerEphemeral, NetId network)
{
auto es = dh(own.secret, peerEphemeral);
auto ss = dh(own.secret, peer);
std::optional<Key> key;
if (es and ss) {
KdfBuffer ikm;
ikm.add(*es).add(*ss);
key = hkdf(ikm, kdfInfo(network, EnvelopeType::Initial, peer, peerEphemeral, own.pub));
}
wipe(es);
wipe(ss);
return key;
}
std::optional<Header>
parseHeader(const uint8_t* data, size_t size)
{
if (not isEnvelope(data, size))
return std::nullopt;
const uint8_t type = data[1] & ~CREDENTIAL_FLAG;
Header header {};
header.credential = data[1] & CREDENTIAL_FLAG;
if (type == static_cast<uint8_t>(EnvelopeType::Data)) {
if (header.credential)
return std::nullopt;
header.type = EnvelopeType::Data;
return header;
}
if (type < static_cast<uint8_t>(EnvelopeType::Initial) or type > static_cast<uint8_t>(EnvelopeType::Hello)
or size < HEADER_SIZE)
return std::nullopt;
header.type = static_cast<EnvelopeType>(type);
std::copy_n(data + 2, KEY_SIZE, header.sender.begin());
std::copy_n(data + 2 + KEY_SIZE, KEY_SIZE, header.ephemeral.begin());
return header;
}
namespace {
/* Appends the nonce and the encryption of `credential || data` to the authenticated header in `out` */
Blob
sealWith(const Key& key, Blob out, const Blob& credential, const uint8_t* data, size_t size)
{
const size_t headerSize = out.size();
const size_t len = credential.size() + size;
out.resize(headerSize + NONCE_SIZE + len + TAG_SIZE);
auto nonce = out.data() + headerSize;
randomBytes(nonce, NONCE_SIZE, GNUTLS_RND_NONCE);
auto payload = nonce + NONCE_SIZE;
std::copy(credential.begin(), credential.end(), payload);
if (size)
std::memcpy(payload + credential.size(), data, size);
struct chacha_poly1305_ctx ctx;
chacha_poly1305_set_key(&ctx, key.data());
chacha_poly1305_set_nonce(&ctx, nonce);
chacha_poly1305_update(&ctx, headerSize, out.data());
chacha_poly1305_encrypt(&ctx, len, payload, payload);
chachaDigest(&ctx, payload + len);
gnutls_memset(&ctx, 0, sizeof(ctx));
return out;
}
} // namespace
Blob
seal(const Key& key,
EnvelopeType type,
const Key& sender,
const Key& ephemeral,
const uint8_t* data,
size_t size,
const Blob& credential)
{
Blob header;
header.reserve(MAX_ENVELOPE_OVERHEAD + size);
header.push_back(ENVELOPE_MARKER);
header.push_back(static_cast<uint8_t>(type) | (credential.empty() ? 0 : CREDENTIAL_FLAG));
header.insert(header.end(), sender.begin(), sender.end());
header.insert(header.end(), ephemeral.begin(), ephemeral.end());
return sealWith(key, std::move(header), credential, data, size);
}
Blob
sealData(const Key& key, const uint8_t* data, size_t size)
{
Blob header;
header.reserve(DATA_ENVELOPE_OVERHEAD + size);
header.push_back(ENVELOPE_MARKER);
header.push_back(static_cast<uint8_t>(EnvelopeType::Data));
return sealWith(key, std::move(header), {}, data, size);
}
std::optional<Blob>
open(const Key& key, const uint8_t* data, size_t size)
{
auto header = parseHeader(data, size);
if (not header or header->type == EnvelopeType::Hello)
return std::nullopt;
const size_t headerSize = header->type == EnvelopeType::Data ? DATA_HEADER_SIZE : HEADER_SIZE;
if (size < headerSize + NONCE_SIZE + TAG_SIZE)
return std::nullopt;
const size_t len = size - headerSize - NONCE_SIZE - TAG_SIZE;
const auto nonce = data + headerSize;
struct chacha_poly1305_ctx ctx;
chacha_poly1305_set_key(&ctx, key.data());
chacha_poly1305_set_nonce(&ctx, nonce);
chacha_poly1305_update(&ctx, headerSize, data);
Blob out(len);
chacha_poly1305_decrypt(&ctx, len, out.data(), nonce + NONCE_SIZE);
std::array<uint8_t, TAG_SIZE> tag;
chachaDigest(&ctx, tag.data());
gnutls_memset(&ctx, 0, sizeof(ctx));
if (not memeql_sec(tag.data(), data + size - TAG_SIZE, TAG_SIZE))
return std::nullopt;
return out;
}
Blob
hello(const Key& sender, const Key& ephemeral, const Blob& credential, const uint8_t* data, size_t size)
{
Blob out;
out.reserve(HEADER_SIZE + credential.size() + size);
out.push_back(ENVELOPE_MARKER);
out.push_back(static_cast<uint8_t>(EnvelopeType::Hello));
out.insert(out.end(), sender.begin(), sender.end());
out.insert(out.end(), ephemeral.begin(), ephemeral.end());
out.insert(out.end(), credential.begin(), credential.end());
out.insert(out.end(), data, data + size);
return out;
}
void
wipe(Key& key)
{
gnutls_memset(key.data(), 0, key.size());
}
} // namespace transport
} // namespace dht
+279
View File
@@ -10,6 +10,7 @@
#include <mutex>
#include <condition_variable>
#include <fstream>
#include <filesystem>
#include <cstdio>
using namespace std::chrono_literals;
using namespace std::literals;
@@ -17,6 +18,7 @@ using namespace std::literals;
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerTester);
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerPushTester);
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerTransportTester);
namespace {
#if defined(OPENDHT_PROXY_CLIENT) && defined(OPENDHT_PUSH_NOTIFICATIONS)
@@ -1579,4 +1581,281 @@ DhtRunnerTester::testListenMultiValuePartialExpire()
node1.cancelListen(key, ftoken.get());
}
namespace {
/* UDP socket recording sent packets */
class RecordingSocket : public dht::net::DatagramSocket
{
public:
RecordingSocket()
: inner_(std::make_unique<dht::net::UdpSocket>(0))
{
inner_->setOnReceive([this](dht::net::PacketList&& packets) {
onReceived(std::move(packets));
return dht::net::PacketList {};
});
}
~RecordingSocket() { inner_.reset(); }
int sendTo(const dht::SockAddr& dest, const uint8_t* data, size_t size, bool replied) override
{
{
std::lock_guard lk(sentLock_);
sent_.emplace_back(data, data + size);
}
return inner_->sendTo(dest, data, size, replied);
}
bool hasIPv4() const override { return inner_->hasIPv4(); }
bool hasIPv6() const override { return inner_->hasIPv6(); }
const dht::SockAddr& getBoundRef(sa_family_t family = AF_UNSPEC) const override
{
return inner_->getBoundRef(family);
}
void stop() override { inner_->stop(); }
std::vector<dht::Blob> sent() const
{
std::lock_guard lk(sentLock_);
return sent_;
}
private:
std::unique_ptr<dht::net::UdpSocket> inner_;
mutable std::mutex sentLock_;
std::vector<dht::Blob> sent_;
};
dht::DhtRunner::Config
transportTestConfig(dht::TransportConfig transport = {})
{
dht::DhtRunner::Config config;
config.dht_config.node_config.max_peer_req_per_sec = -1;
config.dht_config.node_config.max_req_per_sec = -1;
config.dht_config.node_config.transport = std::move(transport);
return config;
}
RecordingSocket*
runRecorded(dht::DhtRunner& node, const dht::DhtRunner::Config& config)
{
auto socket = std::make_unique<RecordingSocket>();
auto ptr = socket.get();
dht::DhtRunner::Context context;
context.sock = std::move(socket);
node.run(config, std::move(context));
return ptr;
}
dht::SockAddr
localBound(const dht::DhtRunner& node)
{
auto bound = node.getBound();
if (bound.isUnspecified())
bound.setLoopback();
return bound;
}
bool
isEnvelope(const dht::Blob& packet)
{
return dht::transport::isEnvelope(packet.data(), packet.size());
}
void
putGet(dht::DhtRunner& from, dht::DhtRunner& to, const std::string& keyName)
{
auto key = dht::InfoHash::get(keyName);
dht::Value val {keyName};
auto data = val.data;
std::promise<bool> p;
auto future = p.get_future();
from.put(key, std::move(val), [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(std::move(future)));
auto vals = getFutureValue(to.get(key));
CPPUNIT_ASSERT(not vals.empty());
CPPUNIT_ASSERT(vals.front()->data == data);
}
dht::TransportConfig
privateTransport(const dht::transport::Authority& authority)
{
dht::TransportConfig transport;
transport.key = dht::transport::KeyPair::generate();
transport.authorities = {authority.pub};
transport.credential = authority.issue(transport.key.pub);
return transport;
}
} // namespace
void
DhtRunnerTransportTester::testEncryptedByDefault()
{
dht::DhtRunner a, b;
auto socketA = runRecorded(a, transportTestConfig());
auto socketB = runRecorded(b, transportTestConfig());
b.bootstrap(localBound(a));
putGet(b, a, "encrypted");
// a learns b's key from its first request: everything a sends is encrypted
auto sentA = socketA->sent();
CPPUNIT_ASSERT(not sentA.empty());
CPPUNIT_ASSERT(std::all_of(sentA.begin(), sentA.end(), isEnvelope));
// b sends plaintext only until it learns a's key
auto sentB = socketB->sent();
auto firstEnvelope = std::find_if(sentB.begin(), sentB.end(), isEnvelope);
CPPUNIT_ASSERT(firstEnvelope != sentB.end());
CPPUNIT_ASSERT(std::all_of(firstEnvelope, sentB.end(), isEnvelope));
}
void
DhtRunnerTransportTester::testLegacyInterop()
{
dht::TransportConfig legacyTransport;
legacyTransport.enabled = false;
dht::DhtRunner node, legacy;
auto socket = runRecorded(node, transportTestConfig());
auto legacyConfig = transportTestConfig(legacyTransport);
legacy.run(0, legacyConfig);
legacy.bootstrap(localBound(node));
putGet(legacy, node, "legacy to new");
putGet(node, legacy, "new to legacy");
auto sent = socket->sent();
CPPUNIT_ASSERT(not sent.empty());
CPPUNIT_ASSERT(std::none_of(sent.begin(), sent.end(), isEnvelope));
}
void
DhtRunnerTransportTester::testNodeIdFromTransportKey()
{
std::mt19937_64 rd(42);
auto configuredId = dht::InfoHash::getRandom(rd);
dht::TransportConfig transport;
transport.key = dht::transport::KeyPair::generate();
auto config = transportTestConfig(transport);
config.dht_config.node_config.node_id = configuredId;
dht::DhtRunner node;
node.run(0, config);
CPPUNIT_ASSERT_EQUAL(dht::transport::nodeId(transport.key.pub), node.getNodeId());
transport.enabled = false;
config = transportTestConfig(transport);
config.dht_config.node_config.node_id = configuredId;
dht::DhtRunner legacy;
legacy.run(0, config);
CPPUNIT_ASSERT_EQUAL(configuredId, legacy.getNodeId());
}
void
DhtRunnerTransportTester::testPersistedTransportKey()
{
const auto path = std::filesystem::temp_directory_path()
/ ("opendht_test_transport_state_" + dht::InfoHash::getRandom().toString());
auto previous = path;
previous += ".previous";
std::filesystem::remove(path);
std::filesystem::remove(previous);
auto runNode = [&](const dht::TransportConfig& transport) {
auto config = transportTestConfig(transport);
config.dht_config.node_config.persist_path = path.string();
dht::DhtRunner node;
node.run(0, config);
auto id = node.getNodeId();
std::promise<void> stopped;
node.shutdown([&] { stopped.set_value(); });
stopped.get_future().wait();
node.join();
return id;
};
// A generated key is persisted, keeping the node ID
auto generatedId = runNode({});
CPPUNIT_ASSERT(std::filesystem::exists(path));
CPPUNIT_ASSERT_EQUAL(generatedId, runNode({}));
#ifndef _WIN32
auto perms = std::filesystem::status(path).permissions();
CPPUNIT_ASSERT((perms & (std::filesystem::perms::group_all | std::filesystem::perms::others_all))
== std::filesystem::perms::none);
#endif
// A configured key takes precedence and isn't persisted
dht::TransportConfig configured;
configured.key = dht::transport::KeyPair::generate();
std::filesystem::create_hard_link(path, previous);
auto previousSize = std::filesystem::file_size(previous);
CPPUNIT_ASSERT_EQUAL(dht::transport::nodeId(configured.key.pub), runNode(configured));
CPPUNIT_ASSERT(not std::filesystem::equivalent(path, previous));
CPPUNIT_ASSERT_EQUAL(previousSize, std::filesystem::file_size(previous));
auto newId = runNode({});
CPPUNIT_ASSERT(newId != generatedId);
CPPUNIT_ASSERT(newId != dht::transport::nodeId(configured.key.pub));
CPPUNIT_ASSERT_EQUAL(newId, runNode({}));
#ifndef _WIN32
std::filesystem::remove(path);
std::filesystem::create_symlink(previous, path);
CPPUNIT_ASSERT_EQUAL(generatedId, runNode({}));
CPPUNIT_ASSERT(not std::filesystem::is_symlink(path));
CPPUNIT_ASSERT_EQUAL(previousSize, std::filesystem::file_size(previous));
#endif
std::filesystem::remove(path);
std::filesystem::remove(previous);
}
void
DhtRunnerTransportTester::testPrivateNetwork()
{
auto authority = dht::transport::Authority::generate();
auto nextAuthority = dht::transport::Authority::generate();
dht::DhtRunner member1, member2, outsider;
auto transport1 = privateTransport(authority);
auto transport2 = privateTransport(nextAuthority);
transport1.authorities = transport2.authorities = {authority.pub, nextAuthority.pub};
auto socket1 = runRecorded(member1, transportTestConfig(transport1));
auto config2 = transportTestConfig(transport2);
member2.run(0, config2);
auto outsiderConfig = transportTestConfig(privateTransport(dht::transport::Authority::generate()));
outsider.run(0, outsiderConfig);
member2.bootstrap(localBound(member1));
putGet(member2, member1, "private");
outsider.bootstrap(localBound(member1));
std::this_thread::sleep_for(2s);
CPPUNIT_ASSERT_EQUAL(0u, outsider.getNodesStats(AF_INET).getKnownNodes());
CPPUNIT_ASSERT_EQUAL(1u, member1.getNodesStats(AF_INET).getKnownNodes());
auto sent = socket1->sent();
CPPUNIT_ASSERT(not sent.empty());
CPPUNIT_ASSERT(std::all_of(sent.begin(), sent.end(), isEnvelope));
}
void
DhtRunnerTransportTester::testInvalidPrivateConfig()
{
auto authority = dht::transport::Authority::generate();
auto otherKey = privateTransport(authority);
otherKey.key = dht::transport::KeyPair::generate();
dht::DhtRunner node1;
auto config = transportTestConfig(otherKey);
CPPUNIT_ASSERT_THROW(node1.run(0, config), dht::DhtException);
auto untrusted = privateTransport(authority);
untrusted.authorities = {dht::transport::Authority::generate().pub};
dht::DhtRunner node2;
config = transportTestConfig(untrusted);
CPPUNIT_ASSERT_THROW(node2.run(0, config), dht::DhtException);
auto disabled = privateTransport(authority);
disabled.enabled = false;
dht::DhtRunner node3;
config = transportTestConfig(disabled);
CPPUNIT_ASSERT_THROW(node3.run(0, config), dht::DhtException);
}
} // namespace test
+20
View File
@@ -26,6 +26,26 @@ public:
void testPushWhileRunning();
};
class DhtRunnerTransportTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(DhtRunnerTransportTester);
CPPUNIT_TEST(testEncryptedByDefault);
CPPUNIT_TEST(testLegacyInterop);
CPPUNIT_TEST(testNodeIdFromTransportKey);
CPPUNIT_TEST(testPersistedTransportKey);
CPPUNIT_TEST(testPrivateNetwork);
CPPUNIT_TEST(testInvalidPrivateConfig);
CPPUNIT_TEST_SUITE_END();
public:
void testEncryptedByDefault();
void testLegacyInterop();
void testNodeIdFromTransportKey();
void testPersistedTransportKey();
void testPrivateNetwork();
void testInvalidPrivateConfig();
};
class DhtRunnerTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(DhtRunnerTester);
File diff suppressed because it is too large. Load diff
+108 -2
View File
@@ -20,13 +20,66 @@ class NetworkEngineTester : public CppUnit::TestFixture
CPPUNIT_TEST(testDropsMalformedCompletedPartialMessage);
CPPUNIT_TEST(testCompletesPartialSessionFromSameSource);
CPPUNIT_TEST(testSeparatesPartialSessionsBySource);
CPPUNIT_TEST(testListenConfirmationCarriesToken);
CPPUNIT_TEST(testPlaintextListenRequiresToken);
CPPUNIT_TEST(testListenConfirmationUpdatesSearchNodeToken);
CPPUNIT_TEST(testListenReopensSocketAfterNodeExpiration);
CPPUNIT_TEST(testUnauthorizedListenFlushClearsListenState);
CPPUNIT_TEST(testGetRequestReuse);
CPPUNIT_TEST(testFilteredProjectionDoesNotWaitForUnrelatedPeer);
CPPUNIT_TEST(testFindNodeRequestReuse);
CPPUNIT_TEST(testNodeTransportState);
CPPUNIT_TEST(testTransportDisabledKeepsLegacyWire);
CPPUNIT_TEST(testTransportEncryptsAfterKeyExchange);
CPPUNIT_TEST(testTransportLegacyPeerStaysPlaintext);
CPPUNIT_TEST(testTransportRejectsMismatchedNodeId);
CPPUNIT_TEST(testTransportIgnoresForgedNodeListKey);
CPPUNIT_TEST(testTransportPlaintextDoesNotMoveKeyedNode);
CPPUNIT_TEST(testTransportPrivateNetwork);
CPPUNIT_TEST(testTransportPrivateClientKeepsAuthorization);
CPPUNIT_TEST(testTransportPrivateNetworkRejectsOutsiders);
CPPUNIT_TEST(testTransportEpochRotation);
CPPUNIT_TEST(testTransportPeerRestart);
CPPUNIT_TEST(testTransportHelloIsUnauthenticated);
CPPUNIT_TEST(testTransportSessionAuthorizesWrites);
CPPUNIT_TEST(testTransportInitialWriteIsUnauthorized);
CPPUNIT_TEST(testTransportUnvalidatedGetIsUnauthorized);
CPPUNIT_TEST(testTransportEphemeralBoundToAddress);
CPPUNIT_TEST(testTransportRestartedPeerReconnects);
CPPUNIT_TEST(testTransportSpoofedInitialKeepsSession);
CPPUNIT_TEST(testTransportSpoofedHelloKeepsSession);
CPPUNIT_TEST(testTransportSpoofedInitialsDoNotFillSessionCache);
CPPUNIT_TEST(testTransportSessionCacheEvictsLeastRecentlyUsed);
CPPUNIT_TEST(testTransportRejectedSessionsKeepCache);
CPPUNIT_TEST(testTransportInitialSenderConfirmsAfterSessionEnvelope);
CPPUNIT_TEST(testTransportEncryptedFragmentsIgnorePlaintextParts);
CPPUNIT_TEST(testTransportEncryptedFragmentsIgnorePlaintextHeader);
CPPUNIT_TEST(testTransportEncryptedFragmentsRequireSenderKey);
CPPUNIT_TEST(testValueFragmentsFitMinimumMtu);
CPPUNIT_TEST(testTransportRetryRecoversSessionBeforePayload);
CPPUNIT_TEST(testTransportRetryNeverResendsPayloadInInitial);
CPPUNIT_TEST(testTransportRecoveryWithholdsAllRequests);
CPPUNIT_TEST(testTransportColdRequestsShareProbe);
CPPUNIT_TEST(testTransportDelayedReplyDoesNotResend);
CPPUNIT_TEST(testTransportAuthenticatedReplyCanMoveNode);
CPPUNIT_TEST(testTransportRecoveryRefreshesDeadline);
CPPUNIT_TEST(testTransportSearchRoutesBeforeFetching);
CPPUNIT_TEST(testTransportSearchProbeSlots);
CPPUNIT_TEST(testTransportSearchHalfUnreachable);
CPPUNIT_TEST(testTransportProbeFailureNotifiesWaiters);
CPPUNIT_TEST(testTransportProbeErrorExpiresWaiters);
CPPUNIT_TEST(testTransportFindReleasesWaiters);
CPPUNIT_TEST(testTransportColdLegacyGet);
CPPUNIT_TEST(testTransportPlaintextCannotPoisonSessionCache);
CPPUNIT_TEST(testTransportPlaintextCannotUpdateKeyedNode);
CPPUNIT_TEST(testTransportPlaintextCannotCompleteEncryptedRequest);
CPPUNIT_TEST(testTransportInitialReplayKeepsPeerState);
CPPUNIT_TEST(testTransportInitialAddressChangeNeedsSession);
CPPUNIT_TEST(testTransportEncryptionFailureDoesNotSendPlaintext);
CPPUNIT_TEST(testRateLimitSeparatesSessionRequests);
CPPUNIT_TEST(testRateLimitCryptoAdmission);
CPPUNIT_TEST(testRateLimitCachedSessionCredentials);
CPPUNIT_TEST(testRateLimitCryptoPerPrefix);
CPPUNIT_TEST(testRateLimitReservesExpectedReplies);
#endif
CPPUNIT_TEST_SUITE_END();
@@ -44,13 +97,66 @@ public:
void testDropsMalformedCompletedPartialMessage();
void testCompletesPartialSessionFromSameSource();
void testSeparatesPartialSessionsBySource();
void testListenConfirmationCarriesToken();
void testPlaintextListenRequiresToken();
void testListenConfirmationUpdatesSearchNodeToken();
void testListenReopensSocketAfterNodeExpiration();
void testUnauthorizedListenFlushClearsListenState();
void testGetRequestReuse();
void testFilteredProjectionDoesNotWaitForUnrelatedPeer();
void testFindNodeRequestReuse();
void testNodeTransportState();
void testTransportDisabledKeepsLegacyWire();
void testTransportEncryptsAfterKeyExchange();
void testTransportLegacyPeerStaysPlaintext();
void testTransportRejectsMismatchedNodeId();
void testTransportIgnoresForgedNodeListKey();
void testTransportPlaintextDoesNotMoveKeyedNode();
void testTransportPrivateNetwork();
void testTransportPrivateClientKeepsAuthorization();
void testTransportPrivateNetworkRejectsOutsiders();
void testTransportEpochRotation();
void testTransportPeerRestart();
void testTransportHelloIsUnauthenticated();
void testTransportSessionAuthorizesWrites();
void testTransportInitialWriteIsUnauthorized();
void testTransportUnvalidatedGetIsUnauthorized();
void testTransportEphemeralBoundToAddress();
void testTransportRestartedPeerReconnects();
void testTransportSpoofedInitialKeepsSession();
void testTransportSpoofedHelloKeepsSession();
void testTransportSpoofedInitialsDoNotFillSessionCache();
void testTransportSessionCacheEvictsLeastRecentlyUsed();
void testTransportRejectedSessionsKeepCache();
void testTransportInitialSenderConfirmsAfterSessionEnvelope();
void testTransportEncryptedFragmentsIgnorePlaintextParts();
void testTransportEncryptedFragmentsIgnorePlaintextHeader();
void testTransportEncryptedFragmentsRequireSenderKey();
void testValueFragmentsFitMinimumMtu();
void testTransportRetryRecoversSessionBeforePayload();
void testTransportRetryNeverResendsPayloadInInitial();
void testTransportRecoveryWithholdsAllRequests();
void testTransportColdRequestsShareProbe();
void testTransportDelayedReplyDoesNotResend();
void testTransportAuthenticatedReplyCanMoveNode();
void testTransportRecoveryRefreshesDeadline();
void testTransportSearchRoutesBeforeFetching();
void testTransportSearchProbeSlots();
void testTransportSearchHalfUnreachable();
void testTransportProbeFailureNotifiesWaiters();
void testTransportProbeErrorExpiresWaiters();
void testTransportFindReleasesWaiters();
void testTransportColdLegacyGet();
void testTransportPlaintextCannotPoisonSessionCache();
void testTransportPlaintextCannotUpdateKeyedNode();
void testTransportPlaintextCannotCompleteEncryptedRequest();
void testTransportInitialReplayKeepsPeerState();
void testTransportInitialAddressChangeNeedsSession();
void testTransportEncryptionFailureDoesNotSendPlaintext();
void testRateLimitSeparatesSessionRequests();
void testRateLimitCryptoAdmission();
void testRateLimitCachedSessionCredentials();
void testRateLimitCryptoPerPrefix();
void testRateLimitReservesExpectedReplies();
#endif
};
+356
View File
@@ -0,0 +1,356 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "test_transport.h"
#include <opendht/transport.h>
#include <opendht/value.h>
#include <msgpack.hpp>
#include <algorithm>
#include <new>
using namespace std::chrono_literals;
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(TransportTester);
using namespace dht;
using namespace dht::transport;
static Key
keyFromHex(std::string_view hex)
{
Key k;
for (size_t i = 0; i < k.size(); i++)
k[i] = std::stoi(std::string(hex.substr(2 * i, 2)), nullptr, 16);
return k;
}
static Blob
message()
{
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
pk.pack_map(2);
pk.pack("y");
pk.pack("q");
pk.pack("q");
pk.pack("ping");
return {buffer.data(), buffer.data() + buffer.size()};
}
void
TransportTester::testX25519Rfc7748()
{
auto alice = KeyPair::fromSecret(keyFromHex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"));
auto bob = KeyPair::fromSecret(keyFromHex("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"));
CPPUNIT_ASSERT(alice.pub == keyFromHex("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"));
CPPUNIT_ASSERT(bob.pub == keyFromHex("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"));
}
void
TransportTester::testSessionKeys()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto b = KeyPair::generate(), eb = KeyPair::generate();
CPPUNIT_ASSERT(a);
CPPUNIT_ASSERT(a.pub != b.pub);
auto ab = sessionKeys(a, ea, b.pub, eb.pub, 0);
auto ba = sessionKeys(b, eb, a.pub, ea.pub, 0);
CPPUNIT_ASSERT(ab and ba);
CPPUNIT_ASSERT(ab->tx == ba->rx);
CPPUNIT_ASSERT(ab->rx == ba->tx);
CPPUNIT_ASSERT(ab->tx != ab->rx);
auto c = KeyPair::generate();
for (const auto& other : {sessionKeys(a, ea, b.pub, eb.pub, 42),
sessionKeys(a, c, b.pub, eb.pub, 0),
sessionKeys(a, ea, b.pub, c.pub, 0),
sessionKeys(a, ea, c.pub, eb.pub, 0),
sessionKeys(c, ea, b.pub, eb.pub, 0)}) {
CPPUNIT_ASSERT(other);
CPPUNIT_ASSERT(other->tx != ab->tx and other->rx != ab->rx);
}
}
void
TransportTester::testSessionKeysErasure()
{
alignas(SessionKeys) std::array<uint8_t, sizeof(SessionKeys)> storage {};
Key tx, rx;
tx.fill(0xA5);
rx.fill(0x5A);
auto keys = new (storage.data()) SessionKeys {tx, rx};
SessionKeys copied = *keys;
SessionKeys assigned {};
assigned = *keys;
keys->~SessionKeys();
CPPUNIT_ASSERT(std::all_of(storage.begin(), storage.end(), [](uint8_t byte) { return byte == 0; }));
CPPUNIT_ASSERT(copied.tx == tx and copied.rx == rx);
CPPUNIT_ASSERT(assigned.tx == tx and assigned.rx == rx);
}
void
TransportTester::testInitialKey()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto b = KeyPair::generate(), eb = KeyPair::generate();
auto sent = initialSendKey(a, ea, b.pub, 0);
auto received = initialReceiveKey(b, a.pub, ea.pub, 0);
CPPUNIT_ASSERT(sent and received);
CPPUNIT_ASSERT(*sent == *received);
auto session = sessionKeys(a, ea, b.pub, eb.pub, 0);
CPPUNIT_ASSERT(*sent != session->tx and *sent != session->rx);
CPPUNIT_ASSERT(*initialSendKey(a, ea, b.pub, 42) != *sent);
CPPUNIT_ASSERT(*initialSendKey(a, eb, b.pub, 0) != *sent);
// directional
CPPUNIT_ASSERT(*initialSendKey(b, ea, a.pub, 0) != *sent);
}
void
TransportTester::testRejectsLowOrderKey()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto b = KeyPair::generate();
Key one {};
one[0] = 1;
for (const auto& bad : {Key {}, one}) {
CPPUNIT_ASSERT(not sessionKeys(a, ea, bad, b.pub, 0));
CPPUNIT_ASSERT(not sessionKeys(a, ea, b.pub, bad, 0));
CPPUNIT_ASSERT(not initialSendKey(a, ea, bad, 0));
CPPUNIT_ASSERT(not initialReceiveKey(a, bad, b.pub, 0));
CPPUNIT_ASSERT(not initialReceiveKey(a, b.pub, bad, 0));
}
}
void
TransportTester::testNodeId()
{
auto a = KeyPair::generate();
auto b = KeyPair::generate();
CPPUNIT_ASSERT(nodeId(a.pub));
CPPUNIT_ASSERT_EQUAL(nodeId(a.pub), nodeId(KeyPair::fromSecret(a.secret).pub));
CPPUNIT_ASSERT(nodeId(a.pub) != nodeId(b.pub));
}
void
TransportTester::testAddressKey()
{
auto secret = randomSecret(), other = randomSecret();
CPPUNIT_ASSERT(secret != other);
const uint8_t x[] = {127, 0, 0, 2, 0x10, 0x7e};
const uint8_t y[] = {127, 0, 0, 2, 0x10, 0x7f};
auto key = addressKey(secret, x, sizeof(x));
CPPUNIT_ASSERT(key);
CPPUNIT_ASSERT(key.pub == KeyPair::fromSecret(key.secret).pub);
CPPUNIT_ASSERT(key.secret == addressKey(secret, x, sizeof(x)).secret);
CPPUNIT_ASSERT(key.secret != addressKey(secret, y, sizeof(y)).secret);
CPPUNIT_ASSERT(key.secret != addressKey(other, x, sizeof(x)).secret);
auto token = addressToken(secret, x, sizeof(x));
CPPUNIT_ASSERT(token == addressToken(secret, x, sizeof(x)));
CPPUNIT_ASSERT(token != addressToken(secret, y, sizeof(y)));
CPPUNIT_ASSERT(token != addressToken(other, x, sizeof(x)));
CPPUNIT_ASSERT(token != key.secret and token != key.pub);
}
void
TransportTester::testSealOpen()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto b = KeyPair::generate(), eb = KeyPair::generate();
auto keys = *sessionKeys(a, ea, b.pub, eb.pub, 0);
auto msg = message();
auto envelope = seal(keys.tx, EnvelopeType::Session, a.pub, ea.pub, msg.data(), msg.size());
CPPUNIT_ASSERT_EQUAL(msg.size() + ENVELOPE_OVERHEAD, envelope.size());
CPPUNIT_ASSERT(isEnvelope(envelope.data(), envelope.size()));
auto header = parseHeader(envelope.data(), envelope.size());
CPPUNIT_ASSERT(header);
CPPUNIT_ASSERT(header->type == EnvelopeType::Session);
CPPUNIT_ASSERT(not header->credential);
CPPUNIT_ASSERT(header->sender == a.pub);
CPPUNIT_ASSERT(header->ephemeral == ea.pub);
auto opened = open(sessionKeys(b, eb, a.pub, ea.pub, 0)->rx, envelope.data(), envelope.size());
CPPUNIT_ASSERT(opened);
CPPUNIT_ASSERT(*opened == msg);
// Random nonces: sealing twice gives different envelopes
CPPUNIT_ASSERT(seal(keys.tx, EnvelopeType::Session, a.pub, ea.pub, msg.data(), msg.size()) != envelope);
Blob credential {1, 2, 3};
auto withCredential = seal(keys.tx, EnvelopeType::Initial, a.pub, ea.pub, msg.data(), msg.size(), credential);
header = parseHeader(withCredential.data(), withCredential.size());
CPPUNIT_ASSERT(header and header->type == EnvelopeType::Initial and header->credential);
auto openedCredential = open(keys.tx, withCredential.data(), withCredential.size());
CPPUNIT_ASSERT(openedCredential);
CPPUNIT_ASSERT(std::equal(credential.begin(), credential.end(), openedCredential->begin()));
CPPUNIT_ASSERT(std::equal(msg.begin(), msg.end(), openedCredential->begin() + credential.size()));
}
void
TransportTester::testOpenRejectsTampering()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto b = KeyPair::generate(), eb = KeyPair::generate();
auto keys = *sessionKeys(a, ea, b.pub, eb.pub, 0);
auto msg = message();
auto envelope = seal(keys.tx, EnvelopeType::Session, a.pub, ea.pub, msg.data(), msg.size());
for (size_t i : {(size_t) 1, (size_t) 2, 2 + KEY_SIZE, HEADER_SIZE, HEADER_SIZE + NONCE_SIZE, envelope.size() - 1}) {
auto tampered = envelope;
tampered[i] ^= 0x01;
CPPUNIT_ASSERT(not open(keys.tx, tampered.data(), tampered.size()));
}
CPPUNIT_ASSERT(not open(keys.tx, envelope.data(), envelope.size() - 1));
CPPUNIT_ASSERT(not open(keys.tx, envelope.data(), ENVELOPE_OVERHEAD - 1));
CPPUNIT_ASSERT(not open(keys.rx, envelope.data(), envelope.size()));
CPPUNIT_ASSERT(not open(sessionKeys(b, eb, a.pub, ea.pub, 1)->rx, envelope.data(), envelope.size()));
}
void
TransportTester::testDataEnvelope()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto b = KeyPair::generate(), eb = KeyPair::generate();
auto keys = *sessionKeys(a, ea, b.pub, eb.pub, 0);
auto msg = message();
auto envelope = sealData(keys.tx, msg.data(), msg.size());
CPPUNIT_ASSERT_EQUAL(msg.size() + DATA_ENVELOPE_OVERHEAD, envelope.size());
CPPUNIT_ASSERT_EQUAL((size_t) 30, DATA_ENVELOPE_OVERHEAD);
CPPUNIT_ASSERT(isEnvelope(envelope.data(), envelope.size()));
CPPUNIT_ASSERT_THROW(msgpack::unpack((const char*) envelope.data(), envelope.size()), msgpack::parse_error);
auto header = parseHeader(envelope.data(), envelope.size());
CPPUNIT_ASSERT(header and header->type == EnvelopeType::Data and not header->credential);
auto opened = open(sessionKeys(b, eb, a.pub, ea.pub, 0)->rx, envelope.data(), envelope.size());
CPPUNIT_ASSERT(opened and *opened == msg);
for (size_t i : {(size_t) 1, DATA_HEADER_SIZE, DATA_HEADER_SIZE + NONCE_SIZE, envelope.size() - 1}) {
auto tampered = envelope;
tampered[i] ^= 0x01;
CPPUNIT_ASSERT(not open(keys.tx, tampered.data(), tampered.size()));
}
CPPUNIT_ASSERT(not open(keys.tx, envelope.data(), DATA_ENVELOPE_OVERHEAD - 1));
CPPUNIT_ASSERT(not open(keys.rx, envelope.data(), envelope.size()));
// Same key, other type: the type is authenticated
auto session = seal(keys.tx, EnvelopeType::Session, a.pub, ea.pub, msg.data(), msg.size());
Blob relabeled {ENVELOPE_MARKER, static_cast<uint8_t>(EnvelopeType::Data)};
relabeled.insert(relabeled.end(), session.begin() + HEADER_SIZE, session.end());
CPPUNIT_ASSERT(not open(keys.tx, relabeled.data(), relabeled.size()));
// Data never carries a credential
auto flagged = envelope;
flagged[1] |= CREDENTIAL_FLAG;
CPPUNIT_ASSERT(not parseHeader(flagged.data(), flagged.size()));
CPPUNIT_ASSERT(not open(keys.tx, flagged.data(), flagged.size()));
}
void
TransportTester::testParseHeader()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto msg = message();
auto envelope = seal(Key {}, EnvelopeType::Session, a.pub, ea.pub, msg.data(), msg.size());
CPPUNIT_ASSERT(parseHeader(envelope.data(), HEADER_SIZE));
CPPUNIT_ASSERT(not parseHeader(envelope.data(), HEADER_SIZE - 1));
for (uint8_t type : {0, 5, 0x80, 0x84, 0x85}) {
auto unknown = envelope;
unknown[1] = type;
CPPUNIT_ASSERT(not parseHeader(unknown.data(), unknown.size()));
}
CPPUNIT_ASSERT(not parseHeader(msg.data(), msg.size()));
}
void
TransportTester::testEnvelopeIsNotMsgpack()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto msg = message();
auto envelope = seal(Key {}, EnvelopeType::Session, a.pub, ea.pub, msg.data(), msg.size());
CPPUNIT_ASSERT_THROW(msgpack::unpack((const char*) envelope.data(), envelope.size()), msgpack::parse_error);
auto h = hello(a.pub, ea.pub, Blob(Credential::PACKED_SIZE), msg.data(), msg.size());
CPPUNIT_ASSERT_THROW(msgpack::unpack((const char*) h.data(), h.size()), msgpack::parse_error);
}
void
TransportTester::testHello()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto credential = Authority::generate().issue(a.pub).pack();
auto msg = message();
auto h = hello(a.pub, ea.pub, credential, msg.data(), msg.size());
CPPUNIT_ASSERT_EQUAL(HEADER_SIZE + Credential::PACKED_SIZE + msg.size(), h.size());
auto header = parseHeader(h.data(), h.size());
CPPUNIT_ASSERT(header);
CPPUNIT_ASSERT(header->type == EnvelopeType::Hello);
CPPUNIT_ASSERT(header->sender == a.pub);
CPPUNIT_ASSERT(header->ephemeral == ea.pub);
CPPUNIT_ASSERT(std::equal(credential.begin(), credential.end(), h.begin() + HEADER_SIZE));
CPPUNIT_ASSERT(std::equal(msg.begin(), msg.end(), h.begin() + HEADER_SIZE + credential.size()));
}
void
TransportTester::testCredential()
{
auto authority = Authority::generate();
CPPUNIT_ASSERT(Authority::fromSecret(authority.secret).pub == authority.pub);
auto key = KeyPair::generate();
auto credential = authority.issue(key.pub);
CPPUNIT_ASSERT(credential);
CPPUNIT_ASSERT(credential.key == key.pub);
CPPUNIT_ASSERT(credential.verify({authority.pub}));
auto nextAuthority = Authority::generate();
auto nextCredential = nextAuthority.issue(key.pub);
CPPUNIT_ASSERT(credential.verify({authority.pub, nextAuthority.pub}));
CPPUNIT_ASSERT(nextCredential.verify({authority.pub, nextAuthority.pub}));
CPPUNIT_ASSERT(not credential.verify({nextAuthority.pub}));
CPPUNIT_ASSERT(nextCredential.verify({nextAuthority.pub}));
auto packed = credential.pack();
CPPUNIT_ASSERT_EQUAL(size_t {97}, packed.size());
CPPUNIT_ASSERT_EQUAL(uint8_t {2}, packed[0]);
CPPUNIT_ASSERT_EQUAL(size_t {Credential::PACKED_SIZE}, packed.size());
auto unpacked = Credential::unpack(packed);
CPPUNIT_ASSERT(unpacked.key == credential.key);
CPPUNIT_ASSERT(unpacked.signature == credential.signature);
CPPUNIT_ASSERT(unpacked.verify({authority.pub}));
}
void
TransportTester::testCredentialRejections()
{
auto authority = Authority::generate();
auto key = KeyPair::generate();
auto credential = authority.issue(key.pub);
CPPUNIT_ASSERT(not credential.verify({}));
CPPUNIT_ASSERT(not credential.verify({Authority::generate().pub}));
auto otherKey = credential;
otherKey.key = KeyPair::generate().pub;
CPPUNIT_ASSERT(not otherKey.verify({authority.pub}));
auto tampered = credential;
tampered.signature[0] ^= 1;
CPPUNIT_ASSERT(not tampered.verify({authority.pub}));
auto packed = credential.pack();
CPPUNIT_ASSERT_THROW(Credential::unpack(packed.data(), packed.size() - 1), std::invalid_argument);
packed[0] = 1;
CPPUNIT_ASSERT_THROW(Credential::unpack(packed), std::invalid_argument);
}
} // namespace test
+51
View File
@@ -0,0 +1,51 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
namespace test {
class TransportTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(TransportTester);
CPPUNIT_TEST(testX25519Rfc7748);
CPPUNIT_TEST(testSessionKeys);
CPPUNIT_TEST(testSessionKeysErasure);
CPPUNIT_TEST(testInitialKey);
CPPUNIT_TEST(testRejectsLowOrderKey);
CPPUNIT_TEST(testNodeId);
CPPUNIT_TEST(testAddressKey);
CPPUNIT_TEST(testSealOpen);
CPPUNIT_TEST(testOpenRejectsTampering);
CPPUNIT_TEST(testDataEnvelope);
CPPUNIT_TEST(testParseHeader);
CPPUNIT_TEST(testEnvelopeIsNotMsgpack);
CPPUNIT_TEST(testHello);
CPPUNIT_TEST(testCredential);
CPPUNIT_TEST(testCredentialRejections);
CPPUNIT_TEST_SUITE_END();
public:
void setUp() {}
void tearDown() {}
void testX25519Rfc7748();
void testSessionKeys();
void testSessionKeysErasure();
void testInitialKey();
void testRejectsLowOrderKey();
void testNodeId();
void testAddressKey();
void testSealOpen();
void testOpenRejectsTampering();
void testDataEnvelope();
void testParseHeader();
void testEnvelopeIsNotMsgpack();
void testHello();
void testCredential();
void testCredentialRejections();
};
} // namespace test
+12 -1
View File
@@ -38,7 +38,11 @@ print_usage()
{
std::cout
<< "Usage: dhtnode [-v [-l logfile]] [-i] [-d] [-n network_id] [-p local_port] [-b bootstrap_host[:port]] "
"[--proxyserver local_port] [--proxyserverssl local_port] [--bundleid bundleid] [--pushserver endpoint]"
"[--proxyserver local_port] [--proxyserverssl local_port] [--bundleid bundleid] [--pushserver endpoint] "
"[--no-encryption] [--transport-key file] [--network-authority key] [--network-credential file]"
<< std::endl
<< " dhtnode --gen-network-authority prefix" << std::endl
<< " dhtnode --issue-credential file --authority-key file --transport-key file"
<< std::endl
<< std::endl;
print_info();
@@ -576,6 +580,13 @@ main(int argc, char** argv)
print_version();
return 0;
}
try {
if (runNetworkAuthorityCommands(params))
return 0;
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
if (params.daemonize) {
daemonize();
+2
View File
@@ -35,6 +35,8 @@ benchPingPong(unsigned netSize, unsigned n_parallel)
DhtRunner::Config config {};
config.dht_config.node_config.max_peer_req_per_sec = -1;
config.dht_config.node_config.max_req_per_sec = -1;
config.dht_config.node_config.max_crypto_per_sec = -1;
config.dht_config.node_config.max_peer_crypto_per_sec = -1;
DhtRunner ping_node, pong_node;
+131
View File
@@ -53,6 +53,7 @@
#include <iostream>
#include <sstream>
#include <fstream>
#include <filesystem>
namespace dht {
namespace tools {
@@ -122,6 +123,60 @@ loadFile(const std::string& path)
return buffer;
}
void
saveFile(const std::string& path, const uint8_t* data, size_t size, bool secret)
{
auto temporary = path + "." + dht::InfoHash::getRandom().toString() + ".tmp";
try {
std::ofstream file;
file.exceptions(std::ios::failbit | std::ios::badbit);
file.open(temporary, std::ios::binary | std::ios::trunc);
if (secret)
std::filesystem::permissions(temporary,
std::filesystem::perms::owner_read | std::filesystem::perms::owner_write);
file.write((const char*) data, size);
file.close();
std::filesystem::rename(temporary, path);
} catch (...) {
std::error_code ec;
std::filesystem::remove(temporary, ec);
throw;
}
}
dht::transport::Key
loadTransportKey(const std::string& path)
{
auto data = loadFile(path);
dht::transport::Key key;
if (data.size() != key.size())
throw std::runtime_error("Invalid key file: " + path);
std::copy(data.begin(), data.end(), key.begin());
return key;
}
/* Loads the transport key from path, or generates and saves it if the file doesn't exist */
dht::transport::KeyPair
loadOrCreateTransportKey(const std::string& path)
{
if (std::filesystem::exists(std::filesystem::symlink_status(path)))
return dht::transport::KeyPair::fromSecret(loadTransportKey(path));
auto key = dht::transport::KeyPair::generate();
saveFile(path, key.secret.data(), key.secret.size(), true);
return key;
}
dht::transport::Key
keyFromHex(const std::string& hex)
{
dht::transport::Key key;
if (hex.size() != 2 * key.size())
throw std::runtime_error("Invalid key: " + hex);
for (size_t i = 0; i < key.size(); i++)
key[i] = std::stoul(hex.substr(2 * i, 2), nullptr, 16);
return key;
}
struct dht_params
{
bool help {false}; // print help and exit
@@ -151,8 +206,47 @@ struct dht_params
std::string save_identity {};
bool no_rate_limit {false};
bool public_stable {false};
bool no_encryption {false};
std::string transport_key {};
std::vector<dht::transport::Key> authorities {};
std::string credential {};
std::string gen_authority {};
std::string issue_credential {};
std::string authority_key {};
};
/* Runs private network management commands. Returns true if one was run. */
bool
runNetworkAuthorityCommands(const dht_params& params)
{
if (not params.gen_authority.empty()) {
for (const auto& suffix : {".key", ".pub"}) {
auto path = params.gen_authority + suffix;
if (std::filesystem::exists(std::filesystem::symlink_status(path)))
throw std::runtime_error("Refusing to overwrite network authority file: " + path);
}
auto authority = dht::transport::Authority::generate();
auto pub = dht::toHex(authority.pub.data(), authority.pub.size());
saveFile(params.gen_authority + ".key", authority.secret.data(), authority.secret.size(), true);
saveFile(params.gen_authority + ".pub", (const uint8_t*) pub.data(), pub.size(), false);
std::cout << "Network authority saved to " << params.gen_authority << ".key" << std::endl
<< "Public key (--network-authority): " << pub << std::endl;
return true;
}
if (not params.issue_credential.empty()) {
if (params.authority_key.empty() or params.transport_key.empty())
throw std::runtime_error("--issue-credential requires --authority-key and --transport-key");
auto authority = dht::transport::Authority::fromSecret(loadTransportKey(params.authority_key));
auto key = loadOrCreateTransportKey(params.transport_key);
auto credential = authority.issue(key.pub).pack();
saveFile(params.issue_credential, credential.data(), credential.size(), false);
std::cout << "Credential for node " << dht::transport::nodeId(key.pub) << " saved to "
<< params.issue_credential << ", does not expire" << std::endl;
return true;
}
return false;
}
std::pair<dht::DhtRunner::Config, dht::DhtRunner::Context>
getDhtConfig(dht_params& params)
{
@@ -181,9 +275,18 @@ getDhtConfig(dht_params& params)
if (params.no_rate_limit) {
config.dht_config.node_config.max_req_per_sec = -1;
config.dht_config.node_config.max_peer_req_per_sec = -1;
config.dht_config.node_config.max_crypto_per_sec = -1;
config.dht_config.node_config.max_peer_crypto_per_sec = -1;
config.dht_config.node_config.max_searches = -1;
config.dht_config.node_config.max_store_size = -1;
}
auto& transport = config.dht_config.node_config.transport;
transport.enabled = not params.no_encryption;
if (not params.transport_key.empty())
transport.key = loadOrCreateTransportKey(params.transport_key);
transport.authorities = params.authorities;
if (not params.credential.empty())
transport.credential = dht::transport::Credential::unpack(loadFile(params.credential));
dht::DhtRunner::Context context {};
if (params.log) {
@@ -253,6 +356,13 @@ static const constexpr struct option long_options[] = {
{"devicekey", required_argument, nullptr, 'z'},
{"bundleid", required_argument, nullptr, 'u'},
{"version", no_argument, nullptr, 'V'},
{"no-encryption", no_argument, nullptr, 'N'},
{"transport-key", required_argument, nullptr, 'T'},
{"network-authority", required_argument, nullptr, 'A'},
{"network-credential", required_argument, nullptr, 'R'},
{"gen-network-authority", required_argument, nullptr, 'G'},
{"issue-credential", required_argument, nullptr, 'Y'},
{"authority-key", required_argument, nullptr, 'X'},
{nullptr, 0, nullptr, 0 }
};
@@ -375,6 +485,27 @@ parseArgs(int argc, char** argv)
case 'I':
params.save_identity = optarg;
break;
case 'N':
params.no_encryption = true;
break;
case 'T':
params.transport_key = optarg;
break;
case 'A':
params.authorities.emplace_back(keyFromHex(optarg));
break;
case 'R':
params.credential = optarg;
break;
case 'G':
params.gen_authority = optarg;
break;
case 'Y':
params.issue_credential = optarg;
break;
case 'X':
params.authority_key = optarg;
break;
default:
break;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "opendht",
"version-string": "4.5.0-rc.1",
"version-string": "4.4.99",
"port-version": 1,
"description": "OpenDHT: A lightweight C++17 Distributed Hash Table library.",
"homepage": "https://opendht.net",