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 53static CURL *easy; 54 55static 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 69static void unit_stop(void) 70{ 71 curl_easy_cleanup(easy); 72 curl_global_cleanup(); 73} 74 75#ifdef DEBUGBUILD 76 77struct 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 95struct ai_family_stats { 96 const char *family; 97 int creations; 98 timediff_t first_created; 99 timediff_t last_created; 100}; 101 102struct 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 110static struct test_case *current_tc; 111static struct test_result *current_tr; 112 113struct 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 122static 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 135static 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 157static 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 175static 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 ENABLE_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 224out: 225 *pcf = (!result)? cf : NULL; 226 if(result) { 227 free(cf); 228 free(ctx); 229 } 230 return result; 231} 232 233static 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 296static 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 352static 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 ENABLE_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 386UNITTEST_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 399UNITTEST_STOP 400