/work/auto_tests/group_announce_test.c
Line | Count | Source |
1 | | /* SPDX-License-Identifier: GPL-3.0-or-later |
2 | | * Copyright © 2026 The TokTok team. |
3 | | */ |
4 | | |
5 | | /** |
6 | | * Tests GCA (group chat announce) response size bounds. |
7 | | * |
8 | | * Sends a GCA announce request to a server that has a full DHT node list and |
9 | | * a full GCA announce list, then verifies that the response fits within |
10 | | * GCA_ANNOUNCE_RESPONSE_MAX_SIZE. |
11 | | */ |
12 | | |
13 | | #include <stdbool.h> |
14 | | #include <stdint.h> |
15 | | #include <stdlib.h> |
16 | | #include <string.h> |
17 | | |
18 | | #include "../toxcore/DHT.h" |
19 | | #include "../toxcore/crypto_core.h" |
20 | | #include "../toxcore/group_announce.h" |
21 | | #include "../toxcore/group_onion_announce.h" |
22 | | #include "../toxcore/logger.h" |
23 | | #include "../toxcore/mono_time.h" |
24 | | #include "../toxcore/net.h" |
25 | | #include "../toxcore/network.h" |
26 | | #include "../toxcore/onion.h" |
27 | | #include "../toxcore/onion_announce.h" |
28 | | #include "../toxcore/os_memory.h" |
29 | | #include "../toxcore/os_random.h" |
30 | | #include "auto_test_support.h" |
31 | | #include "check_compat.h" |
32 | | |
33 | | #ifndef USE_IPV6 |
34 | | #define USE_IPV6 1 |
35 | | #endif |
36 | | |
37 | | static inline IP get_loopback(void) |
38 | 136 | { |
39 | 136 | IP ip; |
40 | | #if USE_IPV6 |
41 | | ip.family = net_family_ipv6(); |
42 | | ip.ip.v6 = get_ip6_loopback(); |
43 | | #else |
44 | 136 | ip.family = net_family_ipv4(); |
45 | 136 | ip.ip.v4 = get_ip4_loopback(); |
46 | 136 | #endif |
47 | 136 | return ip; |
48 | 136 | } |
49 | | |
50 | | typedef struct { |
51 | | bool received; |
52 | | uint16_t length; |
53 | | } Recv3_State; |
54 | | |
55 | | static int handle_recv_3(void *object, const IP_Port *source, const uint8_t *packet, |
56 | | uint16_t length, void *userdata) |
57 | 3 | { |
58 | 3 | Recv3_State *state = (Recv3_State *)object; |
59 | 3 | state->received = true; |
60 | 3 | state->length = length; |
61 | 3 | return 0; |
62 | 3 | } |
63 | | |
64 | | /** Fill @p ann with a maximum-size IPv6 announce (IPv6 ip_port + IPv6 TCP relay). */ |
65 | | static void fill_max_public_announce(const Random *rng, GC_Public_Announce *ann, |
66 | | const uint8_t *chat_pk) |
67 | 73 | { |
68 | 73 | memcpy(ann->chat_public_key, chat_pk, ENC_PUBLIC_KEY_SIZE); |
69 | 73 | random_bytes(rng, ann->base_announce.peer_public_key, ENC_PUBLIC_KEY_SIZE); |
70 | | |
71 | 73 | ann->base_announce.ip_port.ip.family = net_family_ipv6(); |
72 | 73 | random_bytes(rng, ann->base_announce.ip_port.ip.ip.v6.uint8, |
73 | 73 | sizeof(ann->base_announce.ip_port.ip.ip.v6.uint8)); |
74 | 73 | ann->base_announce.ip_port.port = net_htons(33445); |
75 | 73 | ann->base_announce.ip_port_is_set = true; |
76 | | |
77 | 73 | ann->base_announce.tcp_relays_count = GCA_MAX_ANNOUNCED_TCP_RELAYS; |
78 | 73 | random_bytes(rng, ann->base_announce.tcp_relays[0].public_key, |
79 | 73 | CRYPTO_PUBLIC_KEY_SIZE); |
80 | 73 | ann->base_announce.tcp_relays[0].ip_port.ip.family = net_family_tcp_ipv6(); |
81 | 73 | random_bytes(rng, ann->base_announce.tcp_relays[0].ip_port.ip.ip.v6.uint8, |
82 | 73 | sizeof(ann->base_announce.tcp_relays[0].ip_port.ip.ip.v6.uint8)); |
83 | 73 | ann->base_announce.tcp_relays[0].ip_port.port = net_htons(33445); |
84 | 73 | } |
85 | | |
86 | | /** Fill @p ann with a small IPv4-only announce (fits within GCA_ANNOUNCE_MAX_SIZE). */ |
87 | | static void fill_small_public_announce(const Random *rng, GC_Public_Announce *ann, |
88 | | const uint8_t *chat_pk) |
89 | 12 | { |
90 | 12 | memcpy(ann->chat_public_key, chat_pk, ENC_PUBLIC_KEY_SIZE); |
91 | 12 | random_bytes(rng, ann->base_announce.peer_public_key, ENC_PUBLIC_KEY_SIZE); |
92 | | |
93 | 12 | ann->base_announce.ip_port.ip.family = net_family_ipv4(); |
94 | 12 | ann->base_announce.ip_port.ip.ip.v4 = get_ip4_loopback(); |
95 | 12 | ann->base_announce.ip_port.port = net_htons(33445); |
96 | 12 | ann->base_announce.ip_port_is_set = true; |
97 | 12 | ann->base_announce.tcp_relays_count = 0; |
98 | 12 | } |
99 | | |
100 | | static void test_gca_announce_response_size(void) |
101 | 40 | { |
102 | 40 | const Memory *mem = os_memory(); |
103 | 40 | ck_assert(mem != nullptr); |
104 | 40 | const Random *rng = os_random(); |
105 | 40 | ck_assert(rng != nullptr); |
106 | 40 | const Network *ns = os_network(); |
107 | 40 | ck_assert(ns != nullptr); |
108 | | |
109 | | /* ------------------------------------------------------------------ */ |
110 | | /* Server node: Onion_Announce with GCA callback registered. */ |
111 | | /* ------------------------------------------------------------------ */ |
112 | 40 | uint32_t srv_idx = 1; |
113 | 40 | uint32_t cli_idx = 2; |
114 | | |
115 | 40 | Logger *srv_log = logger_new(mem); |
116 | 40 | ck_assert(srv_log != nullptr); |
117 | 40 | logger_callback_log(srv_log, print_debug_logger, nullptr, &srv_idx); |
118 | | |
119 | 40 | Mono_Time *srv_mono = mono_time_new(mem, nullptr, nullptr); |
120 | 40 | ck_assert(srv_mono != nullptr); |
121 | 40 | mono_time_update(srv_mono); |
122 | | |
123 | 40 | IP ip = get_loopback(); |
124 | | |
125 | 40 | Networking_Core *srv_net = new_networking_ex( |
126 | 40 | srv_log, mem, ns, &ip, 36700, |
127 | 40 | 36700 + (TOX_PORTRANGE_TO - TOX_PORTRANGE_FROM), nullptr); |
128 | 40 | ck_assert(srv_net != nullptr); |
129 | | |
130 | 38 | DHT *srv_dht = new_dht(srv_log, mem, rng, ns, srv_mono, srv_net, true, false); |
131 | 38 | ck_assert(srv_dht != nullptr); |
132 | | |
133 | 26 | Onion *srv_onion = new_onion(srv_log, mem, srv_mono, rng, srv_dht, srv_net); |
134 | 26 | ck_assert(srv_onion != nullptr); |
135 | | |
136 | 23 | Onion_Announce *srv_onion_a = new_onion_announce( |
137 | 23 | srv_log, mem, rng, srv_mono, srv_dht, srv_net); |
138 | 23 | ck_assert(srv_onion_a != nullptr); |
139 | | |
140 | 20 | GC_Announces_List *gca_list = new_gca_list(mem); |
141 | 20 | ck_assert(gca_list != nullptr); |
142 | | |
143 | 19 | gca_onion_init(gca_list, srv_onion_a); |
144 | | |
145 | | /* Pre-fill the GCA list with GCA_MAX_SENT_ANNOUNCES full-size IPv6 |
146 | | * announces so the server returns a maximally-populated response. */ |
147 | 19 | uint8_t chat_pk[ENC_PUBLIC_KEY_SIZE]; |
148 | 19 | random_bytes(rng, chat_pk, sizeof(chat_pk)); |
149 | | |
150 | 91 | for (uint32_t i = 0; i < GCA_MAX_SENT_ANNOUNCES; ++i) { |
151 | 73 | GC_Public_Announce ann; |
152 | 73 | fill_max_public_announce(rng, &ann, chat_pk); |
153 | 73 | mono_time_update(srv_mono); |
154 | 73 | GC_Peer_Announce *peer_ann = gca_add_announce(mem, srv_mono, gca_list, &ann); |
155 | 73 | ck_assert_msg(peer_ann != nullptr, "Failed to add pre-filled GCA announce %u", i); |
156 | 73 | } |
157 | | |
158 | | /* Inject MAX_SENT_NODES fake LAN entries so get_close_nodes() returns |
159 | | * a full node list alongside the GCA announces. */ |
160 | 90 | for (uint32_t i = 0; i < MAX_SENT_NODES; ++i) { |
161 | 72 | uint8_t fake_pk[CRYPTO_PUBLIC_KEY_SIZE]; |
162 | 72 | random_bytes(rng, fake_pk, sizeof(fake_pk)); |
163 | 72 | IP_Port fake_ipp; |
164 | 72 | fake_ipp.ip.family = net_family_ipv4(); |
165 | 72 | fake_ipp.ip.ip.v4.uint8[0] = 10; |
166 | 72 | fake_ipp.ip.ip.v4.uint8[1] = 0; |
167 | 72 | fake_ipp.ip.ip.v4.uint8[2] = 0; |
168 | 72 | fake_ipp.ip.ip.v4.uint8[3] = (uint8_t)(i + 1); |
169 | 72 | fake_ipp.port = net_htons(33445 + i); |
170 | 72 | addto_lists(srv_dht, &fake_ipp, fake_pk); |
171 | 72 | } |
172 | | |
173 | | /* ------------------------------------------------------------------ */ |
174 | | /* Client node: raw Networking_Core with a RECV_3 capture handler. */ |
175 | | /* ------------------------------------------------------------------ */ |
176 | 18 | Logger *cli_log = logger_new(mem); |
177 | 18 | ck_assert(cli_log != nullptr); |
178 | 17 | logger_callback_log(cli_log, print_debug_logger, nullptr, &cli_idx); |
179 | | |
180 | 17 | Mono_Time *cli_mono = mono_time_new(mem, nullptr, nullptr); |
181 | 17 | ck_assert(cli_mono != nullptr); |
182 | | |
183 | 15 | Networking_Core *cli_net = new_networking_ex( |
184 | 15 | cli_log, mem, ns, &ip, 36701, |
185 | 15 | 36701 + (TOX_PORTRANGE_TO - TOX_PORTRANGE_FROM), nullptr); |
186 | 15 | ck_assert(cli_net != nullptr); |
187 | | |
188 | 12 | Recv3_State state = {false, 0}; |
189 | 12 | networking_registerhandler(cli_net, NET_PACKET_ONION_RECV_3, |
190 | 12 | handle_recv_3, &state); |
191 | | |
192 | | /* ------------------------------------------------------------------ */ |
193 | | /* Build a GCA announce request and send it directly to the server. */ |
194 | | /* ------------------------------------------------------------------ */ |
195 | | |
196 | | /* Use a small IPv4-only public announce as the gc_data payload so that |
197 | | * gc_data_length fits within the GCA_ANNOUNCE_MAX_SIZE limit. */ |
198 | 12 | GC_Public_Announce my_ann; |
199 | 12 | fill_small_public_announce(rng, &my_ann, chat_pk); |
200 | | |
201 | 12 | uint8_t gc_data[GCA_PUBLIC_ANNOUNCE_MAX_SIZE]; |
202 | 12 | const int gc_data_len = gca_pack_public_announce( |
203 | 12 | srv_log, gc_data, sizeof(gc_data), &my_ann); |
204 | 12 | ck_assert_msg(gc_data_len > 0, "Failed to pack public announce"); |
205 | 12 | ck_assert_msg((uint16_t)gc_data_len <= GCA_ANNOUNCE_MAX_SIZE, |
206 | 12 | "gc_data_len %d exceeds GCA_ANNOUNCE_MAX_SIZE", gc_data_len); |
207 | | |
208 | 12 | uint8_t cli_pk[CRYPTO_PUBLIC_KEY_SIZE]; |
209 | 12 | uint8_t cli_sk[CRYPTO_SECRET_KEY_SIZE]; |
210 | 12 | crypto_new_keypair(rng, cli_pk, cli_sk); |
211 | | |
212 | 12 | uint8_t data_pk[CRYPTO_PUBLIC_KEY_SIZE]; |
213 | 12 | uint8_t data_sk[CRYPTO_SECRET_KEY_SIZE]; |
214 | 12 | crypto_new_keypair(rng, data_pk, data_sk); |
215 | | |
216 | 12 | const uint8_t zeroes[ONION_PING_ID_SIZE] = {0}; |
217 | 12 | const uint64_t sendback = 0x0102030405060708ULL; |
218 | | |
219 | | /* The request buffer must also hold ONION_RETURN_3 return-path bytes |
220 | | * at the tail, as required by the announce request handler. */ |
221 | 12 | const uint16_t req_cap = ONION_ANNOUNCE_REQUEST_MAX_SIZE + ONION_RETURN_3; |
222 | 12 | uint8_t *req_buf = (uint8_t *)malloc(req_cap); |
223 | 12 | ck_assert(req_buf != nullptr); |
224 | | |
225 | 11 | const int req_len = create_gca_announce_request( |
226 | 11 | mem, rng, req_buf, ONION_ANNOUNCE_REQUEST_MAX_SIZE, |
227 | 11 | dht_get_self_public_key(srv_dht), |
228 | 11 | cli_pk, cli_sk, |
229 | 11 | zeroes, cli_pk, data_pk, sendback, |
230 | 11 | gc_data, (uint16_t)gc_data_len); |
231 | 11 | ck_assert_msg(req_len > 0, "Failed to create GCA announce request"); |
232 | | |
233 | 10 | memset(req_buf + req_len, 0, ONION_RETURN_3); |
234 | 10 | const uint16_t total_len = (uint16_t)(req_len + ONION_RETURN_3); |
235 | | |
236 | 10 | const IP_Port srv_ipp = {ip, net_port(srv_net)}; |
237 | 10 | ck_assert_msg(sendpacket(cli_net, &srv_ipp, req_buf, total_len) == total_len, |
238 | 10 | "Failed to send GCA announce request"); |
239 | 9 | free(req_buf); |
240 | | |
241 | | /* ------------------------------------------------------------------ */ |
242 | | /* Poll until response received or 1-second timeout (200 × 5 ms). */ |
243 | | /* ------------------------------------------------------------------ */ |
244 | 1.21k | for (int i = 0; i < 200 && !state.received; ++i) { |
245 | 1.20k | mono_time_update(srv_mono); |
246 | 1.20k | mono_time_update(cli_mono); |
247 | 1.20k | networking_poll(srv_net, nullptr); |
248 | 1.20k | networking_poll(cli_net, nullptr); |
249 | 1.20k | c_sleep(5); |
250 | 1.20k | } |
251 | | |
252 | 9 | ck_assert_msg(state.received, "No GCA announce response received within timeout"); |
253 | | |
254 | | /* The ONION_RECV_3 packet layout: |
255 | | * [1 byte type] [ONION_RETURN_3 bytes return path] [payload bytes] */ |
256 | 3 | ck_assert_msg(state.length > (uint16_t)(1 + ONION_RETURN_3), |
257 | 3 | "RECV_3 packet too short to contain any payload: %u", state.length); |
258 | | |
259 | 3 | const uint16_t resp_len = state.length - (uint16_t)(1 + ONION_RETURN_3); |
260 | | |
261 | 3 | ck_assert_msg(resp_len >= ONION_ANNOUNCE_RESPONSE_MIN_SIZE, |
262 | 3 | "announce response too small: %u (min %u)", |
263 | 3 | resp_len, (unsigned int)ONION_ANNOUNCE_RESPONSE_MIN_SIZE); |
264 | | |
265 | 3 | ck_assert_msg(resp_len <= GCA_ANNOUNCE_RESPONSE_MAX_SIZE, |
266 | 3 | "announce response too large: %u (max %u)", |
267 | 3 | resp_len, (unsigned int)GCA_ANNOUNCE_RESPONSE_MAX_SIZE); |
268 | | |
269 | 3 | printf("GCA announce response payload: %u bytes " |
270 | 3 | "(min: %u, max: %u)\n", |
271 | 3 | resp_len, |
272 | 3 | (unsigned int)ONION_ANNOUNCE_RESPONSE_MIN_SIZE, |
273 | 3 | (unsigned int)GCA_ANNOUNCE_RESPONSE_MAX_SIZE); |
274 | | |
275 | | /* ------------------------------------------------------------------ */ |
276 | | /* Cleanup (reverse order of creation). */ |
277 | | /* ------------------------------------------------------------------ */ |
278 | 3 | networking_registerhandler(cli_net, NET_PACKET_ONION_RECV_3, nullptr, nullptr); |
279 | 3 | kill_networking(cli_net); |
280 | 3 | mono_time_free(mem, cli_mono); |
281 | 3 | logger_kill(cli_log); |
282 | | |
283 | 3 | kill_gca(gca_list); |
284 | 3 | kill_onion_announce(srv_onion_a); |
285 | 3 | kill_onion(srv_onion); |
286 | 3 | kill_dht(srv_dht); |
287 | 3 | kill_networking(srv_net); |
288 | 3 | mono_time_free(mem, srv_mono); |
289 | 3 | logger_kill(srv_log); |
290 | 3 | } |
291 | | |
292 | | static void basic_gca_announce_tests(void) |
293 | 40 | { |
294 | 40 | test_gca_announce_response_size(); |
295 | 40 | } |
296 | | |
297 | | int main(void) |
298 | 84 | { |
299 | 84 | setvbuf(stdout, nullptr, _IONBF, 0); |
300 | | |
301 | 84 | basic_gca_announce_tests(); |
302 | 84 | return 0; |
303 | 84 | } |