From 652ae27eeb4d9c47c0e74bd2162ac43bd126b816 Mon Sep 17 00:00:00 2001 From: jfreegman Date: Mon, 31 Jan 2022 12:03:25 -0500 Subject: [PATCH 1/2] Add DHT queries to private API This commit adds functionality for clients to interact with the DHT, sending getnodes requests to their peers and receiving nodes in getnodes responses. --- .../docker/tox-bootstrapd.sha256 | 2 +- toxcore/DHT.c | 11 +++ toxcore/DHT.h | 6 +- toxcore/tox.c | 88 +++++++++++++++++ toxcore/tox_api.c | 3 + toxcore/tox_private.h | 95 +++++++++++++++++++ 6 files changed, 203 insertions(+), 2 deletions(-) diff --git a/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 b/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 index 00089e3543..c852264b5a 100644 --- a/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 +++ b/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 @@ -1 +1 @@ -502cc22df74fa369b2c09d117176705a1e801726db6f8360c688aee90973fa22 /usr/local/bin/tox-bootstrapd +dd7740d26e60b20495e1c9145ee820d32cbb4996f0a6104f2e3e4705b16244f7 /usr/local/bin/tox-bootstrapd diff --git a/toxcore/DHT.c b/toxcore/DHT.c index 1f4dba5228..1d6a492d9c 100644 --- a/toxcore/DHT.c +++ b/toxcore/DHT.c @@ -105,6 +105,8 @@ struct DHT { Node_format to_bootstrap[MAX_CLOSE_TO_BOOTSTRAP_NODES]; unsigned int num_to_bootstrap; + + dht_get_nodes_response_cb *get_nodes_response; }; const uint8_t *dht_friend_public_key(const DHT_Friend *dht_friend) @@ -1552,6 +1554,10 @@ static int handle_sendnodes_ipv6(void *object, const IP_Port *source, const uint if (ipport_isset(&plain_nodes[i].ip_port)) { ping_node_from_getnodes_ok(dht, plain_nodes[i].public_key, &plain_nodes[i].ip_port); returnedip_ports(dht, &plain_nodes[i].ip_port, plain_nodes[i].public_key, packet + 1); + + if (dht->get_nodes_response) { + dht->get_nodes_response(dht, &plain_nodes[i], userdata); + } } } @@ -2470,6 +2476,11 @@ static int cryptopacket_handle(void *object, const IP_Port *source, const uint8_ return 1; } +void dht_callback_get_nodes_response(DHT *dht, dht_get_nodes_response_cb *function) +{ + dht->get_nodes_response = function; +} + /*----------------------------------------------------------------------------------*/ DHT *new_dht(const Logger *log, Mono_Time *mono_time, Networking_Core *net, bool holepunching_enabled) diff --git a/toxcore/DHT.h b/toxcore/DHT.h index 09a6f4e4dd..3bcea771dd 100644 --- a/toxcore/DHT.h +++ b/toxcore/DHT.h @@ -243,7 +243,6 @@ void dht_get_shared_key_recv(DHT *dht, uint8_t *shared_key, const uint8_t *publi non_null() void dht_get_shared_key_sent(DHT *dht, uint8_t *shared_key, const uint8_t *public_key); - /** Sends a getnodes request to `ip_port` with the public key `public_key` for nodes * that are close to `client_id`. * @@ -254,6 +253,11 @@ bool dht_getnodes(DHT *dht, const IP_Port *ip_port, const uint8_t *public_key, c typedef void dht_ip_cb(void *object, int32_t number, const IP_Port *ip_port); +typedef void dht_get_nodes_response_cb(const DHT *dht, const Node_format *node, void *userdata); + +/** Sets the callback to be triggered on a getnodes response. */ +void dht_callback_get_nodes_response(DHT *dht, dht_get_nodes_response_cb *function); + /** Add a new friend to the friends list. * public_key must be CRYPTO_PUBLIC_KEY_SIZE bytes long. * diff --git a/toxcore/tox.c b/toxcore/tox.c index e0157b8bae..db3b9fd9b1 100644 --- a/toxcore/tox.c +++ b/toxcore/tox.c @@ -21,6 +21,7 @@ #include "group.h" #include "logger.h" #include "mono_time.h" +#include "network.h" #include "../toxencryptsave/defines.h" @@ -35,6 +36,10 @@ static_assert(TOX_HASH_LENGTH == CRYPTO_SHA256_SIZE, "TOX_HASH_LENGTH is assumed to be equal to CRYPTO_SHA256_SIZE"); static_assert(FILE_ID_LENGTH == CRYPTO_SYMMETRIC_KEY_SIZE, "FILE_ID_LENGTH is assumed to be equal to CRYPTO_SYMMETRIC_KEY_SIZE"); +static_assert(TOX_DHT_NODE_IP_STRING_SIZE == IP_NTOA_LEN, + "TOX_DHT_NODE_IP_STRING_SIZE is assumed to be equal to IP_NTOA_LEN"); +static_assert(TOX_DHT_NODE_PUBLIC_KEY_SIZE == CRYPTO_PUBLIC_KEY_SIZE, + "TOX_DHT_NODE_PUBLIC_KEY_SIZE is assumed to be equal to CRYPTO_PUBLIC_KEY_SIZE"); static_assert(TOX_FILE_ID_LENGTH == CRYPTO_SYMMETRIC_KEY_SIZE, "TOX_FILE_ID_LENGTH is assumed to be equal to CRYPTO_SYMMETRIC_KEY_SIZE"); static_assert(TOX_FILE_ID_LENGTH == TOX_HASH_LENGTH, @@ -74,6 +79,7 @@ struct Tox { tox_conference_title_cb *conference_title_callback; tox_conference_peer_name_cb *conference_peer_name_callback; tox_conference_peer_list_changed_cb *conference_peer_list_changed_callback; + tox_dht_get_nodes_response_cb *dht_get_nodes_response_callback; tox_friend_lossy_packet_cb *friend_lossy_packet_callback_per_pktid[UINT8_MAX + 1]; tox_friend_lossless_packet_cb *friend_lossless_packet_callback_per_pktid[UINT8_MAX + 1]; @@ -314,6 +320,21 @@ static void tox_conference_peer_list_changed_handler(Messenger *m, uint32_t conf } } +static void tox_dht_get_nodes_response_handler(const DHT *dht, const Node_format *node, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->dht_get_nodes_response_callback == nullptr) { + return; + } + + char ip[IP_NTOA_LEN]; + ip_ntoa(&node->ip_port.ip, ip, sizeof(ip)); + + tox_data->tox->dht_get_nodes_response_callback(tox_data->tox, node->public_key, ip, net_ntohs(node->ip_port.port), + tox_data->user_data); +} + non_null(1, 4) nullable(6) static void tox_friend_lossy_packet_handler(Messenger *m, uint32_t friend_number, uint8_t packet_id, const uint8_t *data, size_t length, void *user_data) @@ -616,6 +637,7 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error) callback_file_reqchunk(tox->m, tox_file_chunk_request_handler); callback_file_sendrequest(tox->m, tox_file_recv_handler); callback_file_data(tox->m, tox_file_recv_chunk_handler); + dht_callback_get_nodes_response(tox->m->dht, tox_dht_get_nodes_response_handler); g_callback_group_invite(tox->m->conferences_object, tox_conference_invite_handler); g_callback_group_connected(tox->m->conferences_object, tox_conference_connected_handler); g_callback_group_message(tox->m->conferences_object, tox_conference_message_handler); @@ -2553,3 +2575,69 @@ uint16_t tox_self_get_tcp_port(const Tox *tox, Tox_Err_Get_Port *error) unlock(tox); return 0; } + +void tox_callback_dht_get_nodes_response(Tox *tox, tox_dht_get_nodes_response_cb *callback) +{ + assert(tox != nullptr); + tox->dht_get_nodes_response_callback = callback; +} + +bool tox_dht_get_nodes(const Tox *tox, const uint8_t *public_key, const char *ip, uint16_t port, + const uint8_t *target_public_key, Tox_Err_Dht_Get_Nodes *error) +{ + assert(tox != nullptr); + + lock(tox); + + if (tox->m->options.udp_disabled) { + SET_ERROR_PARAMETER(error, TOX_ERR_DHT_GET_NODES_UDP_DISABLED); + unlock(tox); + return false; + } + + if (public_key == nullptr || ip == nullptr || target_public_key == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_DHT_GET_NODES_NULL); + unlock(tox); + return false; + } + + if (port == 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_DHT_GET_NODES_BAD_PORT); + unlock(tox); + return false; + } + + IP_Port *root; + + const int32_t count = net_getipport(ip, &root, TOX_SOCK_DGRAM); + + if (count < 1) { + SET_ERROR_PARAMETER(error, TOX_ERR_DHT_GET_NODES_BAD_IP); + net_freeipport(root); + unlock(tox); + return false; + } + + bool success = 0; + + for (int32_t i = 0; i < count; ++i) { + root[i].port = net_htons(port); + + if (dht_getnodes(tox->m->dht, &root[i], public_key, target_public_key)) { + success = true; + } + } + + unlock(tox); + + net_freeipport(root); + + if (!success) { + SET_ERROR_PARAMETER(error, TOX_ERR_DHT_GET_NODES_FAIL); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_DHT_GET_NODES_OK); + + return true; +} diff --git a/toxcore/tox_api.c b/toxcore/tox_api.c index 89524ae4e2..149bb59ca3 100644 --- a/toxcore/tox_api.c +++ b/toxcore/tox_api.c @@ -2,6 +2,7 @@ * Copyright © 2016-2021 The TokTok team. */ #include "tox.h" +#include "tox_private.h" #include #include @@ -41,6 +42,8 @@ CONST_FUNCTION(hash_length, HASH_LENGTH) CONST_FUNCTION(file_id_length, FILE_ID_LENGTH) CONST_FUNCTION(max_filename_length, MAX_FILENAME_LENGTH) CONST_FUNCTION(max_hostname_length, MAX_HOSTNAME_LENGTH) +CONST_FUNCTION(dht_node_ip_string_size, DHT_NODE_IP_STRING_SIZE) +CONST_FUNCTION(dht_node_public_key_size, DHT_NODE_PUBLIC_KEY_SIZE) #define ACCESSORS(type, ns, name) \ diff --git a/toxcore/tox_private.h b/toxcore/tox_private.h index 593282f7c8..5e16d1a9b6 100644 --- a/toxcore/tox_private.h +++ b/toxcore/tox_private.h @@ -10,6 +10,8 @@ #include #include +#include "DHT.h" + #ifdef __cplusplus extern "C" { #endif @@ -37,6 +39,99 @@ void tox_callback_friend_lossless_packet_per_pktid(Tox *tox, tox_friend_lossless void tox_set_av_object(Tox *tox, void *object); void *tox_get_av_object(const Tox *tox); + +/******************************************************************************* + * + * :: DHT network queries. + * + ******************************************************************************/ + + + + +/** + * The minimum size of an IP string buffer in bytes. + */ +#define TOX_DHT_NODE_IP_STRING_SIZE 96 + +//!TOKSTYLE- +uint32_t tox_dht_node_ip_string_size(void); +//!TOKSTYLE+ + +/** + * The size of a DHT node public key in bytes. + */ +#define TOX_DHT_NODE_PUBLIC_KEY_SIZE 32 + +//!TOKSTYLE- +uint32_t tox_dht_node_public_key_size(void); +//!TOKSTYLE+ + +/** + * @param public_key The node's public key. + * @param ip The node's IP address, represented as a null terminated string. + * @param port The node's port. + */ +typedef void tox_dht_get_nodes_response_cb(Tox *tox, const uint8_t *public_key, const char *ip, uint16_t port, + void *user_data); + + +/** + * Set the callback for the `dht_get_nodes_response` event. Pass NULL to unset. + * + * This event is triggered when a getnodes response is received from a DHT peer. + */ +void tox_callback_dht_get_nodes_response(Tox *tox, tox_dht_get_nodes_response_cb *callback); + + +typedef enum Tox_Err_Dht_Get_Nodes { + /** + * The function returned successfully. + */ + TOX_ERR_DHT_GET_NODES_OK, + + /** + * UDP is disabled in tox options; the DHT can only be queried when UDP is enabled. + */ + TOX_ERR_DHT_GET_NODES_UDP_DISABLED, + + /** + * One of the arguments to the function was NULL when it was not expected. + */ + TOX_ERR_DHT_GET_NODES_NULL, + + /** + * The supplied port is invalid. + */ + TOX_ERR_DHT_GET_NODES_BAD_PORT, + + /** + * The supplied IP address is invalid. + */ + TOX_ERR_DHT_GET_NODES_BAD_IP, + + /** + * The getnodes request failed. This usually means the packet failed to send. + */ + TOX_ERR_DHT_GET_NODES_FAIL, +} Tox_Err_Dht_Get_Nodes; + +/** + * This function sends a getnodes request to a DHT node for its peers that + * are "close" to the passed target public key according to the distance metric used + * by the DHT implementation. + * + * @param public_key The public key of the node that we wish to query. This key must be + * at least `TOX_DHT_NODE_PUBLIC_KEY_SIZE` bytes in length. + * @param ip A NULL terminated string representing the IP address of the node we wish to query. + * @param port The port of the node we wish to query. + * @param target_public_key The public key for which we want to find close nodes. + * + * @return true on success. + */ +bool tox_dht_get_nodes(const Tox *tox, const uint8_t *public_key, const char *ip, uint16_t port, + const uint8_t *target_public_key, Tox_Err_Dht_Get_Nodes *error); + #ifdef __cplusplus } #endif From 73ba0504b089d4945ca5304143da92bcf0b1242e Mon Sep 17 00:00:00 2001 From: jfreegman Date: Thu, 10 Feb 2022 10:20:37 -0500 Subject: [PATCH 2/2] Add autotest for dht getnodes API The test creates a small DHT and makes sure that each peer is able to crawl the full network --- CMakeLists.txt | 1 + auto_tests/auto_test_support.h | 2 +- auto_tests/dht_getnodes_api_test.c | 152 ++++++++++++++++++ .../docker/tox-bootstrapd.sha256 | 2 +- toxcore/BUILD.bazel | 4 +- toxcore/DHT.h | 1 + toxcore/tox.c | 3 +- 7 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 auto_tests/dht_getnodes_api_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 024c5723ca..7937fdfbde 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -470,6 +470,7 @@ auto_test(conference_simple) auto_test(conference_two) auto_test(crypto) #auto_test(dht) # Doesn't work with UNITY_BUILD. +auto_test(dht_getnodes_api) auto_test(encryptsave) auto_test(file_transfer) auto_test(file_saving) diff --git a/auto_tests/auto_test_support.h b/auto_tests/auto_test_support.h index b4f3a2088a..16faa40f63 100644 --- a/auto_tests/auto_test_support.h +++ b/auto_tests/auto_test_support.h @@ -35,7 +35,7 @@ void set_mono_time_callback(AutoTox *tox); typedef enum Graph_Type { GRAPH_COMPLETE = 0, - GRAPH_LINEAR + GRAPH_LINEAR, } Graph_Type; typedef struct Run_Auto_Options { diff --git a/auto_tests/dht_getnodes_api_test.c b/auto_tests/dht_getnodes_api_test.c new file mode 100644 index 0000000000..ae519cf9b2 --- /dev/null +++ b/auto_tests/dht_getnodes_api_test.c @@ -0,0 +1,152 @@ +/** + * This autotest creates a small local DHT and makes sure that each peer can crawl + * the entire DHT using the DHT getnodes api functions. + */ + +#include +#include +#include +#include + +#include "../toxcore/tox.h" +#include "../toxcore/tox_private.h" +#include "auto_test_support.h" +#include "check_compat.h" + +#define NUM_TOXES 30 + +typedef struct Dht_Node { + uint8_t public_key[TOX_DHT_NODE_PUBLIC_KEY_SIZE]; + char ip[TOX_DHT_NODE_IP_STRING_SIZE]; + uint16_t port; +} Dht_Node; + +typedef struct State { + Dht_Node **nodes; + size_t num_nodes; + uint8_t **public_key_list; +} State; + +static void free_nodes(Dht_Node **nodes, size_t num_nodes) +{ + for (size_t i = 0; i < num_nodes; ++i) { + free(nodes[i]); + } + + free(nodes); +} + +static bool node_crawled(Dht_Node **nodes, size_t num_nodes, const uint8_t *public_key) +{ + for (size_t i = 0; i < num_nodes; ++i) { + if (memcmp(nodes[i]->public_key, public_key, TOX_DHT_NODE_PUBLIC_KEY_SIZE) == 0) { + return true; + } + } + + return false; +} + +static bool all_nodes_crawled(AutoTox *autotoxes, uint32_t num_toxes, uint8_t **public_key_list) +{ + for (uint32_t i = 0; i < num_toxes; ++i) { + const State *state = (const State *)autotoxes[i].state; + + // make sure each peer has crawled the correct number of nodes + if (state->num_nodes < num_toxes) { + return false; + } + } + + for (uint32_t i = 0; i < num_toxes; ++i) { + const State *state = (const State *)autotoxes[i].state; + + // make sure each peer has the full list of public keys + for (uint32_t j = 0; j < num_toxes; ++j) { + if (!node_crawled(state->nodes, state->num_nodes, public_key_list[j])) { + return false; + } + } + } + + return true; +} + +static void getnodes_response_cb(Tox *tox, const uint8_t *public_key, const char *ip, uint16_t port, void *user_data) +{ + ck_assert(user_data != nullptr); + + AutoTox *autotoxes = (AutoTox *)user_data; + State *state = (State *)autotoxes->state; + + if (node_crawled(state->nodes, state->num_nodes, public_key)) { + return; + } + + ck_assert(state->num_nodes < NUM_TOXES); + + Dht_Node *node = (Dht_Node *)calloc(1, sizeof(Dht_Node)); + ck_assert(node != nullptr); + + memcpy(node->public_key, public_key, TOX_DHT_NODE_PUBLIC_KEY_SIZE); + snprintf(node->ip, sizeof(node->ip), "%s", ip); + node->port = port; + + state->nodes[state->num_nodes] = node; + ++state->num_nodes; + + // ask new node to give us their close nodes to every public key + for (size_t i = 0; i < NUM_TOXES; ++i) { + tox_dht_get_nodes(tox, public_key, ip, port, state->public_key_list[i], nullptr); + } +} + +static void test_dht_getnodes(AutoTox *autotoxes) +{ + ck_assert(NUM_TOXES >= 2); + + uint8_t **public_key_list = (uint8_t **)calloc(NUM_TOXES, sizeof(uint8_t *)); + ck_assert(public_key_list != nullptr); + + for (size_t i = 0; i < NUM_TOXES; ++i) { + State *state = (State *)autotoxes[i].state; + + state->nodes = (Dht_Node **)calloc(NUM_TOXES, sizeof(Dht_Node *)); + ck_assert(state->nodes != nullptr); + + state->num_nodes = 0; + state->public_key_list = public_key_list; + + public_key_list[i] = (uint8_t *)malloc(sizeof(uint8_t) * TOX_PUBLIC_KEY_SIZE); + ck_assert(public_key_list[i] != nullptr); + + tox_self_get_dht_id(autotoxes[i].tox, public_key_list[i]); + tox_callback_dht_get_nodes_response(autotoxes[i].tox, getnodes_response_cb); + } + + while (!all_nodes_crawled(autotoxes, NUM_TOXES, public_key_list)) { + iterate_all_wait(autotoxes, NUM_TOXES, ITERATION_INTERVAL); + } + + for (size_t i = 0; i < NUM_TOXES; ++i) { + State *state = (State *)autotoxes[i].state; + free_nodes(state->nodes, state->num_nodes); + free(public_key_list[i]); + } + + free(public_key_list); +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options options = default_run_auto_options; + options.graph = GRAPH_LINEAR; + + run_auto_test(nullptr, NUM_TOXES, test_dht_getnodes, sizeof(State), &options); + + return 0; +} + +#undef NUM_TOXES diff --git a/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 b/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 index c852264b5a..a3a6110184 100644 --- a/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 +++ b/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 @@ -1 +1 @@ -dd7740d26e60b20495e1c9145ee820d32cbb4996f0a6104f2e3e4705b16244f7 /usr/local/bin/tox-bootstrapd +7d31dd00dd4d8fefd21d375f2c9b69499f356867b364a386c562c15d6ac58d93 /usr/local/bin/tox-bootstrapd diff --git a/toxcore/BUILD.bazel b/toxcore/BUILD.bazel index 34e016ad50..ecef1c65ad 100644 --- a/toxcore/BUILD.bazel +++ b/toxcore/BUILD.bazel @@ -452,9 +452,11 @@ cc_library( srcs = [ "tox.c", "tox_api.c", + ], + hdrs = [ + "tox.h", "tox_private.h", ], - hdrs = ["tox.h"], visibility = ["//c-toxcore:__subpackages__"], deps = [ ":Messenger", diff --git a/toxcore/DHT.h b/toxcore/DHT.h index 3bcea771dd..102441dcf6 100644 --- a/toxcore/DHT.h +++ b/toxcore/DHT.h @@ -256,6 +256,7 @@ typedef void dht_ip_cb(void *object, int32_t number, const IP_Port *ip_port); typedef void dht_get_nodes_response_cb(const DHT *dht, const Node_format *node, void *userdata); /** Sets the callback to be triggered on a getnodes response. */ +non_null(1) nullable(2) void dht_callback_get_nodes_response(DHT *dht, dht_get_nodes_response_cb *function); /** Add a new friend to the friends list. diff --git a/toxcore/tox.c b/toxcore/tox.c index db3b9fd9b1..961fe57b7d 100644 --- a/toxcore/tox.c +++ b/toxcore/tox.c @@ -320,6 +320,7 @@ static void tox_conference_peer_list_changed_handler(Messenger *m, uint32_t conf } } +non_null(1, 2) nullable(3) static void tox_dht_get_nodes_response_handler(const DHT *dht, const Node_format *node, void *user_data) { struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; @@ -2618,7 +2619,7 @@ bool tox_dht_get_nodes(const Tox *tox, const uint8_t *public_key, const char *ip return false; } - bool success = 0; + bool success = false; for (int32_t i = 0; i < count; ++i) { root[i].port = net_htons(port);