]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
multi: add CURLMOPT_NETWORK_CHANGED to signal network changed
authorStefan Eissing <stefan@eissing.org>
Fri, 13 Jun 2025 10:38:44 +0000 (12:38 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 29 Jul 2025 09:18:26 +0000 (11:18 +0200)
New multi option CURLMOPT_NETWORK_CHANGED with a long bitmask value:

- CURLM_NWCOPT_CLEAR_CONNS: do not reuse existing connections, close all
  idle connections.

- CURLM_NWCOPT_CLEAR_DNS: clear the multi's DNS cache.

All other bits reserved for future extensions.

Fixes #17225
Reported-by: ウさん
Closes #17613

19 files changed:
docs/libcurl/curl_multi_setopt.md
docs/libcurl/opts/CURLMOPT_NETWORK_CHANGED.md [new file with mode: 0644]
docs/libcurl/opts/CURLOPT_FORBID_REUSE.md
docs/libcurl/opts/CURLOPT_FRESH_CONNECT.md
docs/libcurl/opts/Makefile.inc
docs/libcurl/symbols-in-versions
include/curl/multi.h
lib/conncache.c
lib/conncache.h
lib/easy.c
lib/hostip.c
lib/hostip.h
lib/multi.c
lib/url.c
lib/urldata.h
tests/data/Makefile.am
tests/data/test3033 [new file with mode: 0644]
tests/libtest/Makefile.inc
tests/libtest/lib3033.c [new file with mode: 0644]

index 55cc11beac2e6c090abbb9afa83bbcb6f31cb690..e646eced62c78cd62a7d321c4d6a4dee17146a15 100644 (file)
@@ -68,6 +68,10 @@ CURLMOPT_MAX_HOST_CONNECTIONS(3)
 
 Max simultaneously open connections. See CURLMOPT_MAX_TOTAL_CONNECTIONS(3)
 
+## CURLMOPT_NETWORK_CHANGED
+
+Signal that the network has changed. See CURLMOPT_NETWORK_CHANGED(3)
+
 ## CURLMOPT_PIPELINING
 
 Enable HTTP multiplexing. See CURLMOPT_PIPELINING(3)
diff --git a/docs/libcurl/opts/CURLMOPT_NETWORK_CHANGED.md b/docs/libcurl/opts/CURLMOPT_NETWORK_CHANGED.md
new file mode 100644 (file)
index 0000000..bcfd1cd
--- /dev/null
@@ -0,0 +1,80 @@
+---
+c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+SPDX-License-Identifier: curl
+Title: CURLMOPT_NETWORK_CHANGED
+Section: 3
+Source: libcurl
+See-also:
+  - CURLOPT_FRESH_CONNECT (3)
+  - CURLOPT_FORBID_REUSE (3)
+Protocol:
+  - All
+Added-in: 8.16.0
+---
+
+# NAME
+
+CURLMOPT_NETWORK_CHANGED - signal network changed
+
+# SYNOPSIS
+
+~~~c
+#include <curl/curl.h>
+
+CURLMcode curl_multi_setopt(CURLM *handle, CURLMOPT_NETWORK_CHANGED,
+                            long value);
+~~~
+
+# DESCRIPTION
+
+Pass a long with a bitmask to tell libcurl how the multi
+handle should react. The following values in the mask are
+defined. All bits not mentioned are reserved for future
+extensions.
+
+This option can be set at any time and repeatedly. Each call only
+affects the *currently* cached connections and DNS information.
+Any connection created or DNS information added afterwards is
+cached the usual way again. Phrasing it another way: the option is
+not persisted but setting it serves as a "trigger"
+to clear the caches.
+
+The call affects only the connection and DNS cache of the multi handle
+itself and not the ones owned by SHARE handles.
+
+## CURLM_NWCOPT_CLEAR_CONNS
+
+No longer reuse any existing connection in the multi handle's
+connection cache. This closes all connections that are not in use.
+Ongoing transfers continue on the connections they operate on.
+
+## CURLM_NWCOPT_CLEAR_DNS
+
+Clear the multi handle's DNS cache.
+
+# DEFAULT
+
+0, which has no effect.
+
+# %PROTOCOLS%
+
+# EXAMPLE
+
+~~~c
+int main(void)
+{
+  CURLM *m = curl_multi_init();
+  /* do transfers on the multi handle */
+  /* do not reuse existing connections */
+  curl_multi_setopt(m, CURLMOPT_NETWORK_CHANGED, CURLM_NWCOPT_CLEAR_CONNS);
+}
+~~~
+
+# %AVAILABILITY%
+
+# RETURN VALUE
+
+curl_multi_setopt(3) returns a CURLMcode indicating success or error.
+
+CURLM_OK (0) means everything was OK, non-zero means an error occurred, see
+libcurl-errors(3).
index 3fdcadc76b1d9020c5e54b5af26e667ea9f6a35f..729dcdc70268863bbe91c38aff6ec57aadc5ebac 100644 (file)
@@ -8,6 +8,7 @@ See-also:
   - CURLOPT_FRESH_CONNECT (3)
   - CURLOPT_MAXCONNECTS (3)
   - CURLOPT_MAXLIFETIME_CONN (3)
+  - CURLMOPT_NETWORK_CHANGED (3)
 Protocol:
   - All
 Added-in: 7.7
index a466b6938de4695d4dbdbb6abf489f83ff3b0286..0198a344f4bd523c065b83b1f154248e06c5bdcc 100644 (file)
@@ -10,6 +10,7 @@ See-also:
   - CURLOPT_FORBID_REUSE (3)
   - CURLOPT_MAXAGE_CONN (3)
   - CURLOPT_MAXLIFETIME_CONN (3)
+  - CURLMOPT_NETWORK_CHANGED (3)
 Added-in: 7.7
 ---
 
index d62d369a6692dbe38b814c6cfe5879fe911bb2c1..1cf960e5ee930e75c3327975aaf93bbf8bb5d5c7 100644 (file)
@@ -108,6 +108,7 @@ man_MANS =                                      \
   CURLMOPT_MAX_PIPELINE_LENGTH.3                \
   CURLMOPT_MAX_TOTAL_CONNECTIONS.3              \
   CURLMOPT_MAXCONNECTS.3                        \
+  CURLMOPT_NETWORK_CHANGED.3                    \
   CURLMOPT_PIPELINING.3                         \
   CURLMOPT_PIPELINING_SERVER_BL.3               \
   CURLMOPT_PIPELINING_SITE_BL.3                 \
index 4603dd322cd98a5f3147e5828d8050a9736d9a86..6387ea3014ac9171f4e60fca837c88e304d29750 100644 (file)
@@ -545,6 +545,8 @@ CURLM_BAD_SOCKET                7.15.4
 CURLM_CALL_MULTI_PERFORM        7.9.6
 CURLM_CALL_MULTI_SOCKET         7.15.5
 CURLM_INTERNAL_ERROR            7.9.6
+CURLM_NWCOPT_CLEAR_CONNS        8.16.0
+CURLM_NWCOPT_CLEAR_DNS          8.16.0
 CURLM_OK                        7.9.6
 CURLM_OUT_OF_MEMORY             7.9.6
 CURLM_RECURSIVE_API_CALL        7.59.0
@@ -559,6 +561,7 @@ CURLMOPT_MAX_HOST_CONNECTIONS   7.30.0
 CURLMOPT_MAX_PIPELINE_LENGTH    7.30.0
 CURLMOPT_MAX_TOTAL_CONNECTIONS  7.30.0
 CURLMOPT_MAXCONNECTS            7.16.3
+CURLMOPT_NETWORK_CHANGED        8.16.0
 CURLMOPT_PIPELINING             7.16.0
 CURLMOPT_PIPELINING_SERVER_BL   7.30.0
 CURLMOPT_PIPELINING_SITE_BL     7.30.0
index aa1291001ffc778e7e5919aa156c65277dc686d1..0fbea88707d717da1430abb9cdea2b0d3a8e8b25 100644 (file)
@@ -395,9 +395,23 @@ typedef enum {
   /* maximum number of concurrent streams to support on a connection */
   CURLOPT(CURLMOPT_MAX_CONCURRENT_STREAMS, CURLOPTTYPE_LONG, 16),
 
+  /* network has changed, adjust caches/connection reuse */
+  CURLOPT(CURLMOPT_NETWORK_CHANGED, CURLOPTTYPE_LONG, 17),
+
   CURLMOPT_LASTENTRY /* the last unused */
 } CURLMoption;
 
+/* Definition of bits for the CURLMOPT_NETWORK_CHANGED argument: */
+
+/* - CURLM_NWCOPT_CLEAR_CONNS tells libcurl to prevent further reuse
+     of existing connections. Connections that are idle will be closed.
+     Ongoing transfers will continue with the connection they have. */
+#define CURLM_NWCOPT_CLEAR_CONNS (1L<<0)
+
+/* - CURLM_NWCOPT_CLEAR_DNS tells libcurl to prevent further reuse
+     of existing connections. Connections that are idle will be closed.
+     Ongoing transfers will continue with the connection they have. */
+#define CURLM_NWCOPT_CLEAR_DNS (1L<<0)
 
 /*
  * Name:    curl_multi_setopt()
index f77a7468740eebbeacdfb52f00a11aa32d004a65..1393bb565be002185435006b70c02e8557cf0541 100644 (file)
@@ -709,7 +709,8 @@ static int cpool_reap_dead_cb(struct Curl_easy *data,
                               struct connectdata *conn, void *param)
 {
   struct cpool_reaper_ctx *rctx = param;
-  if(Curl_conn_seems_dead(conn, data, &rctx->now)) {
+  if((!CONN_INUSE(conn) && conn->bits.no_reuse) ||
+     Curl_conn_seems_dead(conn, data, &rctx->now)) {
     /* stop the iteration here, pass back the connection that was pruned */
     Curl_conn_terminate(data, conn, FALSE);
     return 1;
@@ -849,6 +850,40 @@ void Curl_cpool_do_locked(struct Curl_easy *data,
     cb(conn, data, cbdata);
 }
 
+static int cpool_mark_stale(struct Curl_easy *data,
+                            struct connectdata *conn, void *param)
+{
+  (void)data;
+  (void)param;
+  conn->bits.no_reuse = TRUE;
+  return 0;
+}
+
+static int cpool_reap_no_reuse(struct Curl_easy *data,
+                               struct connectdata *conn, void *param)
+{
+  (void)data;
+  (void)param;
+  if(!CONN_INUSE(conn) && conn->bits.no_reuse) {
+    Curl_conn_terminate(data, conn, FALSE);
+    return 1;
+  }
+  return 0; /* continue iteration */
+}
+
+void Curl_cpool_nw_changed(struct Curl_easy *data)
+{
+  struct cpool *cpool = cpool_get_instance(data);
+
+  if(cpool) {
+    CPOOL_LOCK(cpool, data);
+    cpool_foreach(data, cpool, NULL, cpool_mark_stale);
+    while(cpool_foreach(data, cpool, NULL, cpool_reap_no_reuse))
+      ;
+    CPOOL_UNLOCK(cpool, data);
+  }
+}
+
 #if 0
 /* Useful for debugging the connection pool */
 void Curl_cpool_print(struct cpool *cpool)
index ac07111a7d77ea8b182a1b755f2379d7e3e0ee76..a5f133344fd4d1f4997c50caf71d36d7c5b74365 100644 (file)
@@ -163,4 +163,8 @@ void Curl_cpool_do_locked(struct Curl_easy *data,
                           struct connectdata *conn,
                           Curl_cpool_conn_do_cb *cb, void *cbdata);
 
+/* Close all unused connections, prevent reuse of existing ones. */
+void Curl_cpool_nw_changed(struct Curl_easy *data);
+
+
 #endif /* HEADER_CURL_CONNCACHE_H */
index e79171a3aeb1d8dc4f228fa88f9dce90a787e2a7..b526e6cff0a5b0bd2af59f49e5ff30577aa46447 100644 (file)
@@ -1102,6 +1102,7 @@ void curl_easy_reset(CURL *d)
   data->progress.hide = TRUE;
   data->state.current_speed = -1; /* init to negative == impossible */
   data->state.retrycount = 0;     /* reset the retry counter */
+  data->state.recent_conn_id = -1; /* clear remembered connection id */
 
   /* zero out authentication data: */
   memset(&data->state.authhost, 0, sizeof(struct auth));
index c1bc3d46333addee9888aa2c76c81e55420c12c0..95c2c6224259e31be67e0228f9198e72ebb1d3bb 100644 (file)
@@ -290,6 +290,16 @@ void Curl_dnscache_prune(struct Curl_easy *data)
   dnscache_unlock(data, dnscache);
 }
 
+void Curl_dnscache_clear(struct Curl_easy *data)
+{
+  struct Curl_dnscache *dnscache = dnscache_get(data);
+  if(dnscache) {
+    dnscache_lock(data, dnscache);
+    Curl_hash_clean(&dnscache->entries);
+    dnscache_unlock(data, dnscache);
+  }
+}
+
 #ifdef USE_ALARM_TIMEOUT
 /* Beware this is a global and unique instance. This is used to store the
    return address that we can jump back to from inside a signal handler. This
index cd3d957e1e7a5be121df01e4b8e97657f3f039fe..56c5e9cad126153f1353dd97f8db8dd0ea95b024 100644 (file)
@@ -130,6 +130,9 @@ void Curl_dnscache_destroy(struct Curl_dnscache *dns);
 /* prune old entries from the DNS cache */
 void Curl_dnscache_prune(struct Curl_easy *data);
 
+/* clear the DNS cache */
+void Curl_dnscache_clear(struct Curl_easy *data);
+
 /* IPv4 threadsafe resolve function used for synch and asynch builds */
 struct Curl_addrinfo *Curl_ipv4_resolve_r(const char *hostname, int port);
 
index 79d44796ec871d6782b942230030ef617357d4b5..194cf75d400e2e57e9cacdba310161e13b16e556 100644 (file)
@@ -3235,6 +3235,16 @@ CURLMcode curl_multi_setopt(CURLM *m,
       multi->max_concurrent_streams = (unsigned int)streams;
     }
     break;
+  case CURLMOPT_NETWORK_CHANGED: {
+      long val = va_arg(param, long);
+      if(val & CURLM_NWCOPT_CLEAR_DNS) {
+        Curl_dnscache_clear(multi->admin);
+      }
+      if(val & CURLM_NWCOPT_CLEAR_CONNS) {
+        Curl_cpool_nw_changed(multi->admin);
+      }
+    break;
+  }
   default:
     res = CURLM_UNKNOWN_OPTION;
     break;
index 8a808ef5fd364025bb8773718e8c97b8b4f73dae..3a57be4464a68dba30270e8433b2973de10a5277 100644 (file)
--- a/lib/url.c
+++ b/lib/url.c
@@ -843,7 +843,7 @@ static bool url_match_connect_config(struct connectdata *conn,
                                      struct url_conn_match *m)
 {
   /* connect-only or to-be-closed connections will not be reused */
-  if(conn->connect_only || conn->bits.close)
+  if(conn->connect_only || conn->bits.close || conn->bits.no_reuse)
     return FALSE;
 
   /* ip_version must match */
index cae24966eb24fdcb2c9cd5c0b1e158eb1af4df50..8c3a85cdabe34133402887e596a721d3ab1af05b 100644 (file)
@@ -423,6 +423,7 @@ struct ConnectBits {
   BIT(parallel_connect); /* set TRUE when a parallel connect attempt has
                             started (happy eyeballs) */
   BIT(aborted); /* connection was aborted, e.g. in unclean state */
+  BIT(no_reuse); /* connection should not be reused */
   BIT(shutdown_handler); /* connection shutdown: handler shut down */
   BIT(shutdown_filters); /* connection shutdown: filters shut down */
   BIT(in_cpool);     /* connection is kept in a connection pool */
index cc3ea533d50e50db3d9296971c7f7b1c5e00e122..31f61216e6b26d61b5b56b61613d1e58a0abfa2c 100644 (file)
@@ -273,7 +273,7 @@ test3000 test3001 test3002 test3003 test3004 test3005 test3006 test3007 \
 test3008 test3009 test3010 test3011 test3012 test3013 test3014 test3015 \
 test3016 test3017 test3018 test3019 test3020 test3021 test3022 test3023 \
 test3024 test3025 test3026 test3027 test3028 test3029 test3030 test3031 \
-test3032 \
+test3032 test3033 \
 \
 test3100 test3101 test3102 test3103 test3104 test3105 \
 \
diff --git a/tests/data/test3033 b/tests/data/test3033
new file mode 100644 (file)
index 0000000..8ba008b
--- /dev/null
@@ -0,0 +1,53 @@
+<testcase>
+<info>
+<keywords>
+curl_easy_setopt
+connection reuse
+libtest
+</keywords>
+</info>
+
+#
+# Server-side
+<reply>
+<data>
+HTTP/1.1 200 OK
+Content-Length: 6
+
+-foo-
+</data>
+<datacheck>
+[0] no network change
+-foo-
+[1] signal network change
+-foo-
+[2] no network change
+-foo-
+</datacheck>
+</reply>
+
+#
+# Client-side
+<client>
+<server>
+http
+</server>
+<name>
+CURLOPT_FRESH_CONNECT=2
+</name>
+<tool>
+lib%TESTNUMBER
+</tool>
+<command>
+http://%HOSTIP:%HTTPPORT/%TESTNUMBER
+</command>
+</client>
+
+#
+# Verify data after the test has been "shot"
+<verify>
+<errorcode>
+0
+</errorcode>
+</verify>
+</testcase>
index 8878ae85fa6db26cdd2dda5cf6cef76cb6a3e842..ba402d903bd8968cf91f63365e9469fab1c175a2 100644 (file)
@@ -93,6 +93,6 @@ TESTS_C = \
   lib2402.c           lib2404.c lib2405.c \
   lib2502.c \
   lib2700.c \
-  lib3010.c lib3025.c lib3026.c lib3027.c \
+  lib3010.c lib3025.c lib3026.c lib3027.c lib3033.c \
   lib3100.c lib3101.c lib3102.c lib3103.c lib3104.c lib3105.c \
   lib3207.c lib3208.c
diff --git a/tests/libtest/lib3033.c b/tests/libtest/lib3033.c
new file mode 100644 (file)
index 0000000..f0a6419
--- /dev/null
@@ -0,0 +1,129 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "testtrace.h"
+#include "testutil.h"
+#include "memdebug.h"
+
+
+static CURLcode t3033_req_test(CURLM *multi, CURL *easy,
+                               char *url_3033, int index)
+{
+  CURLMsg *msg = NULL;
+  CURLcode res = CURLE_OK;
+  int still_running = 0;
+
+  if(index == 1) {
+    curl_multi_setopt(multi, CURLMOPT_NETWORK_CHANGED,
+                      CURLM_NWCOPT_CLEAR_CONNS);
+    curl_mprintf("[1] signal network change\n");
+  }
+  else {
+    curl_mprintf("[%d] no network change\n", index);
+  }
+
+  curl_easy_reset(easy);
+  curl_easy_setopt(easy, CURLOPT_URL, url_3033);
+  easy_setopt(easy, CURLOPT_DEBUGDATA, &libtest_debug_config);
+  easy_setopt(easy, CURLOPT_DEBUGFUNCTION, libtest_debug_cb);
+  easy_setopt(easy, CURLOPT_VERBOSE, 1L);
+
+  curl_multi_add_handle(multi, easy);
+
+  do {
+    CURLMcode mres;
+    int num;
+    curl_multi_perform(multi, &still_running);
+    mres = curl_multi_wait(multi, NULL, 0, TEST_HANG_TIMEOUT, &num);
+    if(mres != CURLM_OK) {
+      curl_mfprintf(stderr, "curl_multi_wait() returned %d\n", mres);
+      res = TEST_ERR_MAJOR_BAD;
+      goto test_cleanup;
+    }
+  } while(still_running);
+
+  do {
+    long num_connects = 0L;
+    msg = curl_multi_info_read(multi, &still_running);
+    if(msg) {
+      if(msg->msg != CURLMSG_DONE)
+        continue;
+
+      res = msg->data.result;
+      if(res != CURLE_OK) {
+        curl_mfprintf(stderr, "curl_multi_info_read() returned %d\n", res);
+        goto test_cleanup;
+      }
+
+      curl_easy_getinfo(easy, CURLINFO_NUM_CONNECTS, &num_connects);
+      if(index == 1 && num_connects == 0) {
+        curl_mprintf("[1] should not reuse connection in pool\n");
+        res = TEST_ERR_MAJOR_BAD;
+        goto test_cleanup;
+      }
+      else if(index == 2 && num_connects) {
+        curl_mprintf("[2] should have reused connection from [1]\n");
+        res = TEST_ERR_MAJOR_BAD;
+        goto test_cleanup;
+      }
+    }
+  } while(msg);
+
+test_cleanup:
+
+  curl_multi_remove_handle(multi, easy);
+
+  return res;
+}
+
+static CURLcode test_lib3033(char *URL)
+{
+  CURL *curl = NULL;
+  CURLM *multi = NULL;
+  CURLcode res = CURLE_OK;
+
+  global_init(CURL_GLOBAL_ALL);
+  multi_init(multi);
+  easy_init(curl);
+
+  libtest_debug_config.nohex = 1;
+  libtest_debug_config.tracetime = 1;
+
+  res = t3033_req_test(multi, curl, URL, 0);
+  if(res != CURLE_OK)
+    goto test_cleanup;
+  res = t3033_req_test(multi, curl, URL, 1);
+  if(res != CURLE_OK)
+    goto test_cleanup;
+  res = t3033_req_test(multi, curl, URL, 2);
+  if(res != CURLE_OK)
+    goto test_cleanup;
+
+test_cleanup:
+
+  curl_easy_cleanup(curl);
+  curl_multi_cleanup(multi);
+  curl_global_cleanup();
+
+  return res; /* return the final return code */
+}