Compare commits

..
39 changed files with 2786 additions and 4344 deletions

No files matched your search

+9 -6
View File
@@ -502,7 +502,11 @@ list (APPEND opendht_HEADERS
)
if (OPENDHT_PEER_DISCOVERY)
list (APPEND opendht_SOURCES src/peer_discovery.cpp)
list (APPEND opendht_SOURCES
src/peer_discovery/mdns.cpp
src/peer_discovery/dns_sd.cpp
src/peer_discovery/mdns_transport.cpp
src/peer_discovery/peer_discovery.cpp)
list (APPEND opendht_HEADERS include/opendht/peer_discovery.h)
add_definitions(-DOPENDHT_PEER_DISCOVERY)
endif()
@@ -589,6 +593,7 @@ if (MSVC)
simdutf::simdutf
wsock32
ws2_32
iphlpapi
)
if (OPENDHT_TOOLS)
target_link_libraries(opendht PUBLIC unofficial::readline-win32::readline)
@@ -672,7 +677,7 @@ else()
# System-specific linker pipelines
if (WIN32 AND MINGW) # MSYS2
message(STATUS "Build pipeline: Windows/MSYS2-MinGW")
target_link_libraries(opendht PUBLIC wsock32 ws2_32)
target_link_libraries(opendht PUBLIC wsock32 ws2_32 iphlpapi)
if(OPENDHT_PROXY_OPENSSL)
target_link_libraries(opendht PUBLIC Crypt32)
endif()
@@ -870,10 +875,6 @@ if (BUILD_TESTING)
tests/test_parsedmessage.cpp
tests/test_networkengine.h
tests/test_networkengine.cpp
tests/test_opcache.h
tests/test_opcache.cpp
tests/test_valuecache.h
tests/test_valuecache.cpp
)
if (OPENDHT_PROXY_CLIENT)
list (APPEND test_FILES
@@ -892,6 +893,8 @@ if (BUILD_TESTING)
endif()
if (OPENDHT_PEER_DISCOVERY)
list (APPEND test_FILES
tests/test_mdns.h
tests/test_mdns.cpp
tests/test_peerdiscovery.h
tests/test_peerdiscovery.cpp
)
+2 -9
View File
@@ -204,13 +204,7 @@ struct OPENDHT_PUBLIC PrivateKey
* @param algo : the public key algorithm to use, GNUTLS_PK_RSA or GNUTLS_PK_RSA_OAEP
*/
static PrivateKey generate(unsigned key_length = 4096, gnutls_pk_algorithm_t algo = GNUTLS_PK_RSA);
/**
* Generate a new EC key pair
* @param curve : the elliptic curve to use. If GNUTLS_ECC_CURVE_INVALID is provided, the default curve will be used
* (currently GNUTLS_ECC_CURVE_SECP384R1).
*/
static PrivateKey generateEC(gnutls_ecc_curve_t curve = GNUTLS_ECC_CURVE_INVALID);
static PrivateKey generateEC();
gnutls_privkey_t key {};
gnutls_x509_privkey_t x509_key {};
@@ -278,8 +272,7 @@ public:
* and incremented on subsequent ones.
*/
void sign(const PrivateKey&, const Certificate&, duration validity_period = {}, uint64_t number = 0);
void sign(const Identity& id, duration validity_period = {}, uint64_t number = 0)
{
void sign(const Identity& id, duration validity_period = {}, uint64_t number = 0) {
sign(*id.first, *id.second, validity_period, number);
}
+8 -14
View File
@@ -76,14 +76,8 @@ public:
asio::io_context& io_context() const;
// Monotonic clock for elapsed-time statistics (request rate, uptime).
// Must stay on steady_clock so NTP/wall-clock corrections don't skew durations.
using clock = std::chrono::steady_clock;
using time_point = clock::time_point;
// Wall clock for persisted subscribe/permanent-put lease expirations,
// so they map directly to epoch time and survive restarts.
using sys_clock = std::chrono::system_clock;
using sys_time_point = sys_clock::time_point;
struct PushStats
{
@@ -407,12 +401,12 @@ private:
std::shared_ptr<ConnectionListener> connListener_;
struct PermanentPut
{
sys_time_point expiration;
time_point expiration;
std::string pushToken;
std::string clientId;
std::shared_ptr<PushSessionContext> sessionCtx;
std::unique_ptr<asio::system_timer> expireTimer;
std::unique_ptr<asio::system_timer> expireNotifyTimer;
std::unique_ptr<asio::steady_timer> expireTimer;
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
Sp<Value> value;
PushType type;
std::string topic;
@@ -425,7 +419,7 @@ private:
p.pack("value");
p.pack(value);
p.pack("exp");
p.pack(sys_clock::to_time_t(expiration));
p.pack(to_time_t(expiration));
if (not clientId.empty()) {
p.pack("cid");
p.pack(clientId);
@@ -466,12 +460,12 @@ private:
#ifdef OPENDHT_PUSH_NOTIFICATIONS
struct Listener
{
sys_time_point expiration;
time_point expiration;
std::string clientId;
std::shared_ptr<PushSessionContext> sessionCtx;
std::future<size_t> internalToken;
std::unique_ptr<asio::system_timer> expireTimer;
std::unique_ptr<asio::system_timer> expireNotifyTimer;
std::unique_ptr<asio::steady_timer> expireTimer;
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
PushType type;
std::string topic;
@@ -482,7 +476,7 @@ private:
p.pack("cid");
p.pack(clientId);
p.pack("exp");
p.pack(sys_clock::to_time_t(expiration));
p.pack(to_time_t(expiration));
if (sessionCtx) {
std::lock_guard l(sessionCtx->lock);
p.pack("sid");
-7
View File
@@ -106,16 +106,9 @@ public:
void timeout(const std::chrono::seconds& timeout, HandlerCb cb = {});
/**
* Close the underlying socket.
* Safe to call from any thread: the actual close is dispatched to
* the thread running the io_context.
*/
void close();
private:
void doClose();
template<typename T>
T wrapCallback(T cb) const
{
+13 -52
View File
@@ -3,12 +3,13 @@
#pragma once
#include "def.h"
#include "sockaddr.h"
#include "infohash.h"
#include "logger.h"
#include "sockaddr.h"
#include "utils.h"
#include <asio/steady_timer.hpp>
#include <functional>
#include <memory>
#include <thread>
namespace asio {
@@ -20,66 +21,26 @@ namespace dht {
class OPENDHT_PUBLIC PeerDiscovery
{
public:
static constexpr in_port_t DEFAULT_PORT = 8888;
using ServiceDiscoveredCallback = std::function<void(msgpack::object&&, SockAddr&&)>;
using PeerDiscoveredCallback = std::function<void(const InfoHash&, SockAddr&&)>;
PeerDiscovery(in_port_t port = DEFAULT_PORT,
std::shared_ptr<asio::io_context> ioContext = {},
std::shared_ptr<Logger> logger = {});
PeerDiscovery(std::shared_ptr<asio::io_context> ioContext = {}, std::shared_ptr<Logger> logger = {});
~PeerDiscovery();
/**
* startDiscovery - Keep Listening data from the sender until node is joinned
* or stop is called
*/
void startDiscovery(std::string_view type, ServiceDiscoveredCallback callback);
void startDiscovery(NetId network, PeerDiscoveredCallback callback);
void startPublish(const InfoHash& nodeId, NetId network, in_port_t port);
template<typename T>
void startDiscovery(std::string_view type, std::function<void(T&&, SockAddr&&)> cb)
{
startDiscovery(type, [cb](msgpack::object&& ob, SockAddr&& addr) { cb(ob.as<T>(), std::move(addr)); });
}
/**
* startPublish - Keeping sending data until node is joinned or stop is called
*/
void startPublish(std::string_view type, const msgpack::sbuffer& pack_buf);
void startPublish(sa_family_t domain, std::string_view type, const msgpack::sbuffer& pack_buf);
template<typename T>
void startPublish(std::string_view type, const T& object)
{
msgpack::sbuffer buf;
msgpack::pack(buf, object);
startPublish(type, buf);
}
/**
* Thread Stopper
*/
bool stopDiscovery();
bool stopPublish();
void stop();
/**
* Remove possible callBack to discovery
*/
bool stopDiscovery(std::string_view type);
/**
* Remove different serivce message to send
*/
bool stopPublish(std::string_view type);
bool stopPublish(sa_family_t domain, std::string_view type);
void connectivityChanged();
void stopConnectivityChanged();
private:
class DomainPeerDiscovery;
std::unique_ptr<DomainPeerDiscovery> peerDiscovery4_;
std::unique_ptr<DomainPeerDiscovery> peerDiscovery6_;
class Impl;
std::shared_ptr<Impl> impl_;
std::shared_ptr<asio::io_context> ioContext_;
std::thread ioRunnner_;
std::thread ioRunner_;
};
} // namespace dht
} // namespace dht
+1 -1
View File
@@ -8,7 +8,7 @@ namespace dht {
namespace proxy {
constexpr const std::chrono::hours OP_TIMEOUT {24}; // one day
constexpr const std::chrono::hours OP_MARGIN {8}; // eight hours
constexpr const std::chrono::hours OP_MARGIN {2}; // two hours
using ListenToken = uint64_t;
+18 -2
View File
@@ -123,13 +123,21 @@ if get_option('push_notifications').enabled()
add_project_arguments('-DOPENDHT_PUSH_NOTIFICATIONS', language: 'cpp')
endif
if get_option('peer_discovery').enabled()
opendht_src += 'src/peer_discovery.cpp'
opendht_src += [
'src/peer_discovery/mdns.cpp',
'src/peer_discovery/dns_sd.cpp',
'src/peer_discovery/mdns_transport.cpp',
'src/peer_discovery/peer_discovery.cpp',
]
add_project_arguments('-DOPENDHT_PEER_DISCOVERY', language: 'cpp')
if host_machine.system() == 'windows'
deps += meson.get_compiler('cpp').find_library('iphlpapi')
endif
endif
opendht = library(
'opendht',
opendht_src,
include_directories: opendht_inc,
include_directories: [opendht_inc, opendht_interface_inc],
dependencies: deps,
cpp_args: ['-DOPENDHT_BUILD', '-Dopendht_EXPORTS'],
install: true,
@@ -327,6 +335,14 @@ if get_option('tests').enabled()
endif
if get_option('peer_discovery').enabled()
test_mdns = executable(
'test_mdns',
'tests/test_mdns.cpp',
'tests/tests_runner.cpp',
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
)
test('Mdns', test_mdns)
test_peerdiscovery = executable(
'test_peerdiscovery',
'tests/test_peerdiscovery.cpp',
+2 -4
View File
@@ -1294,16 +1294,14 @@ PrivateKey::generate(unsigned key_length, gnutls_pk_algorithm_t algo)
}
PrivateKey
PrivateKey::generateEC(gnutls_ecc_curve_t curve)
PrivateKey::generateEC()
{
gnutls_x509_privkey_t key;
if (gnutls_x509_privkey_init(&key) != GNUTLS_E_SUCCESS)
throw CryptoException("Unable to initialize private key.");
int err = gnutls_x509_privkey_generate(key,
GNUTLS_PK_EC,
curve == GNUTLS_ECC_CURVE_INVALID
? gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA)
: GNUTLS_CURVE_TO_BITS(curve),
gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA),
0);
if (err != GNUTLS_E_SUCCESS) {
gnutls_x509_privkey_deinit(key);
+21 -35
View File
@@ -25,10 +25,8 @@ struct DhtProxyClient::OperationState
struct DhtProxyClient::Listener
{
// The proxy server is a single logical source that re-delivers values
// (push refresh, listen restart): don't count duplicates.
Listener(ValueCallback&& cb)
: cache(std::move(cb), false)
Listener(OpValueCache&& c)
: cache(std::move(c))
{}
OpValueCache cache;
@@ -36,26 +34,26 @@ struct DhtProxyClient::Listener
Sp<OperationState> opstate;
std::shared_ptr<http::Request> request;
#ifdef OPENDHT_PUSH_NOTIFICATIONS
std::unique_ptr<asio::system_timer> refreshSubscriberTimer;
std::unique_ptr<asio::steady_timer> refreshSubscriberTimer;
#endif
};
struct PermanentPut
{
PermanentPut(const Sp<Value>& v, std::unique_ptr<asio::system_timer>&& j, const Sp<std::atomic_bool>& o)
PermanentPut(const Sp<Value>& v, std::unique_ptr<asio::steady_timer>&& j, const Sp<std::atomic_bool>& o)
: value(v)
, refreshPutTimer(std::move(j))
, ok(o)
{}
Sp<Value> value;
std::unique_ptr<asio::system_timer> refreshPutTimer;
std::unique_ptr<asio::steady_timer> refreshPutTimer;
Sp<std::atomic_bool> ok;
};
struct DhtProxyClient::ProxySearch
{
SearchCache ops {false};
SearchCache ops {};
std::unique_ptr<asio::steady_timer> opExpirationTimer;
std::map<size_t, Listener> listeners {};
std::map<Value::Id, PermanentPut> puts {};
@@ -426,7 +424,7 @@ DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_po
auto& search = searches_[key];
if (val->id) {
auto id = val->id;
auto refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
proxy::OP_TIMEOUT - proxy::OP_MARGIN);
refreshPutTimer->async_wait(
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, id));
@@ -542,7 +540,7 @@ DhtProxyClient::doPut(const InfoHash& key, Sp<Value> val, DoneCallbackSimple cb,
auto it = search.pendingPuts.find(val);
if (it != search.pendingPuts.end()) {
auto sok = std::make_shared<std::atomic_bool>(ok);
auto refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
proxy::OP_TIMEOUT
- proxy::OP_MARGIN);
refreshPutTimer->async_wait(
@@ -893,8 +891,8 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt
* (if the proxy crash for any reason)
*/
if (!l->second.refreshSubscriberTimer)
l->second.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
l->second.refreshSubscriberTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT
l->second.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
l->second.refreshSubscriberTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT
- proxy::OP_MARGIN);
l->second.refreshSubscriberTimer->async_wait(
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, opstate));
@@ -1194,9 +1192,9 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
for (auto& put : search.second.puts) {
doPut(key, put.second.value, [ok = put.second.ok](bool result) { *ok = result; }, time_point::max(), true);
if (!put.second.refreshPutTimer) {
put.second.refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_);
put.second.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_);
}
put.second.refreshPutTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT
put.second.refreshPutTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT
- proxy::OP_MARGIN);
put.second.refreshPutTimer->async_wait(
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, put.first));
@@ -1277,10 +1275,10 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
auto vid = std::stoull(vidIt->second);
auto& put = search.puts.at(vid);
if (!put.refreshPutTimer)
put.refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
std::chrono::system_clock::now());
put.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
std::chrono::steady_clock::now());
else
put.refreshPutTimer->expires_at(std::chrono::system_clock::now());
put.refreshPutTimer->expires_at(std::chrono::steady_clock::now());
put.refreshPutTimer->async_wait(
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, vid));
ret = PushNotificationResult::PutRefresh;
@@ -1292,12 +1290,12 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
: PushNotificationResult::ListenRefresh;
}
} else {
auto key = InfoHash(notification.at("key"));
system_clock::time_point sendTime = system_clock::time_point::min();
try {
sendTime = system_clock::time_point(std::chrono::milliseconds(std::stoull(notification.at("t"))));
} catch (...) {
}
auto key = InfoHash(notification.at("key"));
auto& search = searches_.at(key);
auto expired = notification.find("exp");
for (auto& list : search.listeners) {
@@ -1310,25 +1308,13 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
if (expired == notification.end()) {
auto cb = list.second.cb;
auto oldValues = list.second.cache.getValues();
auto receivedIds = std::make_shared<std::set<Value::Id>>();
get(
key,
[cb, sendTime, receivedIds](const std::vector<Sp<Value>>& vals) {
for (const auto& v : vals)
receivedIds->insert(v->id);
return cb(vals, false, sendTime);
},
[cb, oldValues, sendTime, receivedIds](bool ok) {
if (!ok)
return;
[cb, sendTime](const std::vector<Sp<Value>>& vals) { return cb(vals, false, sendTime); },
[cb, oldValues, sendTime](bool /*ok*/) {
// Decrement old values refcount to expire values not
// present in the new list
std::vector<Sp<Value>> toExpire;
for (const auto& v : oldValues)
if (receivedIds->find(v->id) == receivedIds->end())
toExpire.emplace_back(v);
if (!toExpire.empty())
cb(toExpire, true, sendTime);
cb(oldValues, true, sendTime);
});
ret = PushNotificationResult::Values;
} else {
@@ -1392,9 +1378,9 @@ DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
header.method(restinio::http_method_subscribe());
header.request_target("/key/" + key.toString());
if (!listener.refreshSubscriberTimer) {
listener.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
listener.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
}
listener.refreshSubscriberTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN);
listener.refreshSubscriberTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN);
listener.refreshSubscriberTimer->async_wait(
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, opstate));
auto vcb = listener.cb;
+15 -28
View File
@@ -210,7 +210,7 @@ DhtProxyServer::PermanentPut::msgpack_unpack(const msgpack::object& o)
clientId = cid->as<std::string>();
}
if (auto exp = findMapValue(o, "exp"sv)) {
expiration = sys_clock::from_time_t(exp->as<time_t>());
expiration = from_time_t(exp->as<time_t>());
}
if (auto token = findMapValue(o, "token"sv)) {
pushToken = token->as<std::string>();
@@ -240,7 +240,7 @@ DhtProxyServer::Listener::msgpack_unpack(const msgpack::object& o)
clientId = cid->as<std::string>();
}
if (auto exp = findMapValue(o, "exp"sv)) {
expiration = sys_clock::from_time_t(exp->as<time_t>());
expiration = from_time_t(exp->as<time_t>());
}
if (auto sid = findMapValue(o, "sid"sv)) {
if (not sessionCtx)
@@ -414,7 +414,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
logger_->debug("[proxy:server] Loading {} persistent puts", puts_.size());
for (auto& put : puts_) {
for (auto& pput : put.second.puts) {
pput.second.expireTimer = std::make_unique<asio::system_timer>(io_context(),
pput.second.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
pput.second.expiration);
pput.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut,
this,
@@ -437,7 +437,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
}
return json;
};
pput.second.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
pput.second.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
pput.second.expiration
- proxy::OP_MARGIN);
pput.second.expireNotifyTimer->async_wait(
@@ -450,7 +450,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
pput.second.topic));
}
#endif
dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, dht::time_point::max(), true);
dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, time_point::max(), true);
}
}
} else {
@@ -487,7 +487,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
expired);
});
// expire notify
listener.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
listener.expiration
- proxy::OP_MARGIN);
auto jsonProvider = [infoHash = listeners.first.toString(),
@@ -509,7 +509,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
listener.type,
listener.topic));
// cancel push listen
listener.expireTimer = std::make_unique<asio::system_timer>(io_context(),
listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
listener.expiration);
listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
this,
@@ -571,18 +571,6 @@ DhtProxyServer::~DhtProxyServer()
ioContext_->stop();
if (serverThread_.joinable())
serverThread_.join();
{
// ~Request notifies DONE and its handler erases from requests_:
// detach pending push requests so nothing calls back into this object.
decltype(requests_) pending;
{
std::lock_guard l(requestLock_);
pending = std::move(requests_);
}
for (auto& r : pending)
r.second->add_on_state_change_callback({});
pending.clear();
}
if (logger_)
logger_->debug("[proxy:server] http server closed");
}
@@ -919,7 +907,6 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
Json::Value r;
auto* char_data = reinterpret_cast<const char*>(request->body().data());
auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)) {
auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
response.set_body(RESP_MSG_JSON_INCORRECT);
@@ -963,15 +950,15 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
}
auto& listener = *listIt;
// Expiration (wall-clock: persisted as epoch time, survives restarts)
auto timeout = sys_clock::now() + proxy::OP_TIMEOUT;
// Expiration
auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
listener.expiration = timeout;
listener.type = type;
listener.topic = topic;
if (listener.expireNotifyTimer)
listener.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
else
listener.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(), timeout - proxy::OP_MARGIN);
listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(), timeout - proxy::OP_MARGIN);
auto jsonProvider = [h = infoHash.toString(), clientId, sessionCtx = listener.sessionCtx]() {
Json::Value json;
json["timeout"] = h;
@@ -988,7 +975,7 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
listener.type,
listener.topic));
if (!listener.expireTimer)
listener.expireTimer = std::make_unique<asio::system_timer>(io_context(), timeout);
listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
else
listener.expireTimer->expires_at(timeout);
listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
@@ -1418,7 +1405,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
topic = pVal["topic"].asString();
}
std::lock_guard lock(lockSearchPuts_);
auto timeout = sys_clock::now() + proxy::OP_TIMEOUT;
auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
auto& sPuts = puts_[infoHash];
if (value->id == Value::INVALID_ID) {
for (auto& pp : sPuts.puts) {
@@ -1452,7 +1439,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
pput.expiration = timeout;
if (not pput.expireTimer) {
// cancel permanent put
pput.expireTimer = std::make_unique<asio::system_timer>(io_context(), timeout);
pput.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
if (not pushToken.empty()) {
pput.pushToken = pushToken;
@@ -1491,7 +1478,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
return json;
};
if (!pput.expireNotifyTimer)
pput.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
pput.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
timeout - proxy::OP_MARGIN);
else
pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
@@ -1520,7 +1507,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
response.done();
}
},
dht::time_point::max(),
time_point::max(),
permanent);
return restinio::request_handling_status_t::accepted;
} else {
+14 -34
View File
@@ -21,16 +21,6 @@ namespace dht {
using namespace std::literals;
constexpr auto PEER_DISCOVERY_DHT_SERVICE = "dht"sv;
struct NodeInsertionPack
{
dht::InfoHash nodeId;
in_port_t port;
dht::NetId net;
MSGPACK_DEFINE(nodeId, port, net)
};
DhtRunner::DhtRunner()
: dht_()
{
@@ -247,7 +237,7 @@ DhtRunner::run(const Config& config, Context&& context)
if (config.peer_discovery or config.peer_publish) {
#ifdef OPENDHT_PEER_DISCOVERY
peerDiscovery_ = context.peerDiscovery ? std::move(context.peerDiscovery)
: std::make_shared<PeerDiscovery>();
: std::make_shared<PeerDiscovery>(nullptr, context.logger);
#else
std::cerr << "Peer discovery requested but OpenDHT built without peer discovery support." << std::endl;
#endif
@@ -256,32 +246,22 @@ DhtRunner::run(const Config& config, Context&& context)
#ifdef OPENDHT_PEER_DISCOVERY
auto netId = config.dht_config.node_config.network;
if (config.peer_discovery) {
peerDiscovery_->startDiscovery<NodeInsertionPack>(PEER_DISCOVERY_DHT_SERVICE,
[this, netId](NodeInsertionPack&& v, SockAddr&& addr) {
addr.setPort(v.port);
if (v.nodeId != dht_->getNodeId() && netId == v.net) {
bootstrap(v.nodeId, addr);
}
});
peerDiscovery_->startDiscovery(netId, [this](const InfoHash& nodeId, SockAddr&& address) {
if (nodeId != dht_->getNodeId())
bootstrap(nodeId, address);
});
}
if (config.peer_publish) {
msgpack::sbuffer sbuf_node;
NodeInsertionPack adc;
adc.net = netId;
adc.nodeId = dht_->getNodeId();
if (auto socket = dht_->getSocket()) {
// IPv4
if (const auto& bound4 = socket->getBoundRef(AF_INET)) {
adc.port = bound4.getPort();
msgpack::pack(sbuf_node, adc);
peerDiscovery_->startPublish(AF_INET, PEER_DISCOVERY_DHT_SERVICE, sbuf_node);
}
// IPv6
if (const auto& bound6 = socket->getBoundRef(AF_INET6)) {
adc.port = bound6.getPort();
sbuf_node.clear();
msgpack::pack(sbuf_node, adc);
peerDiscovery_->startPublish(AF_INET6, PEER_DISCOVERY_DHT_SERVICE, sbuf_node);
const auto& bound4 = socket->getBoundRef(AF_INET);
const auto& bound6 = socket->getBoundRef(AF_INET6);
const auto port4 = bound4 ? bound4.getPort() : 0;
const auto port6 = bound6 ? bound6.getPort() : 0;
if (port4 and port6 and port4 != port6) {
if (logger_)
logger_->error("Unable to publish peer discovery with different IPv4 and IPv6 ports");
} else if (const auto port = port4 ? port4 : port6) {
peerDiscovery_->startPublish(dht_->getNodeId(), netId, port);
}
}
}
+4 -26
View File
@@ -233,32 +233,11 @@ Connection::Connection(asio::io_context& ctx,
Connection::~Connection()
{
// No asynchronous operation can be pending here: their handlers keep
// the connection alive (see wrapCallback), so closing synchronously
// is safe regardless of the calling thread.
doClose();
close();
}
void
Connection::close()
{
// asio sockets are not thread-safe. Closing from a thread other than the
// one running the io_context races with in-flight operations on the same
// socket (e.g. async_connect closing and reopening it to try the next
// endpoint), corrupting the reactor's descriptor state.
// Always perform the close on the io_context thread.
if (ctx_.get_executor().running_in_this_thread()) {
doClose();
return;
}
asio::post(ctx_, [w = weak_from_this()] {
if (auto sthis = w.lock())
sthis->doClose();
});
}
void
Connection::doClose()
{
std::lock_guard lock(mutex_);
asio::error_code ec;
@@ -652,13 +631,12 @@ Connection::async_handshake(HandlerCb cb)
{
std::lock_guard lock(mutex_);
if (ssl_socket_) {
// Keep the connection alive while the handshake is in flight, like
// every other asynchronous operation (see wrapCallback).
std::weak_ptr<Connection> wthis = shared_from_this();
ssl_socket_
->async_handshake(asio::ssl::stream<asio::ip::tcp::socket>::client, [sthis = shared_from_this(), cb](const asio::error_code& ec) {
->async_handshake(asio::ssl::stream<asio::ip::tcp::socket>::client, [wthis, cb](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted)
return;
{
if (auto sthis = wthis.lock()) {
auto& this_ = *sthis;
auto verify_ec = SSL_get_verify_result(this_.ssl_socket_->asio_ssl_stream().native_handle());
if (this_.logger_) {
+18 -24
View File
@@ -12,20 +12,18 @@ OpValueCache::onValuesAdded(const std::vector<Sp<Value>>& vals, const system_clo
std::vector<Sp<Value>> newValues;
for (const auto& v : vals) {
auto viop = values.emplace(v->id, v);
auto& stored = viop.first->second;
if (viop.second) {
newValues.emplace_back(v);
} else if (*stored.data != *v) {
if (v->seq <= stored.data->seq)
continue; // older version from a lagging source
// Edited: only the reporting source is known to hold this version
stored.data = v;
stored.refCount = 1;
newValues.emplace_back(v);
} else if (countSources) {
stored.refCount++;
} else if (*viop.first->second.data != *v) {
// Special case for edition
if (v->seq > viop.first->second.data->seq) {
viop.first->second.data = v;
newValues.emplace_back(v);
}
} else {
viop.first->second.refCount++;
}
stored.updated = t;
viop.first->second.updated = t;
}
return newValues.empty() ? true : callback(newValues, false);
}
@@ -39,8 +37,6 @@ OpValueCache::onValuesExpired(const std::vector<Sp<Value>>& vals, const system_c
if (vit != values.end()) {
if (vit->second.updated > t)
continue;
if (v->seq < vit->second.data->seq)
continue; // source held an older version, not counted
vit->second.updated = t;
vit->second.refCount--;
@@ -178,7 +174,7 @@ SearchCache::listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filt
auto op = getOp(q);
if (op == ops.end()) {
// New query
op = ops.emplace(q, std::make_unique<OpCache>(countSources_)).first;
op = ops.emplace(q, std::make_unique<OpCache>()).first;
auto& cache = *op->second;
cache.searchToken = onListen(
q,
@@ -244,17 +240,15 @@ SearchCache::expire(const time_point& now, const std::function<void(size_t)>& on
bool
SearchCache::get(const Value::Filter& f, const Sp<Query>& q, const GetCallback& gcb, const DoneCallback& dcb) const
{
if (not gcb)
return false;
auto op = getOp(q);
if (op == ops.end() or not op->second->isSynced())
return false;
auto vals = op->second->get(f);
if (not vals.empty())
gcb(vals); // a consumer stopping early is not a failure
if (dcb)
dcb(true, {});
return true;
if (op != ops.end()) {
auto vals = op->second->get(f);
if ((not vals.empty() and not gcb(vals)) or op->second->isSynced()) {
dcb(true, {});
return true;
}
}
return false;
}
std::vector<Sp<Value>>
+12 -28
View File
@@ -21,29 +21,19 @@ struct OpCacheValueStorage
class OpValueCache
{
public:
/**
* @param countSources When true, a value is kept until every source
* that reported it has expired it (per-node caches,
* IPv4/IPv6 searches, local storage). When false,
* re-deliveries of the same value are not counted
* and a single expiration removes it (single
* logical source, e.g. a proxy server).
*/
OpValueCache(ValueCallback&& cb, bool countSources = true) noexcept
OpValueCache(ValueCallback&& cb) noexcept
: callback(std::forward<ValueCallback>(cb))
, countSources(countSources)
{}
explicit OpValueCache(OpValueCache&& o) noexcept
: values(std::move(o.values))
, callback(std::move(o.callback))
, countSources(o.countSources)
{
o.callback = {};
}
static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel, bool countSources = true)
static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel)
{
return [cache = std::make_shared<OpValueCache>(std::forward<ValueCallback>(cb), countSources),
return [cache = std::make_shared<OpValueCache>(std::forward<ValueCallback>(cb)),
onCancel = std::move(onCancel)](const std::vector<Sp<Value>>& vals, bool expired) {
auto ret = cache->onValue(vals, expired);
if (not ret)
@@ -99,22 +89,19 @@ private:
size_t syncedNodes {0};
std::map<Value::Id, OpCacheValueStorage> values {};
ValueCallback callback;
bool countSources {true};
};
class OpCache
{
public:
explicit OpCache(bool countSources = true)
: cache(
[this](const std::vector<Sp<Value>>& vals, bool expired) {
if (expired)
onValuesExpired(vals);
else
onValuesAdded(vals);
return true;
},
countSources)
OpCache()
: cache([this](const std::vector<Sp<Value>>& vals, bool expired) {
if (expired)
onValuesExpired(vals);
else
onValuesAdded(vals);
return true;
})
{}
bool onValue(const std::vector<Sp<Value>>& vals, bool expired) { return cache.onValue(vals, expired); }
@@ -169,9 +156,7 @@ private:
class SearchCache
{
public:
explicit SearchCache(bool countSources = true)
: countSources_(countSources)
{}
SearchCache() {}
SearchCache(SearchCache&&) = default;
using OnListen = std::function<size_t(Sp<Query>, ValueCallback, SyncCallback)>;
@@ -207,7 +192,6 @@ private:
size_t nextToken_ {1};
time_point nextExpiration_ {time_point::max()};
bool countSources_ {true};
};
} // namespace dht
-474
View File
@@ -1,474 +0,0 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "peer_discovery.h"
#include "network_utils.h"
#include "utils.h"
#ifdef __ANDROID__
#include "compat/getif_workaround_android.h"
#endif
#include <asio.hpp>
using namespace std::literals;
namespace dht {
// Organization-local Scope multicast
constexpr char MULTICAST_ADDRESS_IPV4[] = "239.192.0.1";
constexpr char MULTICAST_ADDRESS_IPV6[] = "ff08::101";
class PeerDiscovery::DomainPeerDiscovery
{
public:
DomainPeerDiscovery(asio::ip::udp domain,
in_port_t port,
Sp<asio::io_context> ioContext = {},
Sp<Logger> logger = {});
~DomainPeerDiscovery();
void startDiscovery(std::string_view type, ServiceDiscoveredCallback callback);
void startPublish(std::string_view type, const msgpack::sbuffer& pack_buf);
void stop();
bool stopDiscovery(std::string_view type);
bool stopPublish(std::string_view type);
void connectivityChanged();
void stopConnectivityChanged();
private:
Sp<Logger> logger_;
// dmtx_ for callbackmap_ and drunning_ (write)
std::mutex dmtx_;
// mtx_ for messages_ and lrunning (listen)
std::mutex mtx_;
std::shared_ptr<asio::io_context> ioContext_;
static constexpr dht::duration PeerDiscovery_PERIOD_MAX {std::chrono::minutes(1)};
static constexpr std::chrono::seconds PeerDiscovery_PERIOD {10};
asio::steady_timer peerDiscoveryTimer;
std::chrono::steady_clock::duration peerDiscovery_period {PeerDiscovery_PERIOD};
asio::ip::udp::socket sockFd_;
asio::ip::udp::endpoint sockAddrSend_;
std::array<char, 64 * 1024> receiveBuf_;
asio::ip::udp::endpoint receiveFrom_;
msgpack::sbuffer sbuf_;
std::map<std::string, msgpack::sbuffer, std::less<>> messages_;
std::map<std::string, ServiceDiscoveredCallback, std::less<>> callbackmap_;
bool lrunning_ {false};
bool drunning_ {false};
void loopListener();
void query(const asio::ip::udp::endpoint& peer);
void reloadMessages();
void stopDiscovery();
void stopPublish();
void publish(const asio::ip::udp::endpoint& peer);
void reDiscover();
};
PeerDiscovery::DomainPeerDiscovery::DomainPeerDiscovery(asio::ip::udp domain,
in_port_t port,
Sp<asio::io_context> ioContext,
Sp<Logger> logger)
: logger_(logger)
, ioContext_(ioContext)
, peerDiscoveryTimer(*ioContext_)
, sockFd_(*ioContext_, domain)
, sockAddrSend_(asio::ip::make_address(domain.family() == AF_INET ? MULTICAST_ADDRESS_IPV4 : MULTICAST_ADDRESS_IPV6),
port)
{
try {
sockFd_.set_option(asio::ip::multicast::join_group(sockAddrSend_.address()));
sockFd_.set_option(asio::ip::udp::socket::reuse_address(true));
sockFd_.bind({domain, port});
} catch (const std::exception& e) {
#ifdef __ANDROID__
if (domain.family() == AF_INET && strcmp(e.what(), "set_option: No such device") == 0) {
try {
sockFd_.set_option(asio::ip::udp::socket::reuse_address(true));
auto mc_interface = workaround::get_interface();
sockFd_.set_option(asio::ip::multicast::outbound_interface(mc_interface));
sockFd_.set_option(asio::ip::multicast::join_group(sockAddrSend_.address().to_v4(), mc_interface));
sockFd_.bind({domain, port});
} catch (const std::exception& e) {
if (logger_)
logger_->error("Unable to start peer discovery using android workaround: {}", e.what());
}
} else
#endif
if (logger_)
logger_->error("Unable to start peer discovery for {}: {}",
domain.family() == AF_INET ? "IPv4" : "IPv6",
e.what());
}
}
PeerDiscovery::DomainPeerDiscovery::~DomainPeerDiscovery()
{
stop();
sockFd_.close();
}
void
PeerDiscovery::DomainPeerDiscovery::startDiscovery(std::string_view type, ServiceDiscoveredCallback callback)
{
std::lock_guard lck(dmtx_);
callbackmap_[std::string(type)] = callback;
if (not drunning_) {
drunning_ = true;
asio::post(*ioContext_, [this]() {
loopListener();
query(sockAddrSend_);
});
}
}
void
PeerDiscovery::DomainPeerDiscovery::loopListener()
{
std::lock_guard lck(dmtx_);
if (not drunning_)
return;
sockFd_.async_receive_from(asio::buffer(receiveBuf_),
receiveFrom_,
[this](const asio::error_code& error, size_t bytes) {
if (error == asio::error::operation_aborted)
return;
if (error) {
if (logger_)
logger_->error("Error receiving message: {}", error.message());
}
try {
auto rcv = msgpack::unpack(receiveBuf_.data(), bytes);
msgpack::object obj = rcv.get();
if (obj.type == msgpack::type::STR) {
if (lrunning_ and obj.as<std::string_view>() == "q"sv)
publish(receiveFrom_);
} else if (obj.type == msgpack::type::MAP) {
for (unsigned i = 0; i < obj.via.map.size; i++) {
auto& o = obj.via.map.ptr[i];
if (o.key.type != msgpack::type::STR)
continue;
ServiceDiscoveredCallback cb;
{
std::lock_guard lck(dmtx_);
if (drunning_) {
auto callback = callbackmap_.find(o.key.as<std::string_view>());
if (callback != callbackmap_.end())
cb = callback->second;
} else
return;
}
if (cb)
cb(std::move(o.val),
SockAddr {receiveFrom_.data(), (socklen_t) receiveFrom_.size()});
}
} else {
throw msgpack::type_error {};
}
} catch (const std::exception& e) {
if (logger_)
logger_->error("Error receiving packet: {}", e.what());
}
loopListener();
});
}
void
PeerDiscovery::DomainPeerDiscovery::query(const asio::ip::udp::endpoint& peer)
{
std::lock_guard lck(dmtx_);
if (not drunning_)
return;
msgpack::sbuffer pbuf_request;
msgpack::pack(pbuf_request, "q");
sockFd_.async_send_to(asio::buffer(pbuf_request.data(), pbuf_request.size()),
peer,
[logger = logger_, peer](const asio::error_code& ec, size_t) {
if (ec and (ec != asio::error::operation_aborted) and logger)
logger->warn("Error sending packet to: {} with err: {}",
peer.address().to_string(),
ec.message());
});
}
void
PeerDiscovery::DomainPeerDiscovery::publish(const asio::ip::udp::endpoint& peer)
{
std::lock_guard lck(mtx_);
if (not lrunning_)
return;
sockFd_.async_send_to(asio::buffer((const void*) sbuf_.data(), sbuf_.size()),
peer,
[logger = logger_, peer](const asio::error_code& ec, size_t) {
if (ec and (ec != asio::error::operation_aborted) and logger)
logger->warn("Error sending packet to: {} with err: {}",
peer.address().to_string(),
ec.message());
});
}
void
PeerDiscovery::DomainPeerDiscovery::startPublish(std::string_view type, const msgpack::sbuffer& pack_buf)
{
msgpack::sbuffer pack_buf_c(pack_buf.size());
pack_buf_c.write(pack_buf.data(), pack_buf.size());
std::lock_guard lck(mtx_);
messages_[std::string(type)] = std::move(pack_buf_c);
reloadMessages();
lrunning_ = true;
asio::post(*ioContext_, [this]() { publish(sockAddrSend_); });
}
bool
PeerDiscovery::DomainPeerDiscovery::stopDiscovery(std::string_view type)
{
std::lock_guard lck(dmtx_);
auto it = callbackmap_.find(type);
if (it != callbackmap_.end()) {
callbackmap_.erase(it);
if (callbackmap_.empty())
stopDiscovery();
return true;
}
return false;
}
bool
PeerDiscovery::DomainPeerDiscovery::stopPublish(std::string_view type)
{
std::lock_guard lck(mtx_);
auto it = messages_.find(type);
if (it != messages_.end()) {
messages_.erase(it);
if (messages_.empty())
stopPublish();
else
reloadMessages();
return true;
}
return false;
}
void
PeerDiscovery::DomainPeerDiscovery::stopDiscovery()
{
drunning_ = false;
}
void
PeerDiscovery::DomainPeerDiscovery::stopPublish()
{
lrunning_ = false;
}
void
PeerDiscovery::DomainPeerDiscovery::stop()
{
{
std::lock_guard lck(dmtx_);
stopDiscovery();
}
{
std::lock_guard lck(mtx_);
stopPublish();
}
}
void
PeerDiscovery::DomainPeerDiscovery::reloadMessages()
{
sbuf_.clear();
msgpack::packer<msgpack::sbuffer> pk(&sbuf_);
pk.pack_map(messages_.size());
for (const auto& m : messages_) {
pk.pack(m.first);
sbuf_.write(m.second.data(), m.second.size());
}
}
void
PeerDiscovery::DomainPeerDiscovery::reDiscover()
{
asio::error_code ec;
sockFd_.set_option(asio::ip::multicast::join_group(sockAddrSend_.address()), ec);
if (ec and logger_)
logger_->warn("Unable to multicast on {}: {}", sockAddrSend_.address().to_string(), ec.message());
query(sockAddrSend_);
}
void
PeerDiscovery::DomainPeerDiscovery::connectivityChanged()
{
asio::post(*ioContext_, [this]() {
reDiscover();
publish(sockAddrSend_);
});
if (logger_)
logger_->debug("PeerDiscovery: connectivity changed");
if (peerDiscovery_period == PeerDiscovery_PERIOD_MAX) {
peerDiscovery_period = PeerDiscovery_PERIOD;
} else {
peerDiscoveryTimer.expires_after(peerDiscovery_period);
peerDiscoveryTimer.async_wait([this](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted)
return;
connectivityChanged();
});
peerDiscovery_period = std::min(peerDiscovery_period * 2, PeerDiscovery_PERIOD_MAX);
}
}
void
PeerDiscovery::DomainPeerDiscovery::stopConnectivityChanged()
{
peerDiscoveryTimer.cancel();
peerDiscovery_period = PeerDiscovery_PERIOD;
}
PeerDiscovery::PeerDiscovery(in_port_t port, Sp<asio::io_context> ioContext, Sp<Logger> logger)
{
if (not ioContext) {
ioContext = std::make_shared<asio::io_context>();
ioContext_ = ioContext;
ioRunnner_ = std::thread([logger, ioContext] {
try {
if (logger)
logger->d("[peerdiscovery] starting io_context");
auto work = asio::make_work_guard(*ioContext);
ioContext->run();
if (logger)
logger->d("[peerdiscovery] io_context stopped");
} catch (const std::exception& ex) {
if (logger)
logger->e("[peerdiscovery] run error: %s", ex.what());
}
});
}
try {
peerDiscovery4_.reset(new DomainPeerDiscovery(asio::ip::udp::v4(), port, ioContext, logger));
} catch (const std::exception& e) {
if (logger)
logger->e("[peerdiscovery] Unable to start IPv4: %s", e.what());
}
try {
peerDiscovery6_.reset(new DomainPeerDiscovery(asio::ip::udp::v6(), port, ioContext, logger));
} catch (const std::exception& e) {
if (logger)
logger->e("[peerdiscovery] Unable to start IPv6: %s", e.what());
}
}
PeerDiscovery::~PeerDiscovery()
{
stop();
if (ioContext_)
ioContext_->stop();
if (ioRunnner_.joinable())
ioRunnner_.join();
}
void
PeerDiscovery::startDiscovery(std::string_view type, ServiceDiscoveredCallback callback)
{
if (peerDiscovery4_)
peerDiscovery4_->startDiscovery(type, callback);
if (peerDiscovery6_)
peerDiscovery6_->startDiscovery(type, callback);
}
void
PeerDiscovery::startPublish(std::string_view type, const msgpack::sbuffer& pack_buf)
{
if (peerDiscovery4_)
peerDiscovery4_->startPublish(type, pack_buf);
if (peerDiscovery6_)
peerDiscovery6_->startPublish(type, pack_buf);
}
void
PeerDiscovery::startPublish(sa_family_t domain, std::string_view type, const msgpack::sbuffer& pack_buf)
{
if (domain == AF_INET) {
if (peerDiscovery4_)
peerDiscovery4_->startPublish(type, pack_buf);
} else if (domain == AF_INET6) {
if (peerDiscovery6_)
peerDiscovery6_->startPublish(type, pack_buf);
}
}
void
PeerDiscovery::stop()
{
if (peerDiscovery4_)
peerDiscovery4_->stop();
if (peerDiscovery6_)
peerDiscovery6_->stop();
}
bool
PeerDiscovery::stopDiscovery(std::string_view type)
{
bool stopped4 = peerDiscovery4_ and peerDiscovery4_->stopDiscovery(type);
bool stopped6 = peerDiscovery6_ and peerDiscovery6_->stopDiscovery(type);
return stopped4 or stopped6;
}
bool
PeerDiscovery::stopPublish(std::string_view type)
{
bool stopped4 = peerDiscovery4_ and peerDiscovery4_->stopPublish(type);
bool stopped6 = peerDiscovery6_ and peerDiscovery6_->stopPublish(type);
return stopped4 or stopped6;
}
bool
PeerDiscovery::stopPublish(sa_family_t domain, std::string_view type)
{
if (domain == AF_INET) {
return peerDiscovery4_ and peerDiscovery4_->stopPublish(type);
} else if (domain == AF_INET6) {
return peerDiscovery6_ and peerDiscovery6_->stopPublish(type);
}
return false;
}
void
PeerDiscovery::connectivityChanged()
{
if (peerDiscovery4_)
peerDiscovery4_->connectivityChanged();
if (peerDiscovery6_)
peerDiscovery6_->connectivityChanged();
}
void
PeerDiscovery::stopConnectivityChanged()
{
if (peerDiscovery4_)
peerDiscovery4_->stopConnectivityChanged();
if (peerDiscovery6_)
peerDiscovery6_->stopConnectivityChanged();
}
} /* namespace dht */
+504
View File
@@ -0,0 +1,504 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "dns_sd.h"
#include <algorithm>
#include <charconv>
#include <map>
namespace dht::mdns::dns_sd {
namespace {
constexpr std::string_view SERVICE_NAME = "_opendht._udp.local.";
constexpr std::string_view SERVICE_ENUMERATION_NAME = "_services._dns-sd._udp.local.";
constexpr std::string_view SERVICE_SUFFIX = "._opendht._udp.local.";
constexpr std::string_view HOST_SUFFIX = ".local.";
std::vector<uint8_t>
txtString(std::string value)
{
return {value.begin(), value.end()};
}
std::string
asciiLower(std::string_view value)
{
std::string lower;
lower.reserve(value.size());
for (const auto character : value) {
if (character >= 'A' and character <= 'Z')
lower.push_back(static_cast<char>(character + ('a' - 'A')));
else
lower.push_back(character);
}
return lower;
}
void
validateService(const Service& service)
{
if (not service.nodeId)
throw ParseError("DNS-SD service requires a node ID");
if (service.port == 0)
throw ParseError("DNS-SD service requires a port");
if (service.addresses.empty())
throw ParseError("DNS-SD service requires an address");
}
Message
makeAdvertisement(const Service& service, uint32_t serviceTtl, uint32_t hostTtl)
{
validateService(service);
const auto instance = instanceName(service.nodeId);
const auto host = hostName(service.nodeId);
Message message;
message.response = true;
message.authoritative = true;
message.answers.push_back({serviceName(), Type::PTR, CLASS_IN, false, serviceTtl, PtrData {instance}});
message.additionals.push_back({
instance, Type::SRV, CLASS_IN, true, hostTtl, SrvData {0, 0, service.port, host}
});
TxtData txt;
txt.strings.emplace_back(txtString("txtvers=1"));
txt.strings.emplace_back(txtString("id=" + service.nodeId.toString()));
txt.strings.emplace_back(txtString("net=" + std::to_string(service.network)));
message.additionals.push_back({instance, Type::TXT, CLASS_IN, true, serviceTtl, std::move(txt)});
for (const auto& address : service.addresses) {
std::visit(
[&](const auto& data) {
using Data = std::decay_t<decltype(data)>;
constexpr auto type = std::is_same_v<Data, AData> ? Type::A : Type::AAAA;
message.additionals.push_back({host, type, CLASS_IN, true, hostTtl, data});
},
address);
}
return message;
}
std::optional<InfoHash>
nodeIdFromInstance(const Name& instance)
{
const auto& labels = instance.labels();
const auto service = serviceName();
const auto& serviceLabels = service.labels();
if (labels.size() != serviceLabels.size() + 1
or not std::equal(labels.begin() + 1, labels.end(), serviceLabels.begin(), serviceLabels.end()))
return {};
if (labels.front().size() != 40)
return {};
const InfoHash nodeId {labels.front()};
if (not nodeId)
return {};
return nodeId;
}
struct TxtValues
{
InfoHash nodeId;
NetId network {};
};
std::optional<TxtValues>
parseTxt(const TxtData& txt)
{
std::map<std::string, std::string> values;
for (const auto& raw : txt.strings) {
const std::string_view string {reinterpret_cast<const char*>(raw.data()), raw.size()};
const auto separator = string.find('=');
if (separator == std::string_view::npos or separator == 0)
continue;
const auto keyView = string.substr(0, separator);
if (std::any_of(keyView.begin(), keyView.end(), [](char value) {
return value < 0x20 or value > 0x7e or value == '=';
}))
continue;
const auto key = asciiLower(keyView);
if ((key == "txtvers" or key == "id" or key == "net") and values.count(key))
return {};
values.emplace(key, std::string(string.substr(separator + 1)));
}
if (values.size() < 3 or values["txtvers"] != "1" or values["id"].size() != 40)
return {};
TxtValues parsed;
parsed.nodeId = InfoHash {values["id"]};
if (not parsed.nodeId)
return {};
const auto& network = values["net"];
const auto result = std::from_chars(network.data(), network.data() + network.size(), parsed.network);
if (result.ec != std::errc {} or result.ptr != network.data() + network.size())
return {};
return parsed;
}
template<typename Callback>
void
forEachRecord(const Message& message, Callback&& callback)
{
for (const auto& record : message.answers)
callback(record);
for (const auto& record : message.authorities)
callback(record);
for (const auto& record : message.additionals)
callback(record);
}
bool
matches(const Question& question, const ResourceRecord& record)
{
return question.name == record.name and (question.type == Type::ANY or question.type == record.type)
and (question.classCode == CLASS_ANY or question.classCode == record.classCode);
}
bool
sameRecord(const ResourceRecord& lhs, const ResourceRecord& rhs)
{
return lhs.name == rhs.name and lhs.type == rhs.type and lhs.classCode == rhs.classCode and lhs.data == rhs.data;
}
bool
isKnownAnswer(const Message& query, const ResourceRecord& record)
{
return std::any_of(query.answers.begin(), query.answers.end(), [&](const ResourceRecord& known) {
return sameRecord(known, record) and known.ttl > record.ttl / 2;
});
}
} // namespace
class Cache::Impl
{
public:
explicit Impl(Now now)
: now_(std::move(now))
{}
struct Entry
{
ResourceRecord record;
Clock::time_point expires;
};
std::vector<Service> update(const Message& message)
{
expire();
std::vector<const ResourceRecord*> records;
forEachRecord(message, [&](const ResourceRecord& record) { records.push_back(&record); });
std::vector<const ResourceRecord*> flushed;
for (const auto* record : records) {
if (not record->cacheFlush)
continue;
const auto alreadyFlushed = std::any_of(flushed.begin(), flushed.end(), [&](const ResourceRecord* other) {
return other->name == record->name and other->type == record->type
and other->classCode == record->classCode;
});
if (not alreadyFlushed) {
flushed.push_back(record);
entries_.erase(std::remove_if(entries_.begin(),
entries_.end(),
[&](const Entry& entry) {
return entry.record.name == record->name
and entry.record.type == record->type
and entry.record.classCode == record->classCode;
}),
entries_.end());
}
}
for (const auto* record : records) {
if (record->ttl == 0) {
entries_.erase(std::remove_if(entries_.begin(),
entries_.end(),
[&](const Entry& entry) { return sameRecord(entry.record, *record); }),
entries_.end());
continue;
}
const auto expires = now_() + std::chrono::seconds(record->ttl);
const auto existing = std::find_if(entries_.begin(), entries_.end(), [&](const Entry& entry) {
return sameRecord(entry.record, *record);
});
if (existing == entries_.end())
entries_.push_back({*record, expires});
else {
existing->record = *record;
existing->expires = expires;
}
}
auto current = collect();
std::vector<Service> discovered;
for (const auto& service : current) {
if (std::find_if(emitted_.begin(),
emitted_.end(),
[&](const Service& previous) { return equal(previous, service); })
== emitted_.end())
discovered.push_back(service);
}
emitted_ = std::move(current);
return discovered;
}
std::vector<Service> services()
{
expire();
auto current = collect();
emitted_ = current;
return current;
}
void clear()
{
entries_.clear();
emitted_.clear();
}
private:
static bool equal(const Service& lhs, const Service& rhs)
{
return lhs.nodeId == rhs.nodeId and lhs.network == rhs.network and lhs.port == rhs.port
and lhs.addresses == rhs.addresses;
}
void expire()
{
const auto now = now_();
entries_.erase(std::remove_if(entries_.begin(),
entries_.end(),
[&](const Entry& entry) { return entry.expires <= now; }),
entries_.end());
}
std::vector<Service> collect() const
{
std::vector<Service> result;
for (const auto& entry : entries_) {
if (entry.record.type != Type::PTR or entry.record.classCode != CLASS_IN
or entry.record.name != serviceName())
continue;
const auto* ptr = std::get_if<PtrData>(&entry.record.data);
if (not ptr)
continue;
Message message;
message.response = true;
message.answers.push_back(entry.record);
for (const auto& additional : entries_) {
if (&additional != &entry and additional.record.type != Type::PTR)
message.additionals.push_back(additional.record);
}
const auto service = resolve(message);
if (service and std::none_of(result.begin(), result.end(), [&](const Service& existing) {
return equal(existing, *service);
}))
result.push_back(*service);
}
return result;
}
Now now_;
std::vector<Entry> entries_;
std::vector<Service> emitted_;
};
Cache::Cache(Now now)
: impl_(std::make_unique<Impl>(std::move(now)))
{}
Cache::~Cache() = default;
Cache::Cache(Cache&&) noexcept = default;
Cache& Cache::operator=(Cache&&) noexcept = default;
std::vector<Service>
Cache::update(const Message& message)
{
return impl_->update(message);
}
std::vector<Service>
Cache::services()
{
return impl_->services();
}
void
Cache::clear()
{
impl_->clear();
}
Name
serviceName()
{
return Name::fromString(SERVICE_NAME);
}
Name
instanceName(const InfoHash& nodeId)
{
if (not nodeId)
throw ParseError("DNS-SD instance requires a node ID");
return Name::fromString(nodeId.toString() + std::string(SERVICE_SUFFIX));
}
Name
hostName(const InfoHash& nodeId)
{
if (not nodeId)
throw ParseError("mDNS host requires a node ID");
return Name::fromString(nodeId.toString() + std::string(HOST_SUFFIX));
}
Message
announcement(const Service& service)
{
return makeAdvertisement(service, SERVICE_RECORD_TTL, HOST_RECORD_TTL);
}
Message
goodbye(const Service& service)
{
return makeAdvertisement(service, 0, 0);
}
Message
browseQuery()
{
Message query;
query.questions.push_back({serviceName(), Type::PTR, CLASS_IN, false});
return query;
}
std::optional<Message>
respond(const Message& query, const Service& service)
{
if (query.response)
return {};
const auto advertisement = announcement(service);
Message response;
response.id = query.id;
response.response = true;
response.authoritative = true;
const ResourceRecord serviceType {
Name::fromString(SERVICE_ENUMERATION_NAME),
Type::PTR,
CLASS_IN,
false,
SERVICE_RECORD_TTL,
PtrData {serviceName()},
};
auto addMatching = [&](const ResourceRecord& record) {
const auto question = std::find_if(query.questions.begin(), query.questions.end(), [&](const Question& q) {
return matches(q, record);
});
if (question == query.questions.end() or (not question->unicastResponse and isKnownAnswer(query, record)))
return;
if (std::find_if(response.answers.begin(),
response.answers.end(),
[&](const ResourceRecord& answer) { return sameRecord(answer, record); })
== response.answers.end())
response.answers.push_back(record);
};
addMatching(serviceType);
for (const auto& record : advertisement.answers)
addMatching(record);
for (const auto& record : advertisement.additionals)
addMatching(record);
if (response.answers.empty())
return {};
const auto metaOnly = std::all_of(response.answers.begin(), response.answers.end(), [&](const auto& answer) {
return sameRecord(answer, serviceType);
});
if (metaOnly)
return response;
response.additionals = advertisement.additionals;
response.additionals.erase(std::remove_if(response.additionals.begin(),
response.additionals.end(),
[&](const ResourceRecord& record) {
return std::any_of(response.answers.begin(),
response.answers.end(),
[&](const ResourceRecord& answer) {
return sameRecord(answer, record);
});
}),
response.additionals.end());
return response;
}
std::optional<Service>
resolve(const Message& message)
{
std::vector<Name> instances;
forEachRecord(message, [&](const ResourceRecord& record) {
if (record.type == Type::PTR and record.classCode == CLASS_IN and record.name == serviceName()) {
if (const auto ptr = std::get_if<PtrData>(&record.data))
instances.push_back(ptr->name);
}
});
if (instances.size() != 1)
return {};
const auto nodeId = nodeIdFromInstance(instances.front());
if (not nodeId)
return {};
const auto expectedHost = hostName(*nodeId);
const SrvData* srv = nullptr;
const TxtData* txt = nullptr;
std::vector<AddressData> addresses;
auto duplicate = false;
forEachRecord(message, [&](const ResourceRecord& record) {
if (record.classCode != CLASS_IN)
return;
if (record.name == instances.front()) {
if (record.type == Type::SRV) {
if (srv)
duplicate = true;
else
srv = std::get_if<SrvData>(&record.data);
} else if (record.type == Type::TXT) {
if (txt)
duplicate = true;
else
txt = std::get_if<TxtData>(&record.data);
}
} else if (record.name == expectedHost) {
if (record.type == Type::A) {
if (const auto data = std::get_if<AData>(&record.data))
addresses.emplace_back(*data);
} else if (record.type == Type::AAAA) {
if (const auto data = std::get_if<AaaaData>(&record.data))
addresses.emplace_back(*data);
}
}
});
if (duplicate or not srv or not txt or srv->priority != 0 or srv->weight != 0 or srv->port == 0
or srv->target != expectedHost or addresses.empty())
return {};
const auto txtValues = parseTxt(*txt);
if (not txtValues or txtValues->nodeId != *nodeId)
return {};
Service service;
service.nodeId = *nodeId;
service.network = txtValues->network;
service.port = srv->port;
for (auto& address : addresses) {
if (std::find(service.addresses.begin(), service.addresses.end(), address) == service.addresses.end())
service.addresses.emplace_back(std::move(address));
}
return service;
}
} // namespace dht::mdns::dns_sd
+61
View File
@@ -0,0 +1,61 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include "mdns.h"
#include "opendht/infohash.h"
#include "opendht/utils.h"
#include <chrono>
#include <functional>
#include <memory>
#include <optional>
namespace dht::mdns::dns_sd {
constexpr uint32_t HOST_RECORD_TTL = 120;
constexpr uint32_t SERVICE_RECORD_TTL = 4500;
using AddressData = std::variant<AData, AaaaData>;
struct Service
{
InfoHash nodeId;
NetId network {};
in_port_t port {};
std::vector<AddressData> addresses;
};
class OPENDHT_PUBLIC Cache
{
public:
using Clock = std::chrono::steady_clock;
using Now = std::function<Clock::time_point()>;
explicit Cache(Now now = [] { return Clock::now(); });
~Cache();
Cache(Cache&&) noexcept;
Cache& operator=(Cache&&) noexcept;
std::vector<Service> update(const Message& message);
std::vector<Service> services();
void clear();
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
OPENDHT_PUBLIC Name serviceName();
OPENDHT_PUBLIC Name instanceName(const InfoHash& nodeId);
OPENDHT_PUBLIC Name hostName(const InfoHash& nodeId);
OPENDHT_PUBLIC Message announcement(const Service& service);
OPENDHT_PUBLIC Message goodbye(const Service& service);
OPENDHT_PUBLIC Message browseQuery();
OPENDHT_PUBLIC std::optional<Message> respond(const Message& query, const Service& service);
OPENDHT_PUBLIC std::optional<Service> resolve(const Message& message);
} // namespace dht::mdns::dns_sd
+563
View File
@@ -0,0 +1,563 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "mdns.h"
#include <algorithm>
#include <cctype>
#include <limits>
#include <map>
#include <unordered_map>
namespace dht::mdns {
namespace {
constexpr uint16_t FLAG_RESPONSE = 0x8000;
constexpr uint16_t FLAG_AUTHORITATIVE = 0x0400;
constexpr uint16_t FLAG_TRUNCATED = 0x0200;
constexpr uint16_t CLASS_TOP_BIT = 0x8000;
constexpr uint16_t CLASS_MASK = 0x7fff;
char
asciiLower(char value)
{
const auto byte = static_cast<unsigned char>(value);
return static_cast<char>(std::tolower(byte));
}
void
validateLabels(const std::vector<std::string>& labels)
{
size_t wireSize = 0;
for (const auto& label : labels) {
if (label.empty() or label.size() > 63)
throw ParseError("invalid DNS label length");
wireSize += label.size() + 1;
if (wireSize > 255)
throw ParseError("DNS name is too long");
}
}
std::string
suffixKey(const std::vector<std::string>& labels, size_t first)
{
std::string key;
for (auto index = first; index < labels.size(); ++index) {
key.push_back(static_cast<char>(labels[index].size()));
key.append(labels[index]);
}
key.push_back('\0');
return key;
}
class Writer
{
public:
void write8(uint8_t value)
{
ensure(1);
packet_.push_back(value);
}
void write16(uint16_t value)
{
ensure(2);
packet_.push_back(static_cast<uint8_t>(value >> 8));
packet_.push_back(static_cast<uint8_t>(value));
}
void write32(uint32_t value)
{
write16(static_cast<uint16_t>(value >> 16));
write16(static_cast<uint16_t>(value));
}
template<size_t Size>
void write(const std::array<uint8_t, Size>& bytes)
{
write(bytes.data(), bytes.size());
}
void write(const uint8_t* bytes, size_t size)
{
ensure(size);
packet_.insert(packet_.end(), bytes, bytes + size);
}
void writeName(const Name& name)
{
const auto& labels = name.labels();
for (size_t index = 0; index < labels.size(); ++index) {
const auto key = suffixKey(labels, index);
if (const auto found = suffixes_.find(key); found != suffixes_.end()) {
write16(static_cast<uint16_t>(0xc000 | found->second));
return;
}
if (packet_.size() < 0x4000)
suffixes_.emplace(key, static_cast<uint16_t>(packet_.size()));
write8(static_cast<uint8_t>(labels[index].size()));
write(reinterpret_cast<const uint8_t*>(labels[index].data()), labels[index].size());
}
write8(0);
}
size_t size() const noexcept { return packet_.size(); }
void patch16(size_t offset, uint16_t value)
{
if (offset + 2 > packet_.size())
throw ParseError("invalid DNS packet patch offset");
packet_[offset] = static_cast<uint8_t>(value >> 8);
packet_[offset + 1] = static_cast<uint8_t>(value);
}
Packet take() { return std::move(packet_); }
private:
void ensure(size_t additional) const
{
if (additional > MAX_PACKET_SIZE - packet_.size())
throw ParseError("mDNS packet exceeds maximum size");
}
Packet packet_;
std::unordered_map<std::string, uint16_t> suffixes_;
};
class Reader
{
public:
Reader(const uint8_t* packet, size_t size)
: packet_(packet)
, size_(size)
{}
uint8_t read8()
{
require(1);
return packet_[position_++];
}
uint16_t read16()
{
const auto high = read8();
const auto low = read8();
return static_cast<uint16_t>((high << 8) | low);
}
uint32_t read32() { return (static_cast<uint32_t>(read16()) << 16) | read16(); }
Name readName()
{
std::vector<std::string> labels;
std::vector<size_t> visited;
auto cursor = position_;
auto jumped = false;
size_t wireSize = 0;
while (true) {
if (cursor >= size_)
throw ParseError("truncated DNS name");
if (std::find(visited.begin(), visited.end(), cursor) != visited.end())
throw ParseError("DNS compression pointer loop");
visited.push_back(cursor);
const auto length = packet_[cursor];
if ((length & 0xc0) == 0xc0) {
if (cursor + 1 >= size_)
throw ParseError("truncated DNS compression pointer");
const auto pointer = static_cast<size_t>(((length & 0x3f) << 8) | packet_[cursor + 1]);
if (pointer >= cursor)
throw ParseError("DNS compression pointer does not point backwards");
if (not jumped)
position_ = cursor + 2;
cursor = pointer;
jumped = true;
continue;
}
if (length & 0xc0)
throw ParseError("invalid DNS label prefix");
++cursor;
if (length == 0) {
if (not jumped)
position_ = cursor;
break;
}
if (length > 63 or length > size_ - cursor)
throw ParseError("invalid or truncated DNS label");
wireSize += length + 1;
if (wireSize > 255)
throw ParseError("DNS name is too long");
labels.emplace_back(reinterpret_cast<const char*>(packet_ + cursor), length);
cursor += length;
if (not jumped)
position_ = cursor;
}
return Name {std::move(labels)};
}
std::vector<uint8_t> readBytes(size_t size)
{
require(size);
std::vector<uint8_t> bytes(packet_ + position_, packet_ + position_ + size);
position_ += size;
return bytes;
}
size_t position() const noexcept { return position_; }
void setPosition(size_t position)
{
if (position > size_)
throw ParseError("invalid DNS packet position");
position_ = position;
}
private:
void require(size_t count) const
{
if (count > size_ - position_)
throw ParseError("truncated DNS packet");
}
const uint8_t* packet_;
size_t size_;
size_t position_ {};
};
uint16_t
checkedCount(size_t count)
{
if (count > std::numeric_limits<uint16_t>::max())
throw ParseError("too many DNS records");
return static_cast<uint16_t>(count);
}
void
encodeQuestion(Writer& writer, const Question& question)
{
writer.writeName(question.name);
writer.write16(static_cast<uint16_t>(question.type));
writer.write16(static_cast<uint16_t>(question.classCode | (question.unicastResponse ? CLASS_TOP_BIT : 0)));
}
void
encodeNsec(Writer& writer, const NsecData& nsec)
{
writer.writeName(nsec.nextDomain);
std::map<uint8_t, std::array<uint8_t, 32>> windows;
std::map<uint8_t, uint8_t> lengths;
for (const auto type : nsec.types) {
const auto value = static_cast<uint16_t>(type);
const auto window = static_cast<uint8_t>(value >> 8);
const auto low = static_cast<uint8_t>(value);
const auto byte = static_cast<uint8_t>(low / 8);
const auto bit = static_cast<uint8_t>(low % 8);
windows[window][byte] |= static_cast<uint8_t>(1u << (7 - bit));
lengths[window] = std::max<uint8_t>(lengths[window], byte + 1);
}
for (const auto& [window, bitmap] : windows) {
writer.write8(window);
writer.write8(lengths[window]);
writer.write(bitmap.data(), lengths[window]);
}
}
void
encodeRecord(Writer& writer, const ResourceRecord& record)
{
writer.writeName(record.name);
writer.write16(static_cast<uint16_t>(record.type));
writer.write16(static_cast<uint16_t>(record.classCode | (record.cacheFlush ? CLASS_TOP_BIT : 0)));
writer.write32(record.ttl);
const auto lengthOffset = writer.size();
writer.write16(0);
const auto dataOffset = writer.size();
switch (record.type) {
case Type::A:
writer.write(std::get<AData>(record.data).address);
break;
case Type::AAAA:
writer.write(std::get<AaaaData>(record.data).address);
break;
case Type::PTR:
writer.writeName(std::get<PtrData>(record.data).name);
break;
case Type::TXT:
for (const auto& string : std::get<TxtData>(record.data).strings) {
if (string.size() > 255)
throw ParseError("DNS TXT string is too long");
writer.write8(static_cast<uint8_t>(string.size()));
writer.write(string.data(), string.size());
}
break;
case Type::SRV: {
const auto& srv = std::get<SrvData>(record.data);
writer.write16(srv.priority);
writer.write16(srv.weight);
writer.write16(srv.port);
writer.writeName(srv.target);
break;
}
case Type::NSEC:
encodeNsec(writer, std::get<NsecData>(record.data));
break;
default: {
const auto& bytes = std::get<UnknownData>(record.data).bytes;
writer.write(bytes.data(), bytes.size());
break;
}
}
const auto dataSize = writer.size() - dataOffset;
if (dataSize > std::numeric_limits<uint16_t>::max())
throw ParseError("DNS record data is too large");
writer.patch16(lengthOffset, static_cast<uint16_t>(dataSize));
}
Question
decodeQuestion(Reader& reader)
{
Question question;
question.name = reader.readName();
question.type = static_cast<Type>(reader.read16());
const auto classField = reader.read16();
question.classCode = classField & CLASS_MASK;
question.unicastResponse = classField & CLASS_TOP_BIT;
return question;
}
NsecData
decodeNsec(Reader& reader, size_t end)
{
NsecData nsec;
nsec.nextDomain = reader.readName();
while (reader.position() < end) {
const auto window = reader.read8();
const auto length = reader.read8();
if (length == 0 or length > 32 or length > end - reader.position())
throw ParseError("invalid DNS NSEC bitmap");
const auto bitmap = reader.readBytes(length);
for (size_t byte = 0; byte < bitmap.size(); ++byte) {
for (uint8_t bit = 0; bit < 8; ++bit) {
if (bitmap[byte] & (1u << (7 - bit))) {
const auto value = static_cast<uint16_t>((window << 8) | (byte * 8 + bit));
nsec.types.push_back(static_cast<Type>(value));
}
}
}
}
return nsec;
}
ResourceRecord
decodeRecord(Reader& reader)
{
ResourceRecord record;
record.name = reader.readName();
record.type = static_cast<Type>(reader.read16());
const auto classField = reader.read16();
record.classCode = classField & CLASS_MASK;
record.cacheFlush = classField & CLASS_TOP_BIT;
record.ttl = reader.read32();
const auto dataLength = reader.read16();
const auto dataStart = reader.position();
const auto dataEnd = dataStart + dataLength;
if (dataEnd < dataStart)
throw ParseError("invalid DNS record length");
switch (record.type) {
case Type::A: {
if (dataLength != 4)
throw ParseError("invalid DNS A record length");
AData data;
const auto bytes = reader.readBytes(data.address.size());
std::copy(bytes.begin(), bytes.end(), data.address.begin());
record.data = data;
break;
}
case Type::AAAA: {
if (dataLength != 16)
throw ParseError("invalid DNS AAAA record length");
AaaaData data;
const auto bytes = reader.readBytes(data.address.size());
std::copy(bytes.begin(), bytes.end(), data.address.begin());
record.data = data;
break;
}
case Type::PTR:
record.data = PtrData {reader.readName()};
break;
case Type::TXT: {
TxtData data;
while (reader.position() < dataEnd) {
const auto length = reader.read8();
if (length > dataEnd - reader.position())
throw ParseError("truncated DNS TXT string");
data.strings.push_back(reader.readBytes(length));
}
record.data = std::move(data);
break;
}
case Type::SRV: {
if (dataLength < 7)
throw ParseError("invalid DNS SRV record length");
SrvData data;
data.priority = reader.read16();
data.weight = reader.read16();
data.port = reader.read16();
data.target = reader.readName();
record.data = std::move(data);
break;
}
case Type::NSEC:
record.data = decodeNsec(reader, dataEnd);
break;
default:
record.data = UnknownData {reader.readBytes(dataLength)};
break;
}
if (reader.position() != dataEnd)
throw ParseError("DNS record data length mismatch");
return record;
}
} // namespace
Name::Name(std::vector<std::string> labels)
: labels_(std::move(labels))
{
validateLabels(labels_);
}
Name
Name::fromString(std::string_view name)
{
if (name.empty() or name == ".")
return {};
if (name.back() == '.')
name.remove_suffix(1);
std::vector<std::string> labels;
size_t start = 0;
while (start <= name.size()) {
const auto end = name.find('.', start);
const auto length = (end == std::string_view::npos ? name.size() : end) - start;
if (length == 0)
throw ParseError("empty DNS label");
labels.emplace_back(name.substr(start, length));
if (end == std::string_view::npos)
break;
start = end + 1;
}
return Name {std::move(labels)};
}
std::string
Name::toString() const
{
if (labels_.empty())
return ".";
std::string name;
for (const auto& label : labels_) {
name.append(label);
name.push_back('.');
}
return name;
}
bool
operator==(const Name& lhs, const Name& rhs) noexcept
{
if (lhs.labels_.size() != rhs.labels_.size())
return false;
for (size_t index = 0; index < lhs.labels_.size(); ++index) {
const auto& left = lhs.labels_[index];
const auto& right = rhs.labels_[index];
if (left.size() != right.size())
return false;
if (not std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) {
return asciiLower(a) == asciiLower(b);
}))
return false;
}
return true;
}
Packet
encode(const Message& message)
{
const auto recordCount = message.questions.size() + message.answers.size() + message.authorities.size()
+ message.additionals.size();
if (recordCount > MAX_RECORD_COUNT)
throw ParseError("too many DNS records");
Writer writer;
writer.write16(message.id);
uint16_t flags = 0;
if (message.response)
flags |= FLAG_RESPONSE;
if (message.authoritative)
flags |= FLAG_AUTHORITATIVE;
if (message.truncated)
flags |= FLAG_TRUNCATED;
writer.write16(flags);
writer.write16(checkedCount(message.questions.size()));
writer.write16(checkedCount(message.answers.size()));
writer.write16(checkedCount(message.authorities.size()));
writer.write16(checkedCount(message.additionals.size()));
for (const auto& question : message.questions)
encodeQuestion(writer, question);
for (const auto& record : message.answers)
encodeRecord(writer, record);
for (const auto& record : message.authorities)
encodeRecord(writer, record);
for (const auto& record : message.additionals)
encodeRecord(writer, record);
return writer.take();
}
Message
decode(const uint8_t* packet, size_t size)
{
if (packet == nullptr)
throw ParseError("null DNS packet");
if (size > MAX_PACKET_SIZE)
throw ParseError("mDNS packet exceeds maximum size");
Reader reader(packet, size);
Message message;
message.id = reader.read16();
const auto flags = reader.read16();
message.response = flags & FLAG_RESPONSE;
message.authoritative = flags & FLAG_AUTHORITATIVE;
message.truncated = flags & FLAG_TRUNCATED;
const auto questionCount = reader.read16();
const auto answerCount = reader.read16();
const auto authorityCount = reader.read16();
const auto additionalCount = reader.read16();
const auto recordCount = static_cast<size_t>(questionCount) + answerCount + authorityCount + additionalCount;
if (recordCount > MAX_RECORD_COUNT)
throw ParseError("too many DNS records");
message.questions.reserve(questionCount);
message.answers.reserve(answerCount);
message.authorities.reserve(authorityCount);
message.additionals.reserve(additionalCount);
for (size_t index = 0; index < questionCount; ++index)
message.questions.push_back(decodeQuestion(reader));
for (size_t index = 0; index < answerCount; ++index)
message.answers.push_back(decodeRecord(reader));
for (size_t index = 0; index < authorityCount; ++index)
message.authorities.push_back(decodeRecord(reader));
for (size_t index = 0; index < additionalCount; ++index)
message.additionals.push_back(decodeRecord(reader));
if (reader.position() != size)
throw ParseError("trailing bytes in DNS packet");
return message;
}
} // namespace dht::mdns
+194
View File
@@ -0,0 +1,194 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include "opendht/def.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace dht::mdns {
constexpr size_t MAX_PACKET_SIZE = 9000;
constexpr size_t MAX_RECORD_COUNT = 1024;
constexpr uint16_t CLASS_IN = 1;
constexpr uint16_t CLASS_ANY = 255;
enum class Type : uint16_t {
A = 1,
PTR = 12,
TXT = 16,
AAAA = 28,
SRV = 33,
NSEC = 47,
ANY = 255,
};
class OPENDHT_PUBLIC ParseError : public std::runtime_error
{
public:
using std::runtime_error::runtime_error;
};
class OPENDHT_PUBLIC Name
{
public:
Name() = default;
explicit Name(std::vector<std::string> labels);
static Name fromString(std::string_view name);
const std::vector<std::string>& labels() const noexcept { return labels_; }
std::string toString() const;
friend bool operator==(const Name& lhs, const Name& rhs) noexcept;
friend bool operator!=(const Name& lhs, const Name& rhs) noexcept { return not(lhs == rhs); }
private:
std::vector<std::string> labels_;
};
OPENDHT_PUBLIC bool operator==(const Name& lhs, const Name& rhs) noexcept;
struct Question
{
Name name;
Type type {Type::ANY};
uint16_t classCode {CLASS_IN};
bool unicastResponse {false};
};
struct AData
{
std::array<uint8_t, 4> address {};
};
struct AaaaData
{
std::array<uint8_t, 16> address {};
};
struct PtrData
{
Name name;
};
struct TxtData
{
std::vector<std::vector<uint8_t>> strings;
};
struct SrvData
{
uint16_t priority {};
uint16_t weight {};
uint16_t port {};
Name target;
};
struct NsecData
{
Name nextDomain;
std::vector<Type> types;
};
struct UnknownData
{
std::vector<uint8_t> bytes;
};
using RData = std::variant<AData, AaaaData, PtrData, TxtData, SrvData, NsecData, UnknownData>;
struct ResourceRecord
{
Name name;
Type type {Type::ANY};
uint16_t classCode {CLASS_IN};
bool cacheFlush {false};
uint32_t ttl {};
RData data {UnknownData {}};
};
struct Message
{
uint16_t id {};
bool response {false};
bool authoritative {false};
bool truncated {false};
std::vector<Question> questions;
std::vector<ResourceRecord> answers;
std::vector<ResourceRecord> authorities;
std::vector<ResourceRecord> additionals;
};
using Packet = std::vector<uint8_t>;
OPENDHT_PUBLIC Packet encode(const Message& message);
OPENDHT_PUBLIC Message decode(const uint8_t* packet, size_t size);
inline bool
operator==(const Question& lhs, const Question& rhs)
{
return lhs.name == rhs.name and lhs.type == rhs.type and lhs.classCode == rhs.classCode
and lhs.unicastResponse == rhs.unicastResponse;
}
inline bool
operator==(const AData& lhs, const AData& rhs)
{
return lhs.address == rhs.address;
}
inline bool
operator==(const AaaaData& lhs, const AaaaData& rhs)
{
return lhs.address == rhs.address;
}
inline bool
operator==(const PtrData& lhs, const PtrData& rhs)
{
return lhs.name == rhs.name;
}
inline bool
operator==(const TxtData& lhs, const TxtData& rhs)
{
return lhs.strings == rhs.strings;
}
inline bool
operator==(const SrvData& lhs, const SrvData& rhs)
{
return lhs.priority == rhs.priority and lhs.weight == rhs.weight and lhs.port == rhs.port
and lhs.target == rhs.target;
}
inline bool
operator==(const NsecData& lhs, const NsecData& rhs)
{
return lhs.nextDomain == rhs.nextDomain and lhs.types == rhs.types;
}
inline bool
operator==(const UnknownData& lhs, const UnknownData& rhs)
{
return lhs.bytes == rhs.bytes;
}
inline bool
operator==(const ResourceRecord& lhs, const ResourceRecord& rhs)
{
return lhs.name == rhs.name and lhs.type == rhs.type and lhs.classCode == rhs.classCode
and lhs.cacheFlush == rhs.cacheFlush and lhs.ttl == rhs.ttl and lhs.data == rhs.data;
}
inline bool
operator==(const Message& lhs, const Message& rhs)
{
return lhs.id == rhs.id and lhs.response == rhs.response and lhs.authoritative == rhs.authoritative
and lhs.truncated == rhs.truncated and lhs.questions == rhs.questions and lhs.answers == rhs.answers
and lhs.authorities == rhs.authorities and lhs.additionals == rhs.additionals;
}
} // namespace dht::mdns
+455
View File
@@ -0,0 +1,455 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "mdns_transport.h"
#include <asio.hpp>
#include <algorithm>
#include <array>
#include <cerrno>
#include <cstring>
#include <future>
#include <mutex>
#ifdef _WIN32
#include <iphlpapi.h>
#else
#include <ifaddrs.h>
#include <net/if.h>
#endif
namespace dht::mdns {
namespace {
constexpr in_port_t MDNS_PORT = 5353;
constexpr std::string_view MDNS_IPV4 = "224.0.0.251";
constexpr std::string_view MDNS_IPV6 = "ff02::fb";
std::vector<InterfaceAddress>
interfaceAddresses([[maybe_unused]] asio::io_context& ioContext)
{
std::vector<InterfaceAddress> result;
#ifdef _WIN32
ULONG size = 15000;
std::vector<uint8_t> buffer(size);
auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
auto status = GetAdaptersAddresses(AF_UNSPEC,
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
nullptr,
adapters,
&size);
if (status == ERROR_BUFFER_OVERFLOW) {
buffer.resize(size);
adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
status = GetAdaptersAddresses(AF_UNSPEC,
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
nullptr,
adapters,
&size);
}
if (status == NO_ERROR) {
InterfaceId id = 0;
for (auto* adapter = adapters; adapter; adapter = adapter->Next) {
if (adapter->OperStatus != IfOperStatusUp)
continue;
++id;
const auto multicast = not(adapter->Flags & IP_ADAPTER_NO_MULTICAST);
const auto loopback = adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK;
for (auto* current = adapter->FirstUnicastAddress; current; current = current->Next) {
const auto* socketAddress = current->Address.lpSockaddr;
if (socketAddress->sa_family == AF_INET) {
const auto* address = reinterpret_cast<const sockaddr_in*>(socketAddress);
asio::ip::address_v4::bytes_type bytes;
std::memcpy(bytes.data(), &address->sin_addr, bytes.size());
result.push_back({id, adapter->IfIndex, asio::ip::address_v4(bytes), multicast, loopback});
} else if (socketAddress->sa_family == AF_INET6) {
const auto* address = reinterpret_cast<const sockaddr_in6*>(socketAddress);
asio::ip::address_v6::bytes_type bytes;
std::memcpy(bytes.data(), &address->sin6_addr, bytes.size());
result.push_back({id,
adapter->Ipv6IfIndex,
asio::ip::address_v6(bytes, address->sin6_scope_id),
multicast,
loopback});
}
}
}
}
#else
ifaddrs* addresses = nullptr;
if (getifaddrs(&addresses) != 0)
return result;
for (auto* current = addresses; current; current = current->ifa_next) {
if (not current->ifa_addr or not(current->ifa_flags & IFF_UP))
continue;
const auto family = current->ifa_addr->sa_family;
if (family == AF_INET) {
const auto* address = reinterpret_cast<const sockaddr_in*>(current->ifa_addr);
asio::ip::address_v4::bytes_type bytes;
std::memcpy(bytes.data(), &address->sin_addr, bytes.size());
const auto index = if_nametoindex(current->ifa_name);
result.push_back({index,
index,
asio::ip::address_v4(bytes),
bool(current->ifa_flags & IFF_MULTICAST),
bool(current->ifa_flags & IFF_LOOPBACK)});
} else if (family == AF_INET6) {
const auto* address = reinterpret_cast<const sockaddr_in6*>(current->ifa_addr);
asio::ip::address_v6::bytes_type bytes;
std::memcpy(bytes.data(), &address->sin6_addr, bytes.size());
const auto index = if_nametoindex(current->ifa_name);
result.push_back({index,
index,
asio::ip::address_v6(bytes, address->sin6_scope_id),
bool(current->ifa_flags & IFF_MULTICAST),
bool(current->ifa_flags & IFF_LOOPBACK)});
}
}
freeifaddrs(addresses);
#endif
return result;
}
} // namespace
std::vector<NetworkInterface>
groupInterfaces(const std::vector<InterfaceAddress>& addresses)
{
std::vector<NetworkInterface> result;
const auto hasNonLoopback = std::any_of(addresses.begin(), addresses.end(), [](const InterfaceAddress& address) {
return address.multicast and not address.loopback and not address.address.is_unspecified()
and not address.address.is_multicast();
});
for (const auto& address : addresses) {
if (address.id == 0 or not address.multicast or (hasNonLoopback and address.loopback)
or address.address.is_unspecified() or address.address.is_multicast())
continue;
auto scope = std::find_if(result.begin(), result.end(), [&](const NetworkInterface& interface) {
return interface.id == address.id;
});
if (scope == result.end()) {
NetworkInterface interface;
interface.id = address.id;
result.push_back(std::move(interface));
scope = std::prev(result.end());
}
dns_sd::AddressData record;
if (address.address.is_v4()) {
scope->ipv4Index = address.index;
if (not scope->ipv4Outbound)
scope->ipv4Outbound = address.address.to_v4();
record = AData {address.address.to_v4().to_bytes()};
} else {
const auto bytes = address.address.to_v6().to_bytes();
const auto mapped = std::all_of(bytes.begin(), bytes.begin() + 10, [](uint8_t byte) { return byte == 0; })
and bytes[10] == 0xff and bytes[11] == 0xff;
if (mapped)
continue;
scope->ipv6Index = address.index;
record = AaaaData {bytes};
}
if (std::find(scope->addresses.begin(), scope->addresses.end(), record) == scope->addresses.end())
scope->addresses.emplace_back(std::move(record));
}
result.erase(std::remove_if(result.begin(),
result.end(),
[](const NetworkInterface& interface) { return interface.addresses.empty(); }),
result.end());
return result;
}
class MdnsTransport::Impl : public std::enable_shared_from_this<MdnsTransport::Impl>
{
public:
Impl(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger)
: ioContext_(std::move(ioContext))
, logger_(std::move(logger))
, interfaces_(groupInterfaces(interfaceAddresses(*ioContext_)))
{}
struct Socket
{
explicit Socket(asio::io_context& ioContext)
: socket(ioContext)
{}
asio::ip::udp::socket socket;
std::array<uint8_t, MAX_PACKET_SIZE> receiveBuffer {};
asio::ip::udp::endpoint sender;
asio::ip::udp::endpoint multicast;
InterfaceId interface {};
unsigned ipv6Index {};
};
void start(ReceiveCallback callback)
{
callback_ = std::move(callback);
configure();
}
void send(Packet packet) { send(std::move(packet), {}, {}); }
void send(Packet packet, std::optional<asio::ip::udp::endpoint> destination, std::optional<InterfaceId> interface)
{
auto sharedPacket = std::make_shared<Packet>(std::move(packet));
asio::post(*ioContext_, [weak = weak_from_this(), sharedPacket, destination, interface] {
const auto self = weak.lock();
if (not self)
return;
for (const auto& state : self->sockets_) {
if (interface and state->interface != *interface)
continue;
const auto target = destination.value_or(state->multicast);
if (target.address().is_v4() != state->multicast.address().is_v4())
continue;
state->socket.async_send_to(asio::buffer(*sharedPacket),
target,
[logger = self->logger_, sharedPacket](const asio::error_code& error,
size_t) {
if (error and error != asio::error::operation_aborted and logger)
logger->warn("Unable to send mDNS packet: {}", error.message());
});
}
});
}
void sendAndWait(Packet packet, InterfaceId interface)
{
if (ioContext_->get_executor().running_in_this_thread()) {
sendNow(packet, interface);
return;
}
auto completed = std::make_shared<std::promise<void>>();
auto future = completed->get_future();
asio::post(*ioContext_, [weak = weak_from_this(), packet = std::move(packet), interface, completed] {
if (const auto self = weak.lock())
self->sendNow(packet, interface);
completed->set_value();
});
future.wait_for(std::chrono::milliseconds(250));
}
void refresh()
{
{
std::lock_guard lock(interfacesMutex_);
interfaces_ = groupInterfaces(interfaceAddresses(*ioContext_));
}
asio::post(*ioContext_, [weak = weak_from_this()] {
if (const auto self = weak.lock())
self->configure();
});
}
void stop()
{
if (ioContext_->get_executor().running_in_this_thread()) {
stopNow();
return;
}
auto completed = std::make_shared<std::promise<void>>();
auto future = completed->get_future();
asio::post(*ioContext_, [weak = weak_from_this(), completed] {
if (const auto self = weak.lock())
self->stopNow();
completed->set_value();
});
future.wait_for(std::chrono::milliseconds(250));
}
std::vector<NetworkInterface> interfaces() const
{
std::lock_guard lock(interfacesMutex_);
return interfaces_;
}
private:
void sendNow(const Packet& packet, InterfaceId interface)
{
for (const auto& state : sockets_) {
if (state->interface != interface)
continue;
asio::error_code error;
state->socket.send_to(asio::buffer(packet), state->multicast, 0, error);
if (error and logger_)
logger_->warn("Unable to send final mDNS packet: {}", error.message());
}
}
void stopNow()
{
callback_ = {};
stopSockets();
}
void configure()
{
stopSockets();
std::vector<NetworkInterface> currentInterfaces;
{
std::lock_guard lock(interfacesMutex_);
currentInterfaces = interfaces_;
}
for (const auto& interface : currentInterfaces) {
if (interface.ipv4Outbound)
configure(interface, asio::ip::udp::v4(), asio::ip::make_address(MDNS_IPV4));
if (interface.ipv6Index)
configure(interface, asio::ip::udp::v6(), asio::ip::make_address(MDNS_IPV6));
}
}
void configure(const NetworkInterface& interface, asio::ip::udp protocol, const asio::ip::address& multicast)
{
auto state = std::make_shared<Socket>(*ioContext_);
asio::error_code error;
state->socket.open(protocol, error);
if (error)
return logSocketError(protocol, error);
state->socket.set_option(asio::ip::udp::socket::reuse_address(true), error);
#if defined(SO_REUSEPORT) && !defined(_WIN32)
if (not error) {
const int enabled = 1;
if (::setsockopt(state->socket.native_handle(), SOL_SOCKET, SO_REUSEPORT, &enabled, sizeof(enabled)) != 0)
error.assign(errno, asio::error::get_system_category());
}
#endif
if (error)
return logSocketError(protocol, error);
if (protocol == asio::ip::udp::v6())
state->socket.set_option(asio::ip::v6_only(true), error);
state->socket.bind({protocol, MDNS_PORT}, error);
if (error)
return logSocketError(protocol, error);
state->socket.set_option(asio::ip::multicast::hops(255), error);
if (error)
return logSocketError(protocol, error);
state->socket.set_option(asio::ip::multicast::enable_loopback(true), error);
if (protocol == asio::ip::udp::v4()) {
state->socket.set_option(asio::ip::multicast::join_group(multicast.to_v4(), *interface.ipv4Outbound), error);
if (not error)
state->socket.set_option(asio::ip::multicast::outbound_interface(*interface.ipv4Outbound), error);
} else {
state->socket.set_option(asio::ip::multicast::join_group(multicast.to_v6(), interface.ipv6Index), error);
if (not error)
state->socket.set_option(asio::ip::multicast::outbound_interface(interface.ipv6Index), error);
}
if (error)
return logSocketError(protocol, error);
if (protocol == asio::ip::udp::v6())
state->multicast = {asio::ip::address_v6(multicast.to_v6().to_bytes(), interface.ipv6Index), MDNS_PORT};
else
state->multicast = {multicast, MDNS_PORT};
state->interface = interface.id;
state->ipv6Index = interface.ipv6Index;
sockets_.push_back(state);
receive(state);
}
void receive(const std::shared_ptr<Socket>& state)
{
state->socket.async_receive_from(asio::buffer(state->receiveBuffer),
state->sender,
[weak = weak_from_this(), state](const asio::error_code& error, size_t size) {
const auto self = weak.lock();
if (not self)
return;
if (not error and self->callback_) {
Packet packet(state->receiveBuffer.begin(),
state->receiveBuffer.begin() + size);
self->callback_(packet,
{state->sender, state->interface, state->ipv6Index});
} else if (error != asio::error::operation_aborted and self->logger_) {
self->logger_->warn("Unable to receive mDNS packet: {}",
error.message());
}
if (state->socket.is_open())
self->receive(state);
});
}
void stopSockets()
{
for (const auto& state : sockets_) {
asio::error_code error;
state->socket.cancel(error);
state->socket.close(error);
}
sockets_.clear();
}
void logSocketError(asio::ip::udp protocol, const asio::error_code& error)
{
if (logger_)
logger_->warn("Unable to start {} mDNS transport: {}",
protocol == asio::ip::udp::v4() ? "IPv4" : "IPv6",
error.message());
}
std::shared_ptr<asio::io_context> ioContext_;
std::shared_ptr<Logger> logger_;
mutable std::mutex interfacesMutex_;
std::vector<NetworkInterface> interfaces_;
std::vector<std::shared_ptr<Socket>> sockets_;
ReceiveCallback callback_;
};
MdnsTransport::MdnsTransport(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger)
: impl_(std::make_shared<Impl>(std::move(ioContext), std::move(logger)))
{}
MdnsTransport::~MdnsTransport() = default;
void
MdnsTransport::start(ReceiveCallback callback)
{
impl_->start(std::move(callback));
}
void
MdnsTransport::send(Packet packet)
{
impl_->send(std::move(packet));
}
void
MdnsTransport::send(Packet packet, InterfaceId interface)
{
impl_->send(std::move(packet), {}, interface);
}
void
MdnsTransport::send(Packet packet, const asio::ip::udp::endpoint& destination, InterfaceId interface)
{
impl_->send(std::move(packet), destination, interface);
}
void
MdnsTransport::sendAndWait(Packet packet, InterfaceId interface)
{
impl_->sendAndWait(std::move(packet), interface);
}
void
MdnsTransport::refresh()
{
impl_->refresh();
}
void
MdnsTransport::stop()
{
if (impl_)
impl_->stop();
}
std::vector<NetworkInterface>
MdnsTransport::interfaces() const
{
return impl_->interfaces();
}
} // namespace dht::mdns
+74
View File
@@ -0,0 +1,74 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include "dns_sd.h"
#include "opendht/logger.h"
#include <asio/ip/address.hpp>
#include <asio/ip/udp.hpp>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
namespace asio {
class io_context;
}
namespace dht::mdns {
using InterfaceId = uint64_t;
struct InterfaceAddress
{
InterfaceId id {};
unsigned index {};
asio::ip::address address;
bool multicast {true};
bool loopback {false};
};
struct NetworkInterface
{
InterfaceId id {};
unsigned ipv4Index {};
unsigned ipv6Index {};
std::optional<asio::ip::address_v4> ipv4Outbound;
std::vector<dns_sd::AddressData> addresses;
};
OPENDHT_PUBLIC std::vector<NetworkInterface> groupInterfaces(const std::vector<InterfaceAddress>& addresses);
struct ReceiveContext
{
asio::ip::udp::endpoint source;
InterfaceId interface {};
unsigned ipv6Index {};
};
class MdnsTransport
{
public:
using ReceiveCallback = std::function<void(const Packet&, const ReceiveContext&)>;
MdnsTransport(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger = {});
~MdnsTransport();
void start(ReceiveCallback callback);
void send(Packet packet);
void send(Packet packet, InterfaceId interface);
void send(Packet packet, const asio::ip::udp::endpoint& destination, InterfaceId interface);
void sendAndWait(Packet packet, InterfaceId interface);
void refresh();
void stop();
std::vector<NetworkInterface> interfaces() const;
private:
class Impl;
std::shared_ptr<Impl> impl_;
};
} // namespace dht::mdns
+438
View File
@@ -0,0 +1,438 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "peer_discovery.h"
#include "dns_sd.h"
#include "mdns_transport.h"
#include <asio.hpp>
#include <chrono>
#include <cstring>
#include <map>
#include <mutex>
#include <optional>
#include <random>
namespace dht {
namespace {
constexpr auto BROWSE_INTERVAL = std::chrono::seconds(60);
constexpr auto CONNECTIVITY_RETRY = std::chrono::seconds(10);
constexpr auto CONNECTIVITY_RETRY_MAX = std::chrono::minutes(1);
SockAddr
socketAddress(const mdns::dns_sd::AddressData& data, in_port_t port, const mdns::ReceiveContext& context)
{
SockAddr address;
std::visit(
[&](const auto& record) {
using Record = std::decay_t<decltype(record)>;
if constexpr (std::is_same_v<Record, mdns::AData>) {
address.setFamily(AF_INET);
std::memcpy(&address.getIPv4().sin_addr, record.address.data(), record.address.size());
} else {
address.setFamily(AF_INET6);
std::memcpy(&address.getIPv6().sin6_addr, record.address.data(), record.address.size());
const asio::ip::address_v6 parsed(record.address);
if (parsed.is_link_local())
address.getIPv6().sin6_scope_id = context.ipv6Index;
}
},
data);
address.setPort(port);
return address;
}
} // namespace
class PeerDiscovery::Impl : public std::enable_shared_from_this<PeerDiscovery::Impl>
{
public:
Impl(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger)
: ioContext_(std::move(ioContext))
, logger_(std::move(logger))
, transport_(ioContext_, logger_)
, browseTimer_(*ioContext_)
, announceTimer_(*ioContext_)
, connectivityTimer_(*ioContext_)
{}
void start()
{
transport_.start([weak = weak_from_this()](const mdns::Packet& packet, const mdns::ReceiveContext& context) {
if (const auto self = weak.lock())
self->receive(packet, context);
});
if (logger_)
logger_->debug("[peer discovery {:p}] Started.", fmt::ptr(this));
}
void startDiscovery(NetId network, PeerDiscoveredCallback callback)
{
{
std::lock_guard lock(mutex_);
discoveryNetwork_ = network;
discoveryCallback_ = std::move(callback);
}
asio::post(*ioContext_, [weak = weak_from_this()] {
if (const auto self = weak.lock())
self->browse();
});
}
void startPublish(const InfoHash& nodeId, NetId network, in_port_t port)
{
mdns::dns_sd::Service service {nodeId, network, port, {}};
if (transport_.interfaces().empty())
throw mdns::ParseError("DNS-SD service requires a multicast interface");
{
std::lock_guard lock(mutex_);
published_ = std::move(service);
}
announce();
asio::post(*ioContext_, [weak = weak_from_this()] {
const auto self = weak.lock();
if (not self)
return;
self->announceTimer_.expires_after(std::chrono::seconds(1));
self->announceTimer_.async_wait([weak](const asio::error_code& error) {
if (not error) {
if (const auto locked = weak.lock())
locked->announce();
}
});
});
}
bool stopDiscovery()
{
std::lock_guard lock(mutex_);
const auto stopped = bool(discoveryCallback_);
discoveryCallback_ = {};
discoveryNetwork_.reset();
asio::post(*ioContext_, [weak = weak_from_this()] {
if (const auto self = weak.lock())
self->browseTimer_.cancel();
});
return stopped;
}
bool stopPublish()
{
std::optional<mdns::dns_sd::Service> service;
{
std::lock_guard lock(mutex_);
service = std::exchange(published_, {});
}
asio::post(*ioContext_, [weak = weak_from_this()] {
if (const auto self = weak.lock())
self->announceTimer_.cancel();
});
if (service)
advertise(*service, true, false);
return bool(service);
}
void stop()
{
stopConnectivityChanged();
stopDiscovery();
std::optional<mdns::dns_sd::Service> service;
{
std::lock_guard lock(mutex_);
service = std::exchange(published_, {});
}
asio::post(*ioContext_, [weak = weak_from_this()] {
if (const auto self = weak.lock())
self->announceTimer_.cancel();
});
if (service)
advertise(*service, true, true);
transport_.stop();
}
void connectivityChanged()
{
asio::post(*ioContext_, [weak = weak_from_this()] {
if (const auto self = weak.lock())
self->handleConnectivityChanged();
});
}
void stopConnectivityChanged()
{
asio::post(*ioContext_, [weak = weak_from_this()] {
if (const auto self = weak.lock()) {
self->connectivityTimer_.cancel();
self->connectivityRetry_ = CONNECTIVITY_RETRY;
}
});
}
private:
void handleConnectivityChanged()
{
caches_.clear();
transport_.refresh();
refreshPublication();
browse();
connectivityTimer_.expires_after(connectivityRetry_);
connectivityTimer_.async_wait([weak = weak_from_this()](const asio::error_code& error) {
if (not error) {
if (const auto self = weak.lock())
self->handleConnectivityChanged();
}
});
connectivityRetry_ = std::min(connectivityRetry_ * 2,
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
CONNECTIVITY_RETRY_MAX));
}
void browse()
{
bool active;
{
std::lock_guard lock(mutex_);
active = bool(discoveryCallback_);
}
if (not active)
return;
if (logger_)
logger_->debug("[peer discovery {:p}] Browsing.", fmt::ptr(this));
transport_.send(mdns::encode(mdns::dns_sd::browseQuery()));
browseTimer_.expires_after(BROWSE_INTERVAL);
browseTimer_.async_wait([weak = weak_from_this()](const asio::error_code& error) {
if (not error) {
if (const auto self = weak.lock())
self->browse();
}
});
}
void announce()
{
std::optional<mdns::dns_sd::Service> service;
{
std::lock_guard lock(mutex_);
service = published_;
}
if (logger_)
logger_->debug("[peer discovery {:p}] Announcing.", fmt::ptr(this));
if (service)
advertise(*service, false, false);
}
void refreshPublication()
{
std::optional<mdns::dns_sd::Service> service;
{
std::lock_guard lock(mutex_);
if (published_)
service = published_;
}
if (service)
advertise(*service, false, false);
}
void advertise(const mdns::dns_sd::Service& service, bool goodbye, bool wait)
{
for (const auto& interface : transport_.interfaces()) {
auto scoped = service;
scoped.addresses = interface.addresses;
auto packet = mdns::encode(goodbye ? mdns::dns_sd::goodbye(scoped) : mdns::dns_sd::announcement(scoped));
if (wait)
transport_.sendAndWait(std::move(packet), interface.id);
else
transport_.send(std::move(packet), interface.id);
}
}
void receive(const mdns::Packet& packet, const mdns::ReceiveContext& context)
{
if (logger_)
logger_->debug("[peer discovery {:p}] Got packet of size {}.", fmt::ptr(this), packet.size());
try {
const auto message = mdns::decode(packet.data(), packet.size());
if (message.response)
discover(message, context);
else
answer(message, context);
} catch (const std::exception& error) {
if (logger_)
logger_->warn("Ignoring invalid mDNS packet: {}", error.what());
}
}
void answer(const mdns::Message& query, const mdns::ReceiveContext& context)
{
std::optional<mdns::dns_sd::Service> service;
{
std::lock_guard lock(mutex_);
service = published_;
}
if (service) {
const auto interfaces = transport_.interfaces();
const auto interface = std::find_if(interfaces.begin(), interfaces.end(), [&](const auto& candidate) {
return candidate.id == context.interface;
});
if (interface == interfaces.end())
return;
service->addresses = interface->addresses;
if (auto response = mdns::dns_sd::respond(query, *service)) {
const auto unicast = context.source.port() != 5353
or std::any_of(query.questions.begin(),
query.questions.end(),
[](const mdns::Question& question) {
return question.unicastResponse;
});
if (context.source.port() != 5353)
response->questions = query.questions;
auto packet = mdns::encode(*response);
const auto sharedAnswer = std::any_of(response->answers.begin(),
response->answers.end(),
[](const mdns::ResourceRecord& record) {
return not record.cacheFlush;
});
if (not sharedAnswer) {
if (unicast)
transport_.send(std::move(packet), context.source, context.interface);
else
transport_.send(std::move(packet), context.interface);
return;
}
static thread_local std::mt19937 random {std::random_device {}()};
const auto delay = std::chrono::milliseconds(std::uniform_int_distribution<int> {20, 120}(random));
auto timer = std::make_shared<asio::steady_timer>(*ioContext_, delay);
timer->async_wait([weak = weak_from_this(), timer, packet = std::move(packet), context, unicast](
const asio::error_code& error) mutable {
if (error)
return;
const auto self = weak.lock();
if (not self)
return;
if (unicast)
self->transport_.send(std::move(packet), context.source, context.interface);
else
self->transport_.send(std::move(packet), context.interface);
});
}
}
}
void discover(const mdns::Message& message, const mdns::ReceiveContext& context)
{
PeerDiscoveredCallback callback;
std::optional<NetId> network;
std::optional<InfoHash> localNode;
{
std::lock_guard lock(mutex_);
callback = discoveryCallback_;
network = discoveryNetwork_;
if (published_)
localNode = published_->nodeId;
}
if (not callback or not network)
return;
const auto cache = caches_.try_emplace(context.interface).first;
for (const auto& service : cache->second.update(message)) {
if (service.network != *network or (localNode and service.nodeId == *localNode))
continue;
for (const auto& address : service.addresses)
callback(service.nodeId, socketAddress(address, service.port, context));
}
}
std::shared_ptr<asio::io_context> ioContext_;
std::shared_ptr<Logger> logger_;
mdns::MdnsTransport transport_;
asio::steady_timer browseTimer_;
asio::steady_timer announceTimer_;
asio::steady_timer connectivityTimer_;
std::chrono::steady_clock::duration connectivityRetry_ {CONNECTIVITY_RETRY};
std::mutex mutex_;
std::optional<NetId> discoveryNetwork_;
PeerDiscoveredCallback discoveryCallback_;
std::optional<mdns::dns_sd::Service> published_;
std::map<mdns::InterfaceId, mdns::dns_sd::Cache> caches_;
};
PeerDiscovery::PeerDiscovery(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger)
{
if (not ioContext) {
ioContext = std::make_shared<asio::io_context>();
ioContext_ = ioContext;
}
impl_ = std::make_shared<Impl>(ioContext, logger);
impl_->start();
if (ioContext_) {
ioRunner_ = std::thread([logger = std::move(logger), ioContext = std::move(ioContext)] {
try {
auto work = asio::make_work_guard(*ioContext);
ioContext->run();
} catch (const std::exception& error) {
if (logger)
logger->error("Peer discovery event loop failed: {}", error.what());
}
});
}
}
PeerDiscovery::~PeerDiscovery()
{
stop();
impl_.reset();
if (ioContext_)
ioContext_->stop();
if (ioRunner_.joinable())
ioRunner_.join();
}
void
PeerDiscovery::startDiscovery(NetId network, PeerDiscoveredCallback callback)
{
impl_->startDiscovery(network, std::move(callback));
}
void
PeerDiscovery::startPublish(const InfoHash& nodeId, NetId network, in_port_t port)
{
impl_->startPublish(nodeId, network, port);
}
bool
PeerDiscovery::stopDiscovery()
{
return impl_->stopDiscovery();
}
bool
PeerDiscovery::stopPublish()
{
return impl_->stopPublish();
}
void
PeerDiscovery::stop()
{
if (impl_)
impl_->stop();
}
void
PeerDiscovery::connectivityChanged()
{
impl_->connectivityChanged();
}
void
PeerDiscovery::stopConnectivityChanged()
{
impl_->stopConnectivityChanged();
}
} // namespace dht
+1 -3
View File
@@ -232,8 +232,6 @@ Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, t
if (it->data != value) {
size_t size_old = it->data->size();
ssize_t size_diff = size_new - (ssize_t) size_old;
// Re-storing identical content is a refresh, not an edition
size_t edited = (*it->data != *value) ? 1 : 0;
// DHT_LOG.DEBUG("Updating %s -> %s", id.toString().c_str(), value->toString().c_str());
// clear quota for previous value
if (it->store_bucket)
@@ -245,7 +243,7 @@ Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, t
sb->insert(id, *value, expiration);
it->data = value;
total_size += size_diff;
return std::make_pair(&(*it), StoreDiff {size_diff, 0, 0, edited});
return std::make_pair(&(*it), StoreDiff {size_diff, 0, 0, 1});
}
} else {
// DHT_LOG.DEBUG("Storing %s -> %s", id.toString().c_str(), value->toString().c_str());
+7 -16
View File
@@ -245,18 +245,10 @@ from_system_time(system_clock::time_point t, time_point steady_now, system_clock
return time_point::min();
if (t == system_clock::time_point::max())
return time_point::max();
// Saturate before the cast: the steady clock duration may hold a narrower range than the system one.
constexpr auto max_dt = std::chrono::duration_cast<system_clock::duration>(duration::max());
constexpr auto min_dt = std::chrono::duration_cast<system_clock::duration>(duration::min());
auto system_dt = t - system_now;
if (system_dt >= max_dt)
auto dt = t - system_now;
if (dt > system_clock::duration(0) and steady_now > time_point::max() - dt)
return time_point::max();
if (system_dt <= min_dt)
return time_point::min();
auto dt = std::chrono::duration_cast<duration>(system_dt);
if (dt > duration(0) and steady_now > time_point::max() - dt)
return time_point::max();
else if (dt < duration(0) and steady_now < time_point::min() - dt)
else if (dt < system_clock::duration(0) and steady_now < time_point::min() - dt)
return time_point::min();
return steady_now + dt;
}
@@ -274,13 +266,12 @@ to_system_time(time_point t, time_point steady_now, system_clock::time_point sys
return system_clock::time_point::min();
if (t == time_point::max())
return system_clock::time_point::max();
// Cast first: mixing clock durations of different periods would overflow the bound computations below.
auto dt = std::chrono::duration_cast<system_clock::duration>(t - steady_now);
if (dt > system_clock::duration(0) and system_now >= system_clock::time_point::max() - dt)
auto dt = t - steady_now;
if (dt > duration(0) and system_now >= system_clock::time_point::max() - dt)
return system_clock::time_point::max();
else if (dt < system_clock::duration(0) and system_now <= system_clock::time_point::min() - dt)
else if (dt < duration(0) and system_now <= system_clock::time_point::min() - dt)
return system_clock::time_point::min();
return system_now + dt;
return system_now + std::chrono::duration_cast<system_clock::duration>(dt);
}
system_clock::time_point
+6 -27
View File
@@ -71,21 +71,11 @@ public:
{
std::vector<Sp<Value>> expired_values;
for (auto it = values.begin(); it != values.end();) {
auto effectiveExpiration = it->second.expiration;
if (status == ListenSyncStatus::SYNCED)
// While the listen is synced, the remote node pushes
// explicit expirations and periodic refreshes. Add a
// grace period proportional to the value's type
// expiration so that a single lost UDP refresh
// notification does not cause a premature local drop.
// A grace of one type-expiration covers exactly one
// missed re-announcement cycle.
effectiveExpiration += it->second.typeExpiration;
if (effectiveExpiration <= now) {
if (it->second.expiration <= now) {
expired_values.emplace_back(std::move(it->second.data));
it = values.erase(it);
} else {
next = std::min(next, effectiveExpiration);
next = std::min(next, it->second.expiration);
++it;
}
}
@@ -158,14 +148,12 @@ private:
Sp<Value> data {};
time_point created {};
time_point expiration {};
duration typeExpiration {}; /* value type's expiration period, used as grace when synced */
CacheValueStorage() {}
CacheValueStorage(const Sp<Value>& v, time_point t, time_point e, duration te)
CacheValueStorage(const Sp<Value>& v, time_point t, time_point e)
: data(v)
, created(t)
, expiration(e)
, typeExpiration(te)
{}
};
@@ -178,22 +166,15 @@ private:
{
std::vector<Sp<Value>> nvals;
for (const auto& value : new_values) {
auto te = types.getType(value->type).expiration;
auto v = values.find(value->id);
if (v == values.end()) {
// new value
nvals.emplace_back(value);
values.emplace(value->id, CacheValueStorage(value, now, now + te, te));
values.emplace(value->id, CacheValueStorage(value, now, now + types.getType(value->type).expiration));
} else {
// refreshed value
v->second.created = now;
v->second.expiration = now + te;
v->second.typeExpiration = te;
if (value->seq > v->second.data->seq) {
// edited value (same rule as OpValueCache): emit as add
v->second.data = value;
nvals.emplace_back(value);
}
v->second.expiration = now + types.getType(v->second.data->type).expiration;
}
}
CallbackQueue ret;
@@ -220,10 +201,8 @@ private:
auto v = values.find(vid);
if (v == values.end())
return;
auto te = types.getType(v->second.data->type).expiration;
v->second.created = now;
v->second.expiration = now + te;
v->second.typeExpiration = te;
v->second.expiration = now + types.getType(v->second.data->type).expiration;
}
};
-10
View File
@@ -327,16 +327,6 @@ CryptoTester::testCertificateSerialNumber()
}
}
void
CryptoTester::testEcKeyCurve()
{
auto key = dht::crypto::PrivateKey::generateEC(GNUTLS_ECC_CURVE_SECP256R1);
unsigned int bits = 0;
CPPUNIT_ASSERT_EQUAL(static_cast<int>(GNUTLS_PK_EC),
gnutls_x509_privkey_get_pk_algorithm2(key.x509_key, &bits));
CPPUNIT_ASSERT_EQUAL(256u, bits);
}
void
CryptoTester::testOcsp()
{
-5
View File
@@ -19,7 +19,6 @@ class CryptoTester : public CppUnit::TestFixture
CPPUNIT_TEST(testRevocationListPartitionedNumber);
CPPUNIT_TEST(testCertificateRequest);
CPPUNIT_TEST(testCertificateSerialNumber);
CPPUNIT_TEST(testEcKeyCurve);
CPPUNIT_TEST(testOcsp);
CPPUNIT_TEST(testAesEncryption);
CPPUNIT_TEST(testAesEncryptionWithMultipleKeySizes);
@@ -69,10 +68,6 @@ public:
* Test certificate serial number extraction
*/
void testCertificateSerialNumber();
/**
* Test EC private key generation with an explicit curve
*/
void testEcKeyCurve();
/**
* Test OCSP
*/
+1 -1483
View File
File diff suppressed because it is too large. Load diff
+1 -35
View File
@@ -8,6 +8,7 @@
#include <opendht/dhtrunner.h>
#include <opendht/dht_proxy_server.h>
#include <opendht/log.h>
namespace test {
@@ -16,21 +17,10 @@ class DhtProxyTester : public CppUnit::TestFixture
CPPUNIT_TEST_SUITE(DhtProxyTester);
CPPUNIT_TEST(testGetPut);
CPPUNIT_TEST(testListen);
CPPUNIT_TEST(testListenValueEdit);
CPPUNIT_TEST(testPushListenValueEdit);
CPPUNIT_TEST(testResubscribeGetValues);
CPPUNIT_TEST(testPushNotification);
CPPUNIT_TEST(testProxyServerClientFullChain);
CPPUNIT_TEST(testPutGet40KChars);
CPPUNIT_TEST(testFuzzy);
CPPUNIT_TEST(testShutdownStop);
CPPUNIT_TEST(testGetAfterListen);
CPPUNIT_TEST(testListenDuplicatePut);
CPPUNIT_TEST(testPushMultiValueEditExpire);
CPPUNIT_TEST(testPushRefreshNoSpuriousExpire);
CPPUNIT_TEST(testProxyListenEditChain);
CPPUNIT_TEST(testPushRapidEditsNoPhantom);
CPPUNIT_TEST(testPushRefreshAfterEditKeepsLatest);
CPPUNIT_TEST_SUITE_END();
public:
@@ -51,28 +41,11 @@ public:
* Test listen
*/
void testListen();
/**
* Test that editing a value only triggers an add callback via proxy, no expire
*/
void testListenValueEdit();
/**
* Test that editing a value only triggers an add callback via push,
* no phantom expire, and that real expiration is received.
*/
void testPushListenValueEdit();
/**
* When a proxy redo a subscribe on the proxy
* it should retrieve existant values
*/
void testResubscribeGetValues();
/**
* Test push notification mechanism and OpValueCache
*/
void testPushNotification();
/**
* Test end-to-end chain through proxy server and proxy client
*/
void testProxyServerClientFullChain();
/**
* Test MTU put/get on dht
*/
@@ -81,13 +54,6 @@ public:
void testFuzzy();
void testShutdownStop();
void testGetAfterListen();
void testListenDuplicatePut();
void testPushMultiValueEditExpire();
void testPushRefreshNoSpuriousExpire();
void testProxyListenEditChain();
void testPushRapidEditsNoPhantom();
void testPushRefreshAfterEditKeepsLatest();
private:
dht::DhtRunner::Config clientConfig {};
+2 -2
View File
@@ -37,7 +37,7 @@ DhtProxyClientTester::testResubscribeUsesKeyRoute()
#ifdef OPENDHT_PUSH_NOTIFICATIONS
DhtProxyClient client({}, {}, [] {}, "http://127.0.0.1:8080", "OpenDHT-Test", "client-id", "push-token");
DhtProxyClient::Listener listener {ValueCallback([](const std::vector<Sp<Value>>&, bool) { return true; })};
DhtProxyClient::Listener listener {OpValueCache([](const std::vector<Sp<Value>>&, bool) { return true; })};
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
return true;
@@ -67,7 +67,7 @@ DhtProxyClientTester::testSetPushNotificationTokenResubscribesWithNewToken()
auto& search = client.searches_[key];
auto [it, inserted] = search.listeners.emplace(std::piecewise_construct,
std::forward_as_tuple(1),
std::forward_as_tuple(ValueCallback(
std::forward_as_tuple(OpValueCache(
[](const std::vector<Sp<Value>>&, bool) { return true; })));
CPPUNIT_ASSERT(inserted);
+1 -595
View File
@@ -91,42 +91,6 @@ DhtRunnerTester::testGetPut()
CPPUNIT_ASSERT(vals.front()->data == val_data);
}
void
DhtRunnerTester::testStoreEmptyValue()
{
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.max_store_size = 100; // Small size to force expireStore()
config.dht_config.node_config.max_store_keys = -1;
dht::DhtRunner testNode;
testNode.run(0, config);
testNode.bootstrap(node1.getBound());
auto key = testNode.getId();
auto val = std::make_shared<dht::Value>();
val->data.clear(); // Ensure size is 0
CPPUNIT_ASSERT_EQUAL((size_t) 0, val->size());
std::promise<bool> p;
testNode.put(key, val, [&](bool ok) { p.set_value(ok); });
p.get_future().get(); // Wait for put to finish
// Put a large value to exceed max_store_size and trigger expireStore()
auto key2 = dht::InfoHash::get("large_value_test");
auto val2 = std::make_shared<dht::Value>();
val2->data = std::vector<uint8_t>(200, 0); // 200 bytes
std::promise<bool> p2;
testNode.put(key2, val2, [&](bool ok) { p2.set_value(ok); });
p2.get_future().get(); // Wait for put to finish
// Wait a bit to ensure the value is processed
std::this_thread::sleep_for(std::chrono::milliseconds(200));
testNode.join();
}
void
DhtRunnerTester::testPutDuplicate()
{
@@ -623,115 +587,6 @@ DhtRunnerTester::testSavedPortFallback()
std::remove((config.dht_config.node_config.persist_path + "_port.txt").c_str());
}
void
DhtRunnerTester::testListenValueEdit()
{
// Register an editable type on both nodes so the DHT allows edits.
static constexpr dht::ValueType::Id EDITABLE_TYPE_ID = 9999;
const dht::ValueType editableType {EDITABLE_TYPE_ID,
"editable-test",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(editableType);
node2.registerType(editableType);
auto key = dht::InfoHash::get("listenValueEdit");
auto identity = dht::crypto::generateIdentity("ListenEditTester");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events; // (id, expired?)
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals) {
events.emplace_back(v->id, expired);
}
cv.notify_all();
return true;
});
// Put initial value (signed, editable type)
auto val1 = std::make_shared<dht::Value>("version1");
val1->type = EDITABLE_TYPE_ID;
val1->id = 42;
val1->seq = 1;
val1->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, val1, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for the first value to be received
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Edit the value (same id, higher seq)
auto val2 = std::make_shared<dht::Value>("version2");
val2->type = EDITABLE_TYPE_ID;
val2->id = 42;
val2->seq = 2;
val2->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, val2, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for the edited value to be received
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.second)
adds++;
return adds >= 2;
}));
}
// Give some extra time for any stray callbacks
std::this_thread::sleep_for(500ms);
// Verify: we should have exactly 2 add events, no expire events.
// On value edition the listener should see only the new version
// (as an add), not an expiration of the old version.
{
std::lock_guard lk(mtx);
int addCount = 0;
int expireCount = 0;
for (const auto& e : events) {
if (e.second)
expireCount++;
else
addCount++;
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive 2 add callbacks (original + edit)", 2, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive no expire callbacks on edit", 0, expireCount);
}
// Now wait for the edited value to actually expire.
// Type expiration is 2s. The cache may add a grace period.
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Edited value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return e.second; });
}));
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListen()
{
@@ -944,7 +799,7 @@ DhtRunnerTester::testIdOps()
valueCountEdit++;
cv.notify_all();
});
auto listenToken = node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool /*expired*/) {
node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool /*expired*/) {
for (const auto& v : values) {
if (v->seq == 0)
CPPUNIT_ASSERT_EQUAL("v1"s, dht::unpackMsg<std::string>(v->data));
@@ -988,8 +843,6 @@ DhtRunnerTester::testIdOps()
});
std::unique_lock lk(mutex);
CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return valueCountEdit == 4u; }));
lk.unlock();
node2.cancelListen(key2, listenToken.get());
}
void
@@ -1080,451 +933,4 @@ DhtRunnerTester::testMultithread()
CPPUNIT_ASSERT_EQUAL(2 * N, putOkCount);
}
void
DhtRunnerTester::testGetAfterListen()
{
// Test that get() after a synced listen delivers values exactly once
// from the cache, and that the GetCallback return value is honored.
auto key = dht::InfoHash::get("getAfterListen");
// Put a value first
auto val = std::make_shared<dht::Value>("cached_val");
{
std::promise<bool> p;
node2.put(key, val, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Start a listen on node1 and wait for it to sync
std::mutex mtx;
std::condition_variable cv;
std::atomic_int listenAddCount {0};
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (!expired)
listenAddCount += vals.size();
cv.notify_all();
return true;
});
// Wait for listen to receive the value
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return listenAddCount.load() >= 1; }));
}
// Now do a get — should be served from the synced cache
auto vals = getFutureValue(node1.get(key));
CPPUNIT_ASSERT_MESSAGE("get() should return values", not vals.empty());
CPPUNIT_ASSERT(val->data == vals.front()->data);
// The listen callback should not have been called again by the get
std::this_thread::sleep_for(200ms);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Listen should not fire extra callbacks from get()", 1, listenAddCount.load());
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenDuplicatePut()
{
// Test that putting the exact same value multiple times only triggers
// one add callback in the listener (no duplicate delivery).
auto key = dht::InfoHash::get("listenDupPut");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.emplace_back(v->id, expired);
cv.notify_all();
return true;
});
// Put the same value 3 times
auto val = std::make_shared<dht::Value>("duplicate_test");
val->id = 123;
for (int i = 0; i < 3; i++) {
std::promise<bool> p;
node2.put(key, val, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for at least 1 add to arrive
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Give time for any spurious callbacks
std::this_thread::sleep_for(500ms);
// Should have exactly 1 add, 0 expires
{
std::lock_guard lk(mtx);
int addCount = 0, expireCount = 0;
for (const auto& e : events) {
if (e.second)
expireCount++;
else
addCount++;
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Duplicate puts should produce only 1 add callback", 1, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callbacks expected", 0, expireCount);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenMultiSourceExpire()
{
// Test that when multiple nodes store the same value, the listener sees
// exactly 1 add (no duplicates). When the value expires naturally, the
// listener sees exactly 1 expire.
static constexpr dht::ValueType::Id SHORT_TYPE_ID = 8888;
const dht::ValueType shortType {SHORT_TYPE_ID,
"short-lived",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(shortType);
node2.registerType(shortType);
auto key = dht::InfoHash::get("multiSourceExpire");
auto identity = dht::crypto::generateIdentity("MultiSourceTester");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events;
// node1 listens; node1 and node2 put
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.emplace_back(v->id, expired);
cv.notify_all();
return true;
});
// Both node1 and node2 put the same value (same id, same seq)
auto val = std::make_shared<dht::Value>("multi_source");
val->type = SHORT_TYPE_ID;
val->id = 55;
val->seq = 1;
val->sign(*identity.first);
{
std::promise<bool> p1, p2;
node1.put(key, val, [&](bool ok) { p1.set_value(ok); });
node2.put(key, val, [&](bool ok) { p2.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p1.get_future()));
CPPUNIT_ASSERT(getFutureValue(p2.get_future()));
}
// Wait for at least one add
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Allow time for duplicate callbacks
std::this_thread::sleep_for(1s);
// Should have exactly 1 add (deduplication in OpValueCache)
{
std::lock_guard lk(mtx);
int addCount = 0;
for (const auto& e : events)
if (!e.second)
addCount++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Multiple sources should produce only 1 add callback", 1, addCount);
}
// Wait for natural expiration (type is 2s + grace period)
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return e.second; });
}));
}
// Should have exactly 1 expire (no duplicate expires from multiple sources)
{
std::lock_guard lk(mtx);
int expireCount = 0;
for (const auto& e : events)
if (e.second)
expireCount++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive exactly 1 expire callback", 1, expireCount);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenEditChainThenExpire()
{
// Chain of 5 rapid edits (seq 1→5) on the same value, then natural expiration.
// Must see 5 adds (one per edit), 0 expires during edits, then exactly 1 expire.
static constexpr dht::ValueType::Id CHAIN_TYPE_ID = 7654;
const dht::ValueType chainType {CHAIN_TYPE_ID,
"chain-edit",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(chainType);
node2.registerType(chainType);
auto key = dht::InfoHash::get("editChainExpire");
auto identity = dht::crypto::generateIdentity("ChainEditTester");
std::mutex mtx;
std::condition_variable cv;
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::vector<Event> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.push_back({v->id, v->seq, expired});
cv.notify_all();
return true;
});
// Rapid chain of 5 edits — wait for the first to arrive before continuing
constexpr int NUM_EDITS = 5;
{
auto v = std::make_shared<dht::Value>("version1");
v->type = CHAIN_TYPE_ID;
v->id = 99;
v->seq = 1;
v->sign(*identity.first);
std::promise<bool> p;
node2.put(key, v, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for first add to arrive before sending more edits
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return !e.expired; });
}));
}
// Now send remaining edits rapidly
for (int seq = 2; seq <= NUM_EDITS; seq++) {
auto v = std::make_shared<dht::Value>("version" + std::to_string(seq));
v->type = CHAIN_TYPE_ID;
v->id = 99;
v->seq = seq;
v->sign(*identity.first);
std::promise<bool> p;
node2.put(key, v, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for edits to arrive (at least 2: initial + one or more updates)
// Note: rapid edits may coalesce — the DHT stores only the latest seq,
// so the listener may not see every intermediate version.
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.expired)
adds++;
return adds >= 2;
}));
}
// Wait a bit more for any remaining events
std::this_thread::sleep_for(1s);
// Verify: multiple adds, 0 expires during edits (no phantom expiry)
int totalAdds;
{
std::lock_guard lk(mtx);
totalAdds = 0;
int expireCount = 0;
for (const auto& e : events) {
if (e.expired)
expireCount++;
else
totalAdds++;
}
CPPUNIT_ASSERT_MESSAGE("Should receive at least 2 add callbacks", totalAdds >= 2);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expires during edit chain", 0, expireCount);
// Verify the latest add has the highest seq
uint16_t maxSeq = 0;
for (const auto& e : events)
if (!e.expired && e.seq > maxSeq)
maxSeq = e.seq;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Latest add should have the final seq", (uint16_t) NUM_EDITS, maxSeq);
}
// Wait for natural expiration
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.expired; });
}));
}
// Verify exactly 1 expire with the latest seq
{
std::lock_guard lk(mtx);
int expireCount = 0;
uint16_t expiredSeq = 0;
for (const auto& e : events) {
if (e.expired) {
expireCount++;
expiredSeq = e.seq;
}
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive exactly 1 expire", 1, expireCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the latest version", (uint16_t) NUM_EDITS, expiredSeq);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenMultiValuePartialExpire()
{
// Two values with different types: one short-lived (2s), one long-lived (60s).
// The short one expires while the long one stays. Verify isolation:
// no spurious expire for the long-lived value.
static constexpr dht::ValueType::Id SHORT_TYPE_ID = 3001;
static constexpr dht::ValueType::Id LONG_TYPE_ID = 3002;
const dht::ValueType shortType {SHORT_TYPE_ID,
"short-partial",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
const dht::ValueType longType {LONG_TYPE_ID,
"long-partial",
std::chrono::seconds(600),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(shortType);
node1.registerType(longType);
node2.registerType(shortType);
node2.registerType(longType);
auto key = dht::InfoHash::get("multiValuePartialExpire");
auto identity = dht::crypto::generateIdentity("PartialExpireTester");
std::mutex mtx;
std::condition_variable cv;
struct Event
{
dht::Value::Id id;
bool expired;
};
std::vector<Event> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.push_back({v->id, expired});
cv.notify_all();
return true;
});
// Put short-lived value (id=1)
auto shortVal = std::make_shared<dht::Value>("short");
shortVal->type = SHORT_TYPE_ID;
shortVal->id = 1;
shortVal->seq = 1;
shortVal->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, shortVal, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Put long-lived value (id=2)
auto longVal = std::make_shared<dht::Value>("long");
longVal->type = LONG_TYPE_ID;
longVal->id = 2;
longVal->seq = 1;
longVal->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, longVal, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for both adds
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.expired)
adds++;
return adds >= 2;
}));
}
// Wait for short-lived value to expire
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Short-lived value should expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.id == 1 && e.expired; });
}));
}
// Give extra time for any spurious callbacks
std::this_thread::sleep_for(1s);
// Verify: value 1 expired, value 2 did NOT expire
{
std::lock_guard lk(mtx);
bool longExpired = std::any_of(events.begin(), events.end(), [](const Event& e) {
return e.id == 2 && e.expired;
});
CPPUNIT_ASSERT_MESSAGE("Long-lived value should NOT be expired", !longExpired);
int shortExpires = 0;
for (const auto& e : events)
if (e.id == 1 && e.expired)
shortExpires++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Short-lived value should expire exactly once", 1, shortExpires);
}
node1.cancelListen(key, ftoken.get());
}
} // namespace test
-20
View File
@@ -18,9 +18,7 @@ class DhtRunnerTester : public CppUnit::TestFixture
CPPUNIT_TEST(testPutDuplicate);
CPPUNIT_TEST(testPutOverride);
CPPUNIT_TEST(testListen);
CPPUNIT_TEST(testListenValueEdit);
CPPUNIT_TEST(testListenLotOfBytes);
CPPUNIT_TEST(testStoreEmptyValue);
CPPUNIT_TEST(testIdOps);
CPPUNIT_TEST(testImportValuesPreservesRemoteQuota);
CPPUNIT_TEST(testExportValuesUsesSystemTime);
@@ -31,11 +29,6 @@ class DhtRunnerTester : public CppUnit::TestFixture
CPPUNIT_TEST(testBootstrapMissingNodeThenPutFails);
CPPUNIT_TEST(testShutdownCompletesWithPendingPut);
CPPUNIT_TEST(testSavedPortFallback);
CPPUNIT_TEST(testGetAfterListen);
CPPUNIT_TEST(testListenDuplicatePut);
CPPUNIT_TEST(testListenMultiSourceExpire);
CPPUNIT_TEST(testListenEditChainThenExpire);
CPPUNIT_TEST(testListenMultiValuePartialExpire);
CPPUNIT_TEST_SUITE_END();
dht::DhtRunner node1 {};
@@ -83,23 +76,10 @@ public:
void testBootstrapMissingNodeThenPutFails();
void testShutdownCompletesWithPendingPut();
void testSavedPortFallback();
void testGetAfterListen();
void testListenDuplicatePut();
void testListenMultiSourceExpire();
void testListenEditChainThenExpire();
void testListenMultiValuePartialExpire();
/**
* Test that editing a value only triggers an add callback, no expire
*/
void testListenValueEdit();
/**
* Test listen method with lot of datas
*/
void testListenLotOfBytes();
/**
* Test storing an empty value
*/
void testStoreEmptyValue();
/**
* Test multithread
*/
+280
View File
@@ -0,0 +1,280 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "test_mdns.h"
#include "../src/peer_discovery/dns_sd.h"
#include "../src/peer_discovery/mdns.h"
#include "../src/peer_discovery/mdns_transport.h"
#include <array>
#include <string>
namespace test {
using namespace dht::mdns;
CPPUNIT_TEST_SUITE_REGISTRATION(MdnsTester);
void
MdnsTester::testName()
{
const auto name = Name::fromString("Node._opendht._udp.local.");
CPPUNIT_ASSERT_EQUAL(std::string("Node._opendht._udp.local."), name.toString());
CPPUNIT_ASSERT(name == Name::fromString("node._OpenDHT._UDP.LOCAL"));
CPPUNIT_ASSERT_THROW(Name::fromString("bad..local."), ParseError);
CPPUNIT_ASSERT_THROW(Name::fromString(std::string(64, 'a') + ".local."), ParseError);
}
void
MdnsTester::testRecordRoundTrip()
{
const auto service = Name::fromString("_opendht._udp.local.");
const auto instance = Name::fromString("0123456789abcdef0123456789abcdef01234567._opendht._udp.local.");
const auto host = Name::fromString("0123456789abcdef0123456789abcdef01234567.local.");
Message message;
message.response = true;
message.authoritative = true;
message.answers.push_back({service, Type::PTR, CLASS_IN, false, 4500, PtrData {instance}});
message.additionals.push_back({
instance, Type::SRV, CLASS_IN, true, 120, SrvData {0, 0, 4222, host}
});
message.additionals.push_back(
{instance,
Type::TXT,
CLASS_IN,
true,
4500,
TxtData {{{'t', 'x', 't', 'v', 'e', 'r', 's', '=', '1'}, {'n', 'e', 't', '=', '0'}}}});
message.additionals.push_back({host, Type::A, CLASS_IN, true, 120, AData {{192, 0, 2, 1}}});
message.additionals.push_back(
{host, Type::AAAA, CLASS_IN, true, 120, AaaaData {{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}});
message.additionals.push_back({
host, Type::NSEC, CLASS_IN, true, 120, NsecData {host, {Type::A, Type::AAAA}}
});
const auto packet = encode(message);
const auto decoded = decode(packet.data(), packet.size());
CPPUNIT_ASSERT(decoded.response);
CPPUNIT_ASSERT(decoded.authoritative);
CPPUNIT_ASSERT_EQUAL(size_t(1), decoded.answers.size());
CPPUNIT_ASSERT_EQUAL(size_t(5), decoded.additionals.size());
CPPUNIT_ASSERT(decoded == message);
}
void
MdnsTester::testCompression()
{
Message message;
message.questions.push_back({Name::fromString("_opendht._udp.local."), Type::PTR, CLASS_IN, false});
message.questions.push_back({Name::fromString("node._opendht._udp.local."), Type::SRV, CLASS_IN, false});
const auto packet = encode(message);
CPPUNIT_ASSERT(std::find(packet.begin(), packet.end(), uint8_t {0xc0}) != packet.end());
CPPUNIT_ASSERT(decode(packet.data(), packet.size()) == message);
}
void
MdnsTester::testMalformedPackets()
{
const std::array<uint8_t, 3> truncated {
{0, 0, 0}
};
CPPUNIT_ASSERT_THROW(decode(truncated.data(), truncated.size()), ParseError);
const std::array<uint8_t, 18> compressionLoop {
{
0, 0,
0, 0,
0, 1,
0, 0,
0, 0,
0, 0,
0xc0, 0x0c,
0, static_cast<uint8_t>(Type::PTR),
0, CLASS_IN,
}
};
CPPUNIT_ASSERT_THROW(decode(compressionLoop.data(), compressionLoop.size()), ParseError);
}
void
MdnsTester::testPacketSizeLimit()
{
std::array<uint8_t, MAX_PACKET_SIZE + 1> oversized {};
CPPUNIT_ASSERT_THROW(decode(oversized.data(), oversized.size()), ParseError);
}
void
MdnsTester::testDnsSdAdvertisement()
{
dns_sd::Service service;
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
service.network = 42;
service.port = 4222;
service.addresses.emplace_back(AData {
{192, 0, 2, 1}
});
service.addresses.emplace_back(AaaaData {
{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
});
const auto message = dns_sd::announcement(service);
CPPUNIT_ASSERT(message.response);
CPPUNIT_ASSERT(message.authoritative);
CPPUNIT_ASSERT_EQUAL(size_t(1), message.answers.size());
CPPUNIT_ASSERT_EQUAL(size_t(4), message.additionals.size());
CPPUNIT_ASSERT_EQUAL(std::string("_opendht._udp.local."), message.answers.front().name.toString());
const auto resolved = dns_sd::resolve(message);
CPPUNIT_ASSERT(resolved);
CPPUNIT_ASSERT_EQUAL(service.nodeId, resolved->nodeId);
CPPUNIT_ASSERT_EQUAL(service.network, resolved->network);
CPPUNIT_ASSERT_EQUAL(service.port, resolved->port);
CPPUNIT_ASSERT_EQUAL(service.addresses.size(), resolved->addresses.size());
}
void
MdnsTester::testDnsSdGoodbye()
{
dns_sd::Service service;
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
service.port = 4222;
service.addresses.emplace_back(AData {
{192, 0, 2, 1}
});
const auto message = dns_sd::goodbye(service);
CPPUNIT_ASSERT(std::all_of(message.answers.begin(), message.answers.end(), [](const auto& record) {
return record.ttl == 0;
}));
CPPUNIT_ASSERT(std::all_of(message.additionals.begin(), message.additionals.end(), [](const auto& record) {
return record.ttl == 0;
}));
}
void
MdnsTester::testDnsSdValidation()
{
dns_sd::Service service;
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
service.network = 42;
service.port = 4222;
service.addresses.emplace_back(AData {
{192, 0, 2, 1}
});
auto message = dns_sd::announcement(service);
auto& txt = std::get<TxtData>(message.additionals[1].data);
txt.strings.push_back({'n', 'e', 't', '=', '4', '3'});
CPPUNIT_ASSERT(not dns_sd::resolve(message));
message = dns_sd::announcement(service);
auto& srv = std::get<SrvData>(message.additionals[0].data);
srv.target = Name::fromString("other.local.");
CPPUNIT_ASSERT(not dns_sd::resolve(message));
CPPUNIT_ASSERT_THROW(dns_sd::announcement({}), ParseError);
}
void
MdnsTester::testDnsSdQueryResponse()
{
dns_sd::Service service;
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
service.network = 42;
service.port = 4222;
service.addresses.emplace_back(AData {
{192, 0, 2, 1}
});
const auto query = dns_sd::browseQuery();
CPPUNIT_ASSERT(not query.response);
CPPUNIT_ASSERT_EQUAL(size_t(1), query.questions.size());
CPPUNIT_ASSERT(query.questions.front().name == dns_sd::serviceName());
CPPUNIT_ASSERT(query.questions.front().type == Type::PTR);
const auto response = dns_sd::respond(query, service);
CPPUNIT_ASSERT(response);
CPPUNIT_ASSERT(dns_sd::resolve(*response));
auto knownAnswerQuery = query;
knownAnswerQuery.answers = response->answers;
CPPUNIT_ASSERT(not dns_sd::respond(knownAnswerQuery, service));
knownAnswerQuery.answers.front().ttl = dns_sd::SERVICE_RECORD_TTL / 2;
CPPUNIT_ASSERT(dns_sd::respond(knownAnswerQuery, service));
Message metaQuery;
const auto metaName = Name::fromString("_services._dns-sd._udp.local.");
metaQuery.questions.push_back({metaName, Type::PTR, CLASS_IN, false});
const auto metaResponse = dns_sd::respond(metaQuery, service);
CPPUNIT_ASSERT(metaResponse);
CPPUNIT_ASSERT_EQUAL(size_t(1), metaResponse->answers.size());
CPPUNIT_ASSERT(metaResponse->answers.front().name == metaName);
CPPUNIT_ASSERT(std::get<PtrData>(metaResponse->answers.front().data).name == dns_sd::serviceName());
CPPUNIT_ASSERT(metaResponse->additionals.empty());
metaQuery.answers = metaResponse->answers;
CPPUNIT_ASSERT(not dns_sd::respond(metaQuery, service));
}
void
MdnsTester::testDnsSdCache()
{
dns_sd::Service service;
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
service.network = 42;
service.port = 4222;
service.addresses.emplace_back(AData {
{192, 0, 2, 1}
});
auto now = dns_sd::Cache::Clock::now();
dns_sd::Cache cache([&] { return now; });
auto ptr = dns_sd::announcement(service);
auto details = ptr;
ptr.additionals.clear();
details.answers.clear();
CPPUNIT_ASSERT(cache.update(ptr).empty());
const auto discovered = cache.update(details);
CPPUNIT_ASSERT_EQUAL(size_t(1), discovered.size());
CPPUNIT_ASSERT_EQUAL(service.nodeId, discovered.front().nodeId);
CPPUNIT_ASSERT_EQUAL(size_t(1), cache.services().size());
now += std::chrono::seconds(dns_sd::HOST_RECORD_TTL + 1);
CPPUNIT_ASSERT(cache.services().empty());
}
void
MdnsTester::testInterfaceScoping()
{
const std::vector<InterfaceAddress> addresses {
{1, 1, asio::ip::make_address("192.0.2.1"), true, false},
{1, 1, asio::ip::make_address("fe80::1"), true, false},
{2, 2, asio::ip::make_address("fe80::2"), true, false},
{3, 3, asio::ip::make_address("fe80::3"), false, false},
{4, 4, asio::ip::make_address("::1"), true, true },
};
const auto interfaces = groupInterfaces(addresses);
CPPUNIT_ASSERT_EQUAL(size_t(2), interfaces.size());
CPPUNIT_ASSERT_EQUAL(InterfaceId {1}, interfaces[0].id);
CPPUNIT_ASSERT_EQUAL(size_t(2), interfaces[0].addresses.size());
CPPUNIT_ASSERT_EQUAL(InterfaceId {2}, interfaces[1].id);
CPPUNIT_ASSERT_EQUAL(size_t(1), interfaces[1].addresses.size());
dns_sd::Service service;
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
service.port = 4222;
service.addresses = interfaces[0].addresses;
const auto announcement = dns_sd::announcement(service);
const auto resolved = dns_sd::resolve(announcement);
CPPUNIT_ASSERT(resolved);
CPPUNIT_ASSERT(resolved->addresses == interfaces[0].addresses);
}
} // namespace test
+40
View File
@@ -0,0 +1,40 @@
// 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 MdnsTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(MdnsTester);
CPPUNIT_TEST(testName);
CPPUNIT_TEST(testRecordRoundTrip);
CPPUNIT_TEST(testCompression);
CPPUNIT_TEST(testMalformedPackets);
CPPUNIT_TEST(testPacketSizeLimit);
CPPUNIT_TEST(testDnsSdAdvertisement);
CPPUNIT_TEST(testDnsSdGoodbye);
CPPUNIT_TEST(testDnsSdValidation);
CPPUNIT_TEST(testDnsSdQueryResponse);
CPPUNIT_TEST(testDnsSdCache);
CPPUNIT_TEST(testInterfaceScoping);
CPPUNIT_TEST_SUITE_END();
public:
void testName();
void testRecordRoundTrip();
void testCompression();
void testMalformedPackets();
void testPacketSizeLimit();
void testDnsSdAdvertisement();
void testDnsSdGoodbye();
void testDnsSdValidation();
void testDnsSdQueryResponse();
void testDnsSdCache();
void testInterfaceScoping();
};
} // namespace test
-674
View File
@@ -1,674 +0,0 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "test_opcache.h"
// Hack to test internal class
#include "../src/op_cache.cpp"
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(OpCacheTester);
namespace {
using Values = std::vector<std::shared_ptr<dht::Value>>;
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::shared_ptr<dht::Value>
makeValue(dht::Value::Id id, uint16_t seq, std::string data)
{
auto v = std::make_shared<dht::Value>();
v->id = id;
v->seq = seq;
v->data = {data.begin(), data.end()};
return v;
}
dht::ValueCallback
recordEvents(std::vector<Event>& events)
{
return [&events](const Values& vals, bool expired) {
for (const auto& v : vals)
events.push_back({v->id, v->seq, expired});
return true;
};
}
unsigned
count(const std::vector<Event>& events, bool expired)
{
unsigned n = 0;
for (const auto& e : events)
if (e.expired == expired)
n++;
return n;
}
} // namespace
void
OpCacheTester::setUp()
{}
void
OpCacheTester::tearDown()
{}
void
OpCacheTester::testBasicAddExpire()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(10, 0, "A");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT(cache.get(10));
std::vector<dht::Value::Id> ids = {v1->id};
cache.onValuesExpired(ids);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT(!cache.get(10));
}
void
OpCacheTester::testMultipleSources()
{
// Two sources report the same value: it must survive until both have
// expired it, and the user must see exactly one add and one expire.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v = makeValue(20, 0, "B");
cache.onValuesAdded({v}); // source A
cache.onValuesAdded({v}); // source B
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
cache.onValuesExpired({v}); // source A goes away
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value still held by source B", (size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
cache.onValuesExpired({v}); // source B goes away
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testEditResetsRefCount()
{
// A and B hold v1. A reports the edit v2: only A is known to hold v2, so
// the refcount restarts at 1. B then confirms v2 (refcount 2).
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(1, 1, "data1");
auto v2 = makeValue(1, 2, "data2");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(1)->seq);
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
cache.onValuesExpired({v2}); // A expires v2 -> nobody known to hold it
CPPUNIT_ASSERT_EQUAL_MESSAGE("Edit reset the refcount to the editing source", (size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
// Same scenario, but B confirms v2 before A expires it.
events.clear();
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits
cache.onValuesAdded({v2}); // B confirms
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL_MESSAGE("Still held by B", (size_t) 1, cache.size());
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testEditChainMultiSource()
{
// Rapid edit chain seen from two sources with interleaving: the user
// must see each version once as an add and a single final expire.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(7, 1, "v1");
auto v2 = makeValue(7, 2, "v2");
auto v3 = makeValue(7, 3, "v3");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A
cache.onValuesAdded({v3}); // A (B skipped v2)
cache.onValuesAdded({v2}); // B, late and stale: ignored
cache.onValuesAdded({v3}); // B
CPPUNIT_ASSERT_EQUAL(3u, count(events, false));
CPPUNIT_ASSERT_EQUAL((uint16_t) 3, cache.get(7)->seq);
cache.onValuesExpired({v3}); // A
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValuesExpired({v3}); // B
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((uint16_t) 3, events.back().seq);
}
void
OpCacheTester::testStaleAddIgnored()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(30, 10, "C");
auto v2 = makeValue(30, 20, "D");
auto v3 = makeValue(30, 5, "E");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL((uint16_t) 10, cache.get(30)->seq);
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL((uint16_t) 20, cache.get(30)->seq);
// Older version from a lagging source: neither stored nor counted
cache.onValuesAdded({v3});
CPPUNIT_ASSERT_EQUAL((uint16_t) 20, cache.get(30)->seq);
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_MESSAGE("Stale add must not have incremented the refcount", !cache.get(30));
}
void
OpCacheTester::testStaleExpireIgnored()
{
// A source that still holds an older version expiring it must not
// affect the newer version held by others.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(31, 1, "old");
auto v2 = makeValue(31, 2, "new");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits; B never receives v2
cache.onValuesExpired({v1}); // B expires its stale copy
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
cache.onValuesExpired({v2}); // A
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((uint16_t) 2, events.back().seq);
}
void
OpCacheTester::testCallbacks()
{
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(40, 0, "");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
// Duplicate from another source: counted, but not re-delivered
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
auto v2 = makeValue(40, 100, "MOD");
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
// The edit reset the refcount: one expire removes it
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testFilters()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(50, 0, "");
v1->type = 1;
auto v2 = makeValue(51, 0, "");
v2->type = 2;
cache.onValuesAdded({v1, v2});
auto res1 = cache.get(dht::Value::TypeFilter(1));
CPPUNIT_ASSERT_EQUAL((size_t) 1, res1.size());
CPPUNIT_ASSERT_EQUAL(v1->id, res1[0]->id);
auto res2 = cache.get(dht::Value::TypeFilter(2));
CPPUNIT_ASSERT_EQUAL((size_t) 1, res2.size());
CPPUNIT_ASSERT_EQUAL(v2->id, res2[0]->id);
CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.getValues().size());
}
void
OpCacheTester::testSyncStatus()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT(cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT(cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT(cache.isSynced());
}
void
OpCacheTester::testGetWhileSynced()
{
dht::SearchCache searchCache;
int getCallCount = 0;
bool doneCalled = false;
bool doneSuccess = false;
auto v1 = makeValue(100, 0, "X");
auto v2 = makeValue(101, 0, "Y");
auto query = std::make_shared<dht::Query>();
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback vcb,
dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1, v2}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool result = searchCache.get(
{},
query,
[&](const Values& vals) {
getCallCount++;
CPPUNIT_ASSERT_EQUAL((size_t) 2, vals.size());
return true;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT_MESSAGE("get() should be served from a synced cache", result);
CPPUNIT_ASSERT_EQUAL(1, getCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT(doneSuccess);
}
void
OpCacheTester::testGetWhileNotSynced()
{
// An unsynced cache must not answer a get: the caller falls back to the
// network and values are delivered once, from the network only.
dht::SearchCache searchCache;
std::vector<dht::Value::Id> deliveries;
auto v1 = makeValue(200, 0, "A");
auto query = std::make_shared<dht::Query>();
dht::SyncCallback storedScb;
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback vcb,
dht::SyncCallback scb) -> size_t {
storedScb = scb;
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool doneCalled = false;
auto gcb = [&](const Values& vals) {
for (const auto& v : vals)
deliveries.emplace_back(v->id);
return true;
};
auto dcb = [&](bool, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
};
CPPUNIT_ASSERT(!searchCache.get({}, query, gcb, dcb));
CPPUNIT_ASSERT(deliveries.empty());
CPPUNIT_ASSERT(!doneCalled);
// Simulated network reply
gcb({v1});
CPPUNIT_ASSERT_EQUAL((size_t) 1, deliveries.size());
storedScb(dht::ListenSyncStatus::SYNCED);
deliveries.clear();
CPPUNIT_ASSERT(searchCache.get({}, query, gcb, dcb));
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT_EQUAL((size_t) 1, deliveries.size());
}
void
OpCacheTester::testGetEmptySynced()
{
dht::SearchCache searchCache;
int getCallCount = 0;
bool doneCalled = false;
bool doneSuccess = false;
auto query = std::make_shared<dht::Query>();
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback,
dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool result = searchCache.get(
{},
query,
[&](const Values&) {
getCallCount++;
return true;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT(result);
CPPUNIT_ASSERT_EQUAL(0, getCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT(doneSuccess);
}
void
OpCacheTester::testGetWithoutCallback()
{
// Dht::query() passes no GetCallback: the cache cannot answer it and
// must let the network path run instead of calling an empty function.
dht::SearchCache searchCache;
auto v1 = makeValue(300, 0, "Q");
auto query = std::make_shared<dht::Query>();
searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
bool doneCalled = false;
CPPUNIT_ASSERT(!searchCache.get({}, query, {}, [&](bool, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
}));
CPPUNIT_ASSERT(!doneCalled);
}
void
OpCacheTester::testGetCallbackReturnsFalse()
{
// A consumer returning false means "I have enough", not a failure.
dht::SearchCache searchCache;
auto v1 = makeValue(1000, 0, "X");
auto query = std::make_shared<dht::Query>();
searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
bool doneCalled = false;
bool doneSuccess = false;
int gcbCallCount = 0;
bool result = searchCache.get(
{},
query,
[&](const Values&) {
gcbCallCount++;
return false;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT(result);
CPPUNIT_ASSERT_EQUAL(1, gcbCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT_MESSAGE("Stopping early is reported as success, like the network path", doneSuccess);
}
void
OpCacheTester::testTimestampOrdering()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(400, 0, "T");
auto t1 = std::chrono::system_clock::now();
auto t2 = t1 + std::chrono::seconds(10);
cache.onValue({v1}, false, t2);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValue({v1}, true, t1);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Stale expiration should be ignored", (size_t) 1, cache.size());
cache.onValue({v1}, true, t2);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testSourceDropKeepsValue()
{
// Two per-node ValueCaches feed one Search-level OpCache. When one node
// leaves the search (its ValueCache is destroyed, expiring everything it
// held) the value must stay visible as long as the other node holds it.
std::vector<Event> events;
dht::SearchCache searchCache;
auto query = std::make_shared<dht::Query>();
dht::ValueCallback toOpCache;
dht::SyncCallback toOpCacheSync;
searchCache.listen(recordEvents(events),
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
toOpCache = std::move(vcb);
toOpCacheSync = std::move(scb);
return 1;
});
dht::TypeStore types;
auto now = dht::clock::now();
auto v = makeValue(42, 0, "hello");
auto makeNodeCache = [&] {
return std::make_unique<dht::ValueCache>([&](const Values& vals, bool expired) { toOpCache(vals, expired); },
[&](dht::ListenSyncStatus s) { toOpCacheSync(s); });
};
auto nodeA = makeNodeCache();
auto nodeB = makeNodeCache();
nodeA->onValues({v}, {}, {}, types, now);
nodeA->onSynced(true);
nodeB->onValues({v}, {}, {}, types, now);
nodeB->onSynced(true);
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
CPPUNIT_ASSERT_EQUAL((size_t) 1, searchCache.get(dht::Value::Filter {}).size());
nodeA.reset();
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire while another node holds the value", 0u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 1, searchCache.get(dht::Value::Filter {}).size());
nodeB->onValues({v}, {}, {}, types, now + std::chrono::seconds(1));
CPPUNIT_ASSERT_EQUAL_MESSAGE("Refresh from the remaining node is not re-delivered", 1u, count(events, false));
nodeB.reset();
CPPUNIT_ASSERT_EQUAL_MESSAGE("Last holder gone: expire once", 1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, searchCache.get(dht::Value::Filter {}).size());
}
void
OpCacheTester::testHighChurnRefCountConsistency()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v = makeValue(800, 0, "CHURN");
for (int i = 0; i < 50; i++) {
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
}
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
// Overlapping churn: add A, add B, expire A, expire B
for (int i = 0; i < 20; i++) {
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value kept while one source remains", (size_t) 1, cache.size());
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
// Extra expirations from sources that never added must be no-ops
cache.onValuesExpired({v});
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testValueUpdateSingleSourcePhantom()
{
// Editing a value (same id, higher seq) is delivered as a single add of
// the new version, without a phantom expiration of the old one.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(700, 1, "OLD");
auto v2 = makeValue(700, 2, "NEW");
cache.onValuesAdded({v1});
events.clear();
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 1, events.size());
CPPUNIT_ASSERT(!events[0].expired);
CPPUNIT_ASSERT_EQUAL(v2->id, events[0].id);
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(700)->seq);
}
void
OpCacheTester::testExpireAfterUpdate()
{
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(1200, 1, "V1");
auto v2 = makeValue(1200, 5, "V2");
cache.onValuesAdded({v1});
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(1200)->seq);
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the updated one", v2->seq, events.back().seq);
// Once gone, an older version is accepted again
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(v1->seq, cache.get(1200)->seq);
}
void
OpCacheTester::testSingleSourceMode()
{
// Single-source caches (proxy client) get the same values re-delivered
// on refresh: duplicates must not be counted, one expire removes.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events), false);
auto v = makeValue(1100, 0, "DUP");
for (int i = 0; i < 10; i++)
cache.onValuesAdded({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testSingleSourceModeSearchCache()
{
std::vector<Event> events;
dht::SearchCache searchCache(false);
auto query = std::make_shared<dht::Query>();
dht::ValueCallback vcbStored;
searchCache.listen(recordEvents(events),
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback) -> size_t {
vcbStored = std::move(vcb);
return 1;
});
auto v = makeValue(1300, 0, "S");
vcbStored({v}, false);
vcbStored({v}, false); // re-delivery after listen restart
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
vcbStored({v}, true);
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, searchCache.get(dht::Value::Filter {}).size());
}
} // namespace test
-63
View File
@@ -1,63 +0,0 @@
// 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 OpCacheTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(OpCacheTester);
CPPUNIT_TEST(testBasicAddExpire);
CPPUNIT_TEST(testMultipleSources);
CPPUNIT_TEST(testEditResetsRefCount);
CPPUNIT_TEST(testEditChainMultiSource);
CPPUNIT_TEST(testStaleAddIgnored);
CPPUNIT_TEST(testStaleExpireIgnored);
CPPUNIT_TEST(testCallbacks);
CPPUNIT_TEST(testFilters);
CPPUNIT_TEST(testSyncStatus);
CPPUNIT_TEST(testGetWhileSynced);
CPPUNIT_TEST(testGetWhileNotSynced);
CPPUNIT_TEST(testGetEmptySynced);
CPPUNIT_TEST(testGetWithoutCallback);
CPPUNIT_TEST(testGetCallbackReturnsFalse);
CPPUNIT_TEST(testTimestampOrdering);
CPPUNIT_TEST(testSourceDropKeepsValue);
CPPUNIT_TEST(testHighChurnRefCountConsistency);
CPPUNIT_TEST(testValueUpdateSingleSourcePhantom);
CPPUNIT_TEST(testExpireAfterUpdate);
CPPUNIT_TEST(testSingleSourceMode);
CPPUNIT_TEST(testSingleSourceModeSearchCache);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void testBasicAddExpire();
void testMultipleSources();
void testEditResetsRefCount();
void testEditChainMultiSource();
void testStaleAddIgnored();
void testStaleExpireIgnored();
void testCallbacks();
void testFilters();
void testSyncStatus();
void testGetWhileSynced();
void testGetWhileNotSynced();
void testGetEmptySynced();
void testGetWithoutCallback();
void testGetCallbackReturnsFalse();
void testTimestampOrdering();
void testSourceDropKeepsValue();
void testHighChurnRefCountConsistency();
void testValueUpdateSingleSourcePhantom();
void testExpireAfterUpdate();
void testSingleSourceMode();
void testSingleSourceModeSearchCache();
};
} // namespace test
+21 -58
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: MIT
#include "test_peerdiscovery.h"
#include "opendht/value.h"
#include <mutex>
#include <condition_variable>
@@ -11,24 +10,8 @@ namespace test {
using namespace std::literals;
constexpr unsigned MULTICAST_PORT = 2222;
constexpr auto DHT_NODE_NAME {"dht"sv};
constexpr auto JAMI_NODE_NAME {"jami"sv};
struct DhtNode
{
dht::InfoHash nodeid;
in_port_t node_port;
dht::NetId nid;
MSGPACK_DEFINE(nodeid, node_port, nid)
};
struct JamiNode
{
int num;
char cha;
std::string str;
MSGPACK_DEFINE(num, cha, str)
};
constexpr dht::NetId NETWORK_ID = 10;
constexpr in_port_t NODE_PORT = 50000;
CPPUNIT_TEST_SUITE_REGISTRATION(PeerDiscoveryTester);
@@ -39,60 +22,40 @@ PeerDiscoveryTester::setUp()
void
PeerDiscoveryTester::testMulticastToTwoNodes()
{
DhtNode dhtNode;
dhtNode.nid = 10;
dhtNode.node_port = 50000;
dhtNode.nodeid = dht::InfoHash::get("opendht01");
JamiNode jamiNode;
jamiNode.num = 100;
jamiNode.cha = 'a';
jamiNode.str = "jami01";
const auto nodeId = dht::InfoHash::get("opendht01");
std::mutex lock;
std::condition_variable cv;
unsigned countDht {0};
unsigned countJami {0};
unsigned discovered {0};
unsigned wrongNetwork {0};
{
std::unique_lock l(lock);
dht::PeerDiscovery testDht(MULTICAST_PORT);
dht::PeerDiscovery testJami(MULTICAST_PORT);
dht::PeerDiscovery publisher;
dht::PeerDiscovery browser;
dht::PeerDiscovery otherNetworkBrowser;
testJami.startDiscovery<DhtNode>(DHT_NODE_NAME, [&](DhtNode&& v, dht::SockAddr&&) {
CPPUNIT_ASSERT_EQUAL(dhtNode.node_port, v.node_port);
CPPUNIT_ASSERT_EQUAL(dhtNode.nodeid, v.nodeid);
CPPUNIT_ASSERT_EQUAL(dhtNode.nid, v.nid);
browser.startDiscovery(NETWORK_ID, [&](const dht::InfoHash& peerId, dht::SockAddr&& address) {
CPPUNIT_ASSERT_EQUAL(nodeId, peerId);
CPPUNIT_ASSERT_EQUAL(NODE_PORT, address.getPort());
{
std::lock_guard l(lock);
countDht++;
discovered++;
}
cv.notify_all();
});
testJami.startDiscovery(JAMI_NODE_NAME, [&](msgpack::object&& obj, dht::SockAddr&&) {
auto v = obj.as<JamiNode>();
CPPUNIT_ASSERT_EQUAL(jamiNode.num, v.num);
CPPUNIT_ASSERT_EQUAL(jamiNode.cha, v.cha);
CPPUNIT_ASSERT_EQUAL(jamiNode.str, v.str);
{
std::lock_guard l(lock);
countJami++;
}
cv.notify_all();
otherNetworkBrowser.startDiscovery(NETWORK_ID + 1, [&](const dht::InfoHash&, dht::SockAddr&&) {
std::lock_guard l(lock);
wrongNetwork++;
});
testDht.startPublish(DHT_NODE_NAME, dhtNode);
CPPUNIT_ASSERT(cv.wait_for(l, std::chrono::seconds(5), [&] { return countDht > 0; }));
testDht.startPublish(JAMI_NODE_NAME, jamiNode);
CPPUNIT_ASSERT(cv.wait_for(l, std::chrono::seconds(5), [&] { return countDht > 1 and countJami > 0; }));
// we don't verify count values since its a continious multicasting
publisher.startPublish(nodeId, NETWORK_ID, NODE_PORT);
CPPUNIT_ASSERT(cv.wait_for(l, std::chrono::seconds(5), [&] { return discovered > 0; }));
CPPUNIT_ASSERT_EQUAL(0u, wrongNetwork);
l.unlock();
testDht.stopPublish(DHT_NODE_NAME);
testDht.stopPublish(JAMI_NODE_NAME);
testJami.stopDiscovery(DHT_NODE_NAME);
testJami.stopDiscovery(JAMI_NODE_NAME);
publisher.stopPublish();
browser.stopDiscovery();
otherNetworkBrowser.stopDiscovery();
}
}
-560
View File
@@ -1,560 +0,0 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "test_valuecache.h"
#include "../src/value_cache.h"
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(ValueCacheTester);
void
ValueCacheTester::setUp()
{}
void
ValueCacheTester::tearDown()
{}
void
ValueCacheTester::testUpdate()
{
int addCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (!expired)
addCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 1;
v1->data = {'A'};
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 2;
v2->data = {'B'};
auto now = std::chrono::steady_clock::now();
// Add v1
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
// Update v1 -> v2
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Callback should be called for update", 2, addCount);
}
void
ValueCacheTester::testUpdateExpiration()
{
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 1;
v1->data = {'A'};
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 2;
v2->data = {'B'};
auto now = std::chrono::steady_clock::now();
// Add v1
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
// Update v1 -> v2: only the new value is emitted as an add,
// no expire callback for the old value.
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(2, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callback on edit", 0, expireCount);
}
void
ValueCacheTester::testExpiration()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 10;
v1->data = {'A'};
// Default expiration is 10 minutes
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
// Advance time past expiration
auto future = now + std::chrono::minutes(11);
// expireValues returns next expiration time
cache.expireValues(future);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testRefresh()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 20;
v1->data = {'B'};
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
// Refresh at +5 mins
auto later = now + std::chrono::minutes(5);
cache.onValues({}, {v1->id}, {}, types, later);
// Should not expire at +11 mins (original expiration was +10, so +10 from start)
// But refresh should extend it to +10 from 'later' = +15 from start.
auto checkTime = now + std::chrono::minutes(11);
cache.expireValues(checkTime);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Should expire at +16 mins
auto future = now + std::chrono::minutes(16);
cache.expireValues(future);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
}
void
ValueCacheTester::testExplicitExpiration()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 30;
v1->data = {'C'};
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
cache.onValues({}, {}, {v1->id}, types, now);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testClear()
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
auto v2 = std::make_shared<dht::Value>();
v2->id = 2;
cache.onValues({v1, v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.size());
auto cbs = cache.clear();
// Execute callbacks
for (auto& cb : cbs)
cb();
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testSyncStatus()
{
std::vector<dht::ListenSyncStatus> statuses;
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {},
[&](dht::ListenSyncStatus status) { statuses.push_back(status); });
CPPUNIT_ASSERT_EQUAL((size_t) 1, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::ADDED);
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
// Calling onSynced(true) again should not trigger the callback
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
cache.onSynced(false);
CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::UNSYNCED);
// Calling onSynced(false) again should not trigger the callback
cache.onSynced(false);
CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
// Set to synced before destruction to test UNSYNCED on destruction
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 4, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
}
// Destructor should have been called
CPPUNIT_ASSERT_EQUAL((size_t) 6, statuses.size());
CPPUNIT_ASSERT(statuses[4] == dht::ListenSyncStatus::UNSYNCED);
CPPUNIT_ASSERT(statuses[5] == dht::ListenSyncStatus::REMOVED);
}
void
ValueCacheTester::testMaxValues()
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
// MAX_VALUES is 4096
for (int i = 0; i < 4100; ++i) {
auto v = std::make_shared<dht::Value>();
v->id = (dht::Value::Id) i + 1;
cache.onValues({v}, {}, {}, types, now + std::chrono::milliseconds(i));
}
// Should be capped at 4096
CPPUNIT_ASSERT_EQUAL((size_t) 4096, cache.size());
}
void
ValueCacheTester::testUpdateTypeExpiration()
{
// Test that when a value is updated with a different type, the expiration
// uses the new type's expiration period, not the old one.
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
// Register a custom type with a short expiration (2 minutes)
dht::TypeStore types;
dht::ValueType shortType(42, "short", std::chrono::minutes(2));
types.registerType(shortType);
// Register a custom type with a long expiration (30 minutes)
dht::ValueType longType(99, "long", std::chrono::minutes(30));
types.registerType(longType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 42; // short type: 2 min expiration
v1->data = {'A'};
auto now = std::chrono::steady_clock::now();
// Add v1 with short type
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
// Update with new type (long expiration)
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 1;
v2->type = 99; // long type: 30 min expiration
v2->data = {'B'};
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(2, addCount);
// At +3 minutes: should NOT be expired (long type has 30 min expiration)
// Before the fix, the old type (2 min) would be used, causing expiration here
expireCount = 0;
auto at3min = now + std::chrono::minutes(3);
cache.expireValues(at3min);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value updated to long-expiry type should NOT expire at 3 min", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// At +31 minutes: should be expired
auto at31min = now + std::chrono::minutes(31);
cache.expireValues(at31min);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testSameSeqEditIgnored()
{
// Like OpValueCache, an edit is only recognized by an increasing seq:
// a different payload with the same seq is a refresh, not an edit.
int addCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (!expired)
addCount += vals.size();
});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 3;
v1->data = {'A'};
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto sameSeq = std::make_shared<dht::Value>();
sameSeq->id = 1;
sameSeq->seq = 3;
sameSeq->data = {'B'};
cache.onValues({sameSeq}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto older = std::make_shared<dht::Value>();
older->id = 1;
older->seq = 2;
older->data = {'C'};
cache.onValues({older}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto newer = std::make_shared<dht::Value>();
newer->id = 1;
newer->seq = 4;
newer->data = {'D'};
cache.onValues({newer}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(2, addCount);
}
void
ValueCacheTester::testShortExpirationRefreshLost()
{
// Demonstrates that a value with a short expiration type survives
// past its nominal expiration when the listen is synced, thanks to
// a proportional grace period (= typeExpiration).
//
// For a 5s type: effective expiration when synced = expiration + 5s = 10s
//
// Scenario:
// t=0 : Value V added → nominal expiration t+5s, effective t+10s
// t=6 : Past nominal but within grace → alive
// t=8 : Refresh arrives → V refreshed → expiration reset to t8+5s, effective t8+10s=t18s
// t=14: Still alive (within refreshed effective range)
// t=20: No more refreshes → past t8+10s=t18s → expired
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
// Register a type with a very short expiration (5 seconds)
dht::TypeStore types;
dht::ValueType shortType(100, "short_lived", std::chrono::seconds(5));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 100;
v1->data = {'V'};
auto t0 = std::chrono::steady_clock::now();
// t=0: Listen response arrives with V
cache.onValues({v1}, {}, {}, types, t0);
CPPUNIT_ASSERT_EQUAL(1, addCount);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Mark cache as synced (listen is active)
cache.onSynced(true);
// t=6: Past nominal 5s expiration, but grace (5s) gives effective 10s
auto t6 = t0 + std::chrono::seconds(6);
cache.expireValues(t6);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past nominal expiration thanks to proportional grace",
0,
expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=8: Value refresh arrives → expiration reset to t8+5s=t13s, effective t8+10s=t18s
auto t8 = t0 + std::chrono::seconds(8);
cache.onValues({}, {v1->id}, {}, types, t8);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=14: Within refreshed effective range (t18s) → alive
auto t14 = t0 + std::chrono::seconds(14);
cache.expireValues(t14);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past short expiration after refresh", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=20: No more refreshes → past effective t18s → must expire
auto t20 = t0 + std::chrono::seconds(20);
cache.expireValues(t20);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value must eventually expire even while synced if server stops refreshing",
1,
expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testShortExpirationHighChurn()
{
// Simulates high churn: multiple rapid add/expire/refresh cycles with
// short-lived values, verifying the cache stays consistent.
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(101, "fast_churn", std::chrono::seconds(3));
types.registerType(shortType);
auto now = std::chrono::steady_clock::now();
// Simulate 10 churn cycles: add value, advance time, refresh, advance, expire
for (int cycle = 0; cycle < 10; cycle++) {
auto v = std::make_shared<dht::Value>();
v->id = static_cast<dht::Value::Id>(500 + cycle);
v->type = 101;
v->data = {static_cast<uint8_t>('A' + cycle)};
auto t = now + std::chrono::seconds(cycle * 10);
// Add the value
cache.onValues({v}, {}, {}, types, t);
// Refresh halfway through
auto tRefresh = t + std::chrono::seconds(2);
cache.onValues({}, {v->id}, {}, types, tRefresh);
// Explicit expire from server
auto tExpire = t + std::chrono::seconds(8);
cache.onValues({}, {}, {v->id}, types, tExpire);
}
// All 10 values were added and explicitly expired
CPPUNIT_ASSERT_EQUAL(10, addCount);
CPPUNIT_ASSERT_EQUAL(10, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testGracePeriodWhileSynced()
{
// Verify that when the listen is synced, the proportional grace period
// (= typeExpiration) delays time-based expiration but doesn't prevent
// it entirely. For a 2s type: effective expiration = 2s + 2s = 4s.
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(110, "short", std::chrono::seconds(2));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 110;
v1->data = {'X'};
auto t0 = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, t0);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Mark as synced
cache.onSynced(true);
// t=3: Past nominal 2s, within effective 4s → grace protects
auto t3 = t0 + std::chrono::seconds(3);
cache.expireValues(t3);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: grace period should prevent early expiry", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=5: Past effective 4s (2s + 2s grace) → must expire
auto t5 = t0 + std::chrono::seconds(5);
cache.expireValues(t5);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: value must eventually expire after grace period", 1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testUnsyncExpiresImmediately()
{
// When the cache goes from synced to unsynced, the grace period is
// removed and already-past-due values expire on the next check.
// Type expiration = 5s. Grace when synced = 5s → effective = 10s.
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(120, "short", std::chrono::seconds(5));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 120;
v1->data = {'U'};
auto t0 = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, t0);
cache.onSynced(true);
// t=7: past nominal 5s, within effective 10s while synced → alive
auto t7 = t0 + std::chrono::seconds(7);
cache.expireValues(t7);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
// Now unsync (node went away / connectivity loss)
cache.onSynced(false);
// Same time point t=7: past nominal 5s, no grace → expires
cache.expireValues(t7);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Unsynced cache should immediately expire past-due values", 1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
} // namespace test
-49
View File
@@ -1,49 +0,0 @@
// 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 ValueCacheTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(ValueCacheTester);
CPPUNIT_TEST(testUpdate);
CPPUNIT_TEST(testUpdateExpiration);
CPPUNIT_TEST(testExpiration);
CPPUNIT_TEST(testRefresh);
CPPUNIT_TEST(testExplicitExpiration);
CPPUNIT_TEST(testClear);
CPPUNIT_TEST(testSyncStatus);
CPPUNIT_TEST(testMaxValues);
CPPUNIT_TEST(testUpdateTypeExpiration);
CPPUNIT_TEST(testSameSeqEditIgnored);
CPPUNIT_TEST(testShortExpirationRefreshLost);
CPPUNIT_TEST(testShortExpirationHighChurn);
CPPUNIT_TEST(testGracePeriodWhileSynced);
CPPUNIT_TEST(testUnsyncExpiresImmediately);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void testUpdate();
void testUpdateExpiration();
void testExpiration();
void testRefresh();
void testExplicitExpiration();
void testClear();
void testSyncStatus();
void testMaxValues();
void testUpdateTypeExpiration();
void testSameSeqEditIgnored();
void testShortExpirationRefreshLost();
void testShortExpirationHighChurn();
void testGracePeriodWhileSynced();
void testUnsyncExpiresImmediately();
};
} // namespace test