1 /***************************************************************************
2 * _ _ ____ _
3 * Project ___| | | | _ \| |
4 * / __| | | | |_) | |
5 * | (__| |_| | _ <| |___
6 * \___|\___/|_| \_\_____|
7 *
8 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
9 *
10 * This software is licensed as described in the file COPYING, which
11 * you should have received as part of this distribution. The terms
12 * are also available at https://curl.se/docs/copyright.html.
13 *
14 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
15 * copies of the Software, and permit persons to whom the Software is
16 * furnished to do so, under the terms of the COPYING file.
17 *
18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19 * KIND, either express or implied.
20 *
21 * SPDX-License-Identifier: curl
22 *
23 ***************************************************************************/
24 #include "curlcheck.h"
25
26 #ifdef HAVE_NETINET_IN_H
27 #include <netinet/in.h>
28 #endif
29 #ifdef HAVE_NETINET_IN6_H
30 #include <netinet/in6.h>
31 #endif
32 #ifdef HAVE_NETDB_H
33 #include <netdb.h>
34 #endif
35 #ifdef HAVE_ARPA_INET_H
36 #include <arpa/inet.h>
37 #endif
38 #ifdef __VMS
39 #include <in.h>
40 #include <inet.h>
41 #endif
42
43 #include <setjmp.h>
44 #include <signal.h>
45
46 #include "urldata.h"
47 #include "connect.h"
48 #include "cfilters.h"
49 #include "multiif.h"
50 #include "curl_trc.h"
51
52
53 static CURL *easy;
54
unit_setup(void)55 static CURLcode unit_setup(void)
56 {
57 CURLcode res = CURLE_OK;
58
59 global_init(CURL_GLOBAL_ALL);
60 easy = curl_easy_init();
61 if(!easy) {
62 curl_global_cleanup();
63 return CURLE_OUT_OF_MEMORY;
64 }
65 curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
66 return res;
67 }
68
unit_stop(void)69 static void unit_stop(void)
70 {
71 curl_easy_cleanup(easy);
72 curl_global_cleanup();
73 }
74
75 #ifdef DEBUGBUILD
76
77 struct test_case {
78 int id;
79 const char *url;
80 const char *resolve_info;
81 unsigned char ip_version;
82 timediff_t connect_timeout_ms;
83 timediff_t he_timeout_ms;
84 timediff_t cf4_fail_delay_ms;
85 timediff_t cf6_fail_delay_ms;
86
87 int exp_cf4_creations;
88 int exp_cf6_creations;
89 timediff_t min_duration_ms;
90 timediff_t max_duration_ms;
91 CURLcode exp_result;
92 const char *pref_family;
93 };
94
95 struct ai_family_stats {
96 const char *family;
97 int creations;
98 timediff_t first_created;
99 timediff_t last_created;
100 };
101
102 struct test_result {
103 CURLcode result;
104 struct curltime started;
105 struct curltime ended;
106 struct ai_family_stats cf4;
107 struct ai_family_stats cf6;
108 };
109
110 static struct test_case *current_tc;
111 static struct test_result *current_tr;
112
113 struct cf_test_ctx {
114 int ai_family;
115 int transport;
116 char id[16];
117 struct curltime started;
118 timediff_t fail_delay_ms;
119 struct ai_family_stats *stats;
120 };
121
cf_test_destroy(struct Curl_cfilter * cf,struct Curl_easy * data)122 static void cf_test_destroy(struct Curl_cfilter *cf, struct Curl_easy *data)
123 {
124 struct cf_test_ctx *ctx = cf->ctx;
125 #ifndef CURL_DISABLE_VERBOSE_STRINGS
126 infof(data, "%04dms: cf[%s] destroyed",
127 (int)Curl_timediff(Curl_now(), current_tr->started), ctx->id);
128 #else
129 (void)data;
130 #endif
131 free(ctx);
132 cf->ctx = NULL;
133 }
134
cf_test_connect(struct Curl_cfilter * cf,struct Curl_easy * data,bool blocking,bool * done)135 static CURLcode cf_test_connect(struct Curl_cfilter *cf,
136 struct Curl_easy *data,
137 bool blocking, bool *done)
138 {
139 struct cf_test_ctx *ctx = cf->ctx;
140 timediff_t duration_ms;
141
142 (void)data;
143 (void)blocking;
144 *done = FALSE;
145 duration_ms = Curl_timediff(Curl_now(), ctx->started);
146 if(duration_ms >= ctx->fail_delay_ms) {
147 infof(data, "%04dms: cf[%s] fail delay reached",
148 (int)duration_ms, ctx->id);
149 return CURLE_COULDNT_CONNECT;
150 }
151 if(duration_ms)
152 infof(data, "%04dms: cf[%s] continuing", (int)duration_ms, ctx->id);
153 Curl_expire(data, ctx->fail_delay_ms - duration_ms, EXPIRE_RUN_NOW);
154 return CURLE_OK;
155 }
156
157 static struct Curl_cftype cft_test = {
158 "TEST",
159 CF_TYPE_IP_CONNECT,
160 CURL_LOG_LVL_NONE,
161 cf_test_destroy,
162 cf_test_connect,
163 Curl_cf_def_close,
164 Curl_cf_def_get_host,
165 Curl_cf_def_adjust_pollset,
166 Curl_cf_def_data_pending,
167 Curl_cf_def_send,
168 Curl_cf_def_recv,
169 Curl_cf_def_cntrl,
170 Curl_cf_def_conn_is_alive,
171 Curl_cf_def_conn_keep_alive,
172 Curl_cf_def_query,
173 };
174
cf_test_create(struct Curl_cfilter ** pcf,struct Curl_easy * data,struct connectdata * conn,const struct Curl_addrinfo * ai,int transport)175 static CURLcode cf_test_create(struct Curl_cfilter **pcf,
176 struct Curl_easy *data,
177 struct connectdata *conn,
178 const struct Curl_addrinfo *ai,
179 int transport)
180 {
181 struct cf_test_ctx *ctx = NULL;
182 struct Curl_cfilter *cf = NULL;
183 timediff_t created_at;
184 CURLcode result;
185
186 (void)data;
187 (void)conn;
188 ctx = calloc(1, sizeof(*ctx));
189 if(!ctx) {
190 result = CURLE_OUT_OF_MEMORY;
191 goto out;
192 }
193 ctx->ai_family = ai->ai_family;
194 ctx->transport = transport;
195 ctx->started = Curl_now();
196 #ifdef USE_IPV6
197 if(ctx->ai_family == AF_INET6) {
198 ctx->stats = ¤t_tr->cf6;
199 ctx->fail_delay_ms = current_tc->cf6_fail_delay_ms;
200 curl_msprintf(ctx->id, "v6-%d", ctx->stats->creations);
201 ctx->stats->creations++;
202 }
203 else
204 #endif
205 {
206 ctx->stats = ¤t_tr->cf4;
207 ctx->fail_delay_ms = current_tc->cf4_fail_delay_ms;
208 curl_msprintf(ctx->id, "v4-%d", ctx->stats->creations);
209 ctx->stats->creations++;
210 }
211
212 created_at = Curl_timediff(ctx->started, current_tr->started);
213 if(ctx->stats->creations == 1)
214 ctx->stats->first_created = created_at;
215 ctx->stats->last_created = created_at;
216 infof(data, "%04dms: cf[%s] created", (int)created_at, ctx->id);
217
218 result = Curl_cf_create(&cf, &cft_test, ctx);
219 if(result)
220 goto out;
221
222 Curl_expire(data, ctx->fail_delay_ms, EXPIRE_RUN_NOW);
223
224 out:
225 *pcf = (!result)? cf : NULL;
226 if(result) {
227 free(cf);
228 free(ctx);
229 }
230 return result;
231 }
232
check_result(struct test_case * tc,struct test_result * tr)233 static void check_result(struct test_case *tc,
234 struct test_result *tr)
235 {
236 char msg[256];
237 timediff_t duration_ms;
238
239 duration_ms = Curl_timediff(tr->ended, tr->started);
240 fprintf(stderr, "%d: test case took %dms\n", tc->id, (int)duration_ms);
241
242 if(tr->result != tc->exp_result
243 && CURLE_OPERATION_TIMEDOUT != tr->result) {
244 /* on CI we encounter the TIMEOUT result, since images get less CPU
245 * and events are not as sharply timed. */
246 curl_msprintf(msg, "%d: expected result %d but got %d",
247 tc->id, tc->exp_result, tr->result);
248 fail(msg);
249 }
250 if(tr->cf4.creations != tc->exp_cf4_creations) {
251 curl_msprintf(msg, "%d: expected %d ipv4 creations, but got %d",
252 tc->id, tc->exp_cf4_creations, tr->cf4.creations);
253 fail(msg);
254 }
255 if(tr->cf6.creations != tc->exp_cf6_creations) {
256 curl_msprintf(msg, "%d: expected %d ipv6 creations, but got %d",
257 tc->id, tc->exp_cf6_creations, tr->cf6.creations);
258 fail(msg);
259 }
260
261 duration_ms = Curl_timediff(tr->ended, tr->started);
262 if(duration_ms < tc->min_duration_ms) {
263 curl_msprintf(msg, "%d: expected min duration of %dms, but took %dms",
264 tc->id, (int)tc->min_duration_ms, (int)duration_ms);
265 fail(msg);
266 }
267 if(duration_ms > tc->max_duration_ms) {
268 curl_msprintf(msg, "%d: expected max duration of %dms, but took %dms",
269 tc->id, (int)tc->max_duration_ms, (int)duration_ms);
270 fail(msg);
271 }
272 if(tr->cf6.creations && tr->cf4.creations && tc->pref_family) {
273 /* did ipv4 and ipv6 both, expect the preferred family to start right arway
274 * with the other being delayed by the happy_eyeball_timeout */
275 struct ai_family_stats *stats1 = !strcmp(tc->pref_family, "v6")?
276 &tr->cf6 : &tr->cf4;
277 struct ai_family_stats *stats2 = !strcmp(tc->pref_family, "v6")?
278 &tr->cf4 : &tr->cf6;
279
280 if(stats1->first_created > 100) {
281 curl_msprintf(msg, "%d: expected ip%s to start right away, instead "
282 "first attempt made after %dms",
283 tc->id, stats1->family, (int)stats1->first_created);
284 fail(msg);
285 }
286 if(stats2->first_created < tc->he_timeout_ms) {
287 curl_msprintf(msg, "%d: expected ip%s to start delayed after %dms, "
288 "instead first attempt made after %dms",
289 tc->id, stats2->family, (int)tc->he_timeout_ms,
290 (int)stats2->first_created);
291 fail(msg);
292 }
293 }
294 }
295
test_connect(struct test_case * tc)296 static void test_connect(struct test_case *tc)
297 {
298 struct test_result tr;
299 struct curl_slist *list = NULL;
300
301 Curl_debug_set_transport_provider(TRNSPRT_TCP, cf_test_create);
302 current_tc = tc;
303 current_tr = &tr;
304
305 list = curl_slist_append(NULL, tc->resolve_info);
306 fail_unless(list, "error allocating resolve list entry");
307 curl_easy_setopt(easy, CURLOPT_RESOLVE, list);
308 curl_easy_setopt(easy, CURLOPT_IPRESOLVE, (long)tc->ip_version);
309 curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS,
310 (long)tc->connect_timeout_ms);
311 curl_easy_setopt(easy, CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS,
312 (long)tc->he_timeout_ms);
313
314 curl_easy_setopt(easy, CURLOPT_URL, tc->url);
315 memset(&tr, 0, sizeof(tr));
316 tr.cf6.family = "v6";
317 tr.cf4.family = "v4";
318
319 tr.started = Curl_now();
320 tr.result = curl_easy_perform(easy);
321 tr.ended = Curl_now();
322
323 curl_easy_setopt(easy, CURLOPT_RESOLVE, NULL);
324 curl_slist_free_all(list);
325 list = NULL;
326 current_tc = NULL;
327 current_tr = NULL;
328
329 check_result(tc, &tr);
330 }
331
332 #endif /* DEBUGBUILD */
333
334 /*
335 * How these test cases work:
336 * - replace the creation of the TCP socket filter with our test filter
337 * - test filter does nothing and reports failure after configured delay
338 * - we feed addresses into the resolve cache to simulate different cases
339 * - we monitor how many instances of ipv4/v6 attempts are made and when
340 * - for mixed families, we expect HAPPY_EYEBALLS_TIMEOUT to trigger
341 *
342 * Max Duration checks needs to be conservative since CI jobs are not
343 * as sharp.
344 */
345 #define TURL "http://test.com:123"
346
347 #define R_FAIL CURLE_COULDNT_CONNECT
348 /* timeout values accounting for low cpu resources in CI */
349 #define TC_TMOT 90000 /* 90 sec max test duration */
350 #define CNCT_TMOT 60000 /* 60sec connect timeout */
351
352 static struct test_case TEST_CASES[] = {
353 /* TIMEOUT_MS, FAIL_MS CREATED DURATION Result, HE_PREF */
354 /* CNCT HE v4 v6 v4 v6 MIN MAX */
355 { 1, TURL, "test.com:123:192.0.2.1", CURL_IPRESOLVE_WHATEVER,
356 CNCT_TMOT, 150, 200, 200, 1, 0, 200, TC_TMOT, R_FAIL, NULL },
357 /* 1 ipv4, fails after ~200ms, reports COULDNT_CONNECT */
358 { 2, TURL, "test.com:123:192.0.2.1,192.0.2.2", CURL_IPRESOLVE_WHATEVER,
359 CNCT_TMOT, 150, 200, 200, 2, 0, 400, TC_TMOT, R_FAIL, NULL },
360 /* 2 ipv4, fails after ~400ms, reports COULDNT_CONNECT */
361 #ifdef USE_IPV6
362 { 3, TURL, "test.com:123:::1", CURL_IPRESOLVE_WHATEVER,
363 CNCT_TMOT, 150, 200, 200, 0, 1, 200, TC_TMOT, R_FAIL, NULL },
364 /* 1 ipv6, fails after ~200ms, reports COULDNT_CONNECT */
365 { 4, TURL, "test.com:123:::1,::2", CURL_IPRESOLVE_WHATEVER,
366 CNCT_TMOT, 150, 200, 200, 0, 2, 400, TC_TMOT, R_FAIL, NULL },
367 /* 2 ipv6, fails after ~400ms, reports COULDNT_CONNECT */
368
369 { 5, TURL, "test.com:123:192.0.2.1,::1", CURL_IPRESOLVE_WHATEVER,
370 CNCT_TMOT, 150, 200, 200, 1, 1, 350, TC_TMOT, R_FAIL, "v4" },
371 /* mixed ip4+6, v4 starts, v6 kicks in on HE, fails after ~350ms */
372 { 6, TURL, "test.com:123:::1,192.0.2.1", CURL_IPRESOLVE_WHATEVER,
373 CNCT_TMOT, 150, 200, 200, 1, 1, 350, TC_TMOT, R_FAIL, "v6" },
374 /* mixed ip6+4, v6 starts, v4 never starts due to high HE, TIMEOUT */
375 { 7, TURL, "test.com:123:192.0.2.1,::1", CURL_IPRESOLVE_V4,
376 CNCT_TMOT, 150, 500, 500, 1, 0, 400, TC_TMOT, R_FAIL, NULL },
377 /* mixed ip4+6, but only use v4, check it uses full connect timeout,
378 although another address of the 'wrong' family is available */
379 { 8, TURL, "test.com:123:::1,192.0.2.1", CURL_IPRESOLVE_V6,
380 CNCT_TMOT, 150, 500, 500, 0, 1, 400, TC_TMOT, R_FAIL, NULL },
381 /* mixed ip4+6, but only use v6, check it uses full connect timeout,
382 although another address of the 'wrong' family is available */
383 #endif
384 };
385
386 UNITTEST_START
387
388 #if defined(DEBUGBUILD)
389 size_t i;
390
391 for(i = 0; i < sizeof(TEST_CASES)/sizeof(TEST_CASES[0]); ++i) {
392 test_connect(&TEST_CASES[i]);
393 }
394 #else
395 (void)TEST_CASES;
396 (void)test_connect;
397 #endif
398
399 UNITTEST_STOP
400