xref: /curl/tests/unit/unit2600.c (revision 44fc2687)
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 "select.h"
51 #include "curl_trc.h"
52 #include "memdebug.h"
53 
54 static CURL *easy;
55 
unit_setup(void)56 static CURLcode unit_setup(void)
57 {
58   CURLcode res = CURLE_OK;
59 
60   global_init(CURL_GLOBAL_ALL);
61   easy = curl_easy_init();
62   if(!easy) {
63     curl_global_cleanup();
64     return CURLE_OUT_OF_MEMORY;
65   }
66   curl_global_trace("all");
67   curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
68   return res;
69 }
70 
unit_stop(void)71 static void unit_stop(void)
72 {
73   curl_easy_cleanup(easy);
74   curl_global_cleanup();
75 }
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_wait_ms(10);
154   }
155   Curl_expire(data, ctx->fail_delay_ms - duration_ms, EXPIRE_RUN_NOW);
156   return CURLE_OK;
157 }
158 
cf_test_adjust_pollset(struct Curl_cfilter * cf,struct Curl_easy * data,struct easy_pollset * ps)159 static void cf_test_adjust_pollset(struct Curl_cfilter *cf,
160                                    struct Curl_easy *data,
161                                    struct easy_pollset *ps)
162 {
163   /* just for testing, give one socket with events back */
164   (void)cf;
165   Curl_pollset_set(data, ps, 1, TRUE, TRUE);
166 }
167 
168 static struct Curl_cftype cft_test = {
169   "TEST",
170   CF_TYPE_IP_CONNECT,
171   CURL_LOG_LVL_NONE,
172   cf_test_destroy,
173   cf_test_connect,
174   Curl_cf_def_close,
175   Curl_cf_def_shutdown,
176   Curl_cf_def_get_host,
177   cf_test_adjust_pollset,
178   Curl_cf_def_data_pending,
179   Curl_cf_def_send,
180   Curl_cf_def_recv,
181   Curl_cf_def_cntrl,
182   Curl_cf_def_conn_is_alive,
183   Curl_cf_def_conn_keep_alive,
184   Curl_cf_def_query,
185 };
186 
cf_test_create(struct Curl_cfilter ** pcf,struct Curl_easy * data,struct connectdata * conn,const struct Curl_addrinfo * ai,int transport)187 static CURLcode cf_test_create(struct Curl_cfilter **pcf,
188                                struct Curl_easy *data,
189                                struct connectdata *conn,
190                                const struct Curl_addrinfo *ai,
191                                int transport)
192 {
193   struct cf_test_ctx *ctx = NULL;
194   struct Curl_cfilter *cf = NULL;
195   timediff_t created_at;
196   CURLcode result;
197 
198   (void)data;
199   (void)conn;
200   ctx = calloc(1, sizeof(*ctx));
201   if(!ctx) {
202     result = CURLE_OUT_OF_MEMORY;
203     goto out;
204   }
205   ctx->ai_family = ai->ai_family;
206   ctx->transport = transport;
207   ctx->started = Curl_now();
208 #ifdef USE_IPV6
209   if(ctx->ai_family == AF_INET6) {
210     ctx->stats = &current_tr->cf6;
211     ctx->fail_delay_ms = current_tc->cf6_fail_delay_ms;
212     curl_msprintf(ctx->id, "v6-%d", ctx->stats->creations);
213     ctx->stats->creations++;
214   }
215   else
216 #endif
217   {
218     ctx->stats = &current_tr->cf4;
219     ctx->fail_delay_ms = current_tc->cf4_fail_delay_ms;
220     curl_msprintf(ctx->id, "v4-%d", ctx->stats->creations);
221     ctx->stats->creations++;
222   }
223 
224   created_at = Curl_timediff(ctx->started, current_tr->started);
225   if(ctx->stats->creations == 1)
226     ctx->stats->first_created = created_at;
227   ctx->stats->last_created = created_at;
228   infof(data, "%04dms: cf[%s] created", (int)created_at, ctx->id);
229 
230   result = Curl_cf_create(&cf, &cft_test, ctx);
231   if(result)
232     goto out;
233 
234   Curl_expire(data, ctx->fail_delay_ms, EXPIRE_RUN_NOW);
235 
236 out:
237   *pcf = (!result) ? cf : NULL;
238   if(result) {
239     free(cf);
240     free(ctx);
241   }
242   return result;
243 }
244 
check_result(struct test_case * tc,struct test_result * tr)245 static void check_result(struct test_case *tc,
246                          struct test_result *tr)
247 {
248   char msg[256];
249   timediff_t duration_ms;
250 
251   duration_ms = Curl_timediff(tr->ended, tr->started);
252   fprintf(stderr, "%d: test case took %dms\n", tc->id, (int)duration_ms);
253 
254   if(tr->result != tc->exp_result
255     && CURLE_OPERATION_TIMEDOUT != tr->result) {
256     /* on CI we encounter the TIMEOUT result, since images get less CPU
257      * and events are not as sharply timed. */
258     curl_msprintf(msg, "%d: expected result %d but got %d",
259                   tc->id, tc->exp_result, tr->result);
260     fail(msg);
261   }
262   if(tr->cf4.creations != tc->exp_cf4_creations) {
263     curl_msprintf(msg, "%d: expected %d ipv4 creations, but got %d",
264                   tc->id, tc->exp_cf4_creations, tr->cf4.creations);
265     fail(msg);
266   }
267   if(tr->cf6.creations != tc->exp_cf6_creations) {
268     curl_msprintf(msg, "%d: expected %d ipv6 creations, but got %d",
269                   tc->id, tc->exp_cf6_creations, tr->cf6.creations);
270     fail(msg);
271   }
272 
273   duration_ms = Curl_timediff(tr->ended, tr->started);
274   if(duration_ms < tc->min_duration_ms) {
275     curl_msprintf(msg, "%d: expected min duration of %dms, but took %dms",
276                   tc->id, (int)tc->min_duration_ms, (int)duration_ms);
277     fail(msg);
278   }
279   if(duration_ms > tc->max_duration_ms) {
280     curl_msprintf(msg, "%d: expected max duration of %dms, but took %dms",
281                   tc->id, (int)tc->max_duration_ms, (int)duration_ms);
282     fail(msg);
283   }
284   if(tr->cf6.creations && tr->cf4.creations && tc->pref_family) {
285     /* did ipv4 and ipv6 both, expect the preferred family to start right arway
286      * with the other being delayed by the happy_eyeball_timeout */
287     struct ai_family_stats *stats1 = !strcmp(tc->pref_family, "v6") ?
288                                      &tr->cf6 : &tr->cf4;
289     struct ai_family_stats *stats2 = !strcmp(tc->pref_family, "v6") ?
290                                      &tr->cf4 : &tr->cf6;
291 
292     if(stats1->first_created > 100) {
293       curl_msprintf(msg, "%d: expected ip%s to start right away, instead "
294                     "first attempt made after %dms",
295                     tc->id, stats1->family, (int)stats1->first_created);
296       fail(msg);
297     }
298     if(stats2->first_created < tc->he_timeout_ms) {
299       curl_msprintf(msg, "%d: expected ip%s to start delayed after %dms, "
300                     "instead first attempt made after %dms",
301                     tc->id, stats2->family, (int)tc->he_timeout_ms,
302                     (int)stats2->first_created);
303       fail(msg);
304     }
305   }
306 }
307 
test_connect(struct test_case * tc)308 static void test_connect(struct test_case *tc)
309 {
310   struct test_result tr;
311   struct curl_slist *list = NULL;
312 
313   Curl_debug_set_transport_provider(TRNSPRT_TCP, cf_test_create);
314   current_tc = tc;
315   current_tr = &tr;
316 
317   list = curl_slist_append(NULL, tc->resolve_info);
318   fail_unless(list, "error allocating resolve list entry");
319   curl_easy_setopt(easy, CURLOPT_RESOLVE, list);
320   curl_easy_setopt(easy, CURLOPT_IPRESOLVE, (long)tc->ip_version);
321   curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS,
322                    (long)tc->connect_timeout_ms);
323   curl_easy_setopt(easy, CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS,
324                    (long)tc->he_timeout_ms);
325 
326   curl_easy_setopt(easy, CURLOPT_URL, tc->url);
327   memset(&tr, 0, sizeof(tr));
328   tr.cf6.family = "v6";
329   tr.cf4.family = "v4";
330 
331   tr.started = Curl_now();
332   tr.result = curl_easy_perform(easy);
333   tr.ended = Curl_now();
334 
335   curl_easy_setopt(easy, CURLOPT_RESOLVE, NULL);
336   curl_slist_free_all(list);
337   list = NULL;
338   current_tc = NULL;
339   current_tr = NULL;
340 
341   check_result(tc, &tr);
342 }
343 
344 /*
345  * How these test cases work:
346  * - replace the creation of the TCP socket filter with our test filter
347  * - test filter does nothing and reports failure after configured delay
348  * - we feed addresses into the resolve cache to simulate different cases
349  * - we monitor how many instances of ipv4/v6 attempts are made and when
350  * - for mixed families, we expect HAPPY_EYEBALLS_TIMEOUT to trigger
351  *
352  * Max Duration checks needs to be conservative since CI jobs are not
353  * as sharp.
354  */
355 #define TURL "http://test.com:123"
356 
357 #define R_FAIL      CURLE_COULDNT_CONNECT
358 /* timeout values accounting for low cpu resources in CI */
359 #define TC_TMOT     90000  /* 90 sec max test duration */
360 #define CNCT_TMOT   60000  /* 60sec connect timeout */
361 
362 static struct test_case TEST_CASES[] = {
363   /* TIMEOUT_MS,    FAIL_MS      CREATED    DURATION     Result, HE_PREF */
364   /* CNCT   HE      v4    v6     v4 v6      MIN   MAX */
365   { 1, TURL, "test.com:123:192.0.2.1", CURL_IPRESOLVE_WHATEVER,
366     CNCT_TMOT, 150, 200,  200,    1,  0,      200,  TC_TMOT,  R_FAIL, NULL },
367   /* 1 ipv4, fails after ~200ms, reports COULDNT_CONNECT   */
368   { 2, TURL, "test.com:123:192.0.2.1,192.0.2.2", CURL_IPRESOLVE_WHATEVER,
369     CNCT_TMOT, 150, 200,  200,    2,  0,      400,  TC_TMOT,  R_FAIL, NULL },
370   /* 2 ipv4, fails after ~400ms, reports COULDNT_CONNECT   */
371 #ifdef USE_IPV6
372   { 3, TURL, "test.com:123:::1", CURL_IPRESOLVE_WHATEVER,
373     CNCT_TMOT, 150, 200,  200,    0,  1,      200,  TC_TMOT,  R_FAIL, NULL },
374   /* 1 ipv6, fails after ~200ms, reports COULDNT_CONNECT   */
375   { 4, TURL, "test.com:123:::1,::2", CURL_IPRESOLVE_WHATEVER,
376     CNCT_TMOT, 150, 200,  200,    0,  2,      400,  TC_TMOT,  R_FAIL, NULL },
377   /* 2 ipv6, fails after ~400ms, reports COULDNT_CONNECT   */
378 
379   { 5, TURL, "test.com:123:192.0.2.1,::1", CURL_IPRESOLVE_WHATEVER,
380     CNCT_TMOT, 150, 200, 200,     1,  1,      350,  TC_TMOT,  R_FAIL, "v6" },
381   /* mixed ip4+6, v6 always first, v4 kicks in on HE, fails after ~350ms */
382   { 6, TURL, "test.com:123:::1,192.0.2.1", CURL_IPRESOLVE_WHATEVER,
383     CNCT_TMOT, 150, 200, 200,     1,  1,      350,  TC_TMOT,  R_FAIL, "v6" },
384   /* mixed ip6+4, v6 starts, v4 never starts due to high HE, TIMEOUT */
385   { 7, TURL, "test.com:123:192.0.2.1,::1", CURL_IPRESOLVE_V4,
386     CNCT_TMOT, 150, 500, 500,     1,  0,      400,  TC_TMOT,  R_FAIL, NULL },
387   /* mixed ip4+6, but only use v4, check it uses full connect timeout,
388      although another address of the 'wrong' family is available */
389   { 8, TURL, "test.com:123:::1,192.0.2.1", CURL_IPRESOLVE_V6,
390     CNCT_TMOT, 150, 500, 500,     0,  1,      400,  TC_TMOT,  R_FAIL, NULL },
391   /* mixed ip4+6, but only use v6, check it uses full connect timeout,
392      although another address of the 'wrong' family is available */
393 #endif
394 };
395 
396 UNITTEST_START
397 
398   size_t i;
399 
400   for(i = 0; i < sizeof(TEST_CASES)/sizeof(TEST_CASES[0]); ++i) {
401     test_connect(&TEST_CASES[i]);
402   }
403 
404 UNITTEST_STOP
405