commit 20f568902c43f0bdb1d676b01154fa2d5daffa2d
parent ebe53cf61e21b00a987f42a3dd7ca23d0dbc84e8
Author: Christian Grothoff <christian@grothoff.org>
Date: Fri, 7 Aug 2026 00:06:08 +0200
implement connection limit options and handle OOM for buffer allocation
Diffstat:
5 files changed, 169 insertions(+), 10 deletions(-)
diff --git a/README b/README
@@ -114,6 +114,20 @@ Paivana reads an INI-style `.conf` file. The only section used is
consulted with `-f`. See "Trusted proxies" below.
TRUSTED_PROXIES6
IPv6 counterpart of TRUSTED_PROXIES.
+ CONNECTION_LIMIT
+ Total number of concurrent client connections to accept,
+ default 512. Divided evenly over the listen sockets that
+ come up, so the process-wide total is what you set --
+ with no BIND_TO there are two (IPv4 and IPv6). Paivana
+ also spends file descriptors on outbound requests from
+ the same table, so leave headroom below `ulimit -n`.
+ PER_IP_CONNECTION_LIMIT
+ Concurrent connections accepted from any one client
+ address, default 32; 0 disables the check. Set it to 0
+ wherever the peer address is not the client's -- under
+ SERVE = unix or systemd every client shares one peer
+ address, and behind a reverse proxy or a NAT many clients
+ do, so a limit there throttles everyone at once.
SERVE `tcp` (default) or `unix` (Unix-domain socket) or `systemd`
(systemd socket activation).
PORT TCP port, used when SERVE = tcp.
diff --git a/src/backend/paivana-httpd.c b/src/backend/paivana-httpd.c
@@ -67,6 +67,10 @@ bool PH_have_trusted_proxies;
unsigned long long PH_request_buffer_max = 1024 * 1024;
+unsigned int PH_connection_limit = 512;
+
+unsigned int PH_per_ip_connection_limit = 32;
+
int PH_global_ret;
int PH_global_cookie;
@@ -363,6 +367,47 @@ run (void *cls,
return;
}
{
+ unsigned long long v;
+
+ if (GNUNET_OK ==
+ GNUNET_CONFIGURATION_get_value_number (c,
+ "paivana",
+ "CONNECTION_LIMIT",
+ &v))
+ {
+ if ( (0 == v) ||
+ (v > UINT_MAX) )
+ {
+ GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
+ "paivana",
+ "CONNECTION_LIMIT",
+ "must be between 1 and UINT_MAX");
+ PH_global_ret = EXIT_NOTCONFIGURED;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ PH_connection_limit = (unsigned int) v;
+ }
+ if (GNUNET_OK ==
+ GNUNET_CONFIGURATION_get_value_number (c,
+ "paivana",
+ "PER_IP_CONNECTION_LIMIT",
+ &v))
+ {
+ if (v > UINT_MAX)
+ {
+ GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
+ "paivana",
+ "PER_IP_CONNECTION_LIMIT",
+ "must not exceed UINT_MAX");
+ PH_global_ret = EXIT_NOTCONFIGURED;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ PH_per_ip_connection_limit = (unsigned int) v;
+ }
+ }
+ {
unsigned int n4;
unsigned int n6;
diff --git a/src/backend/paivana-httpd.h b/src/backend/paivana-httpd.h
@@ -181,6 +181,30 @@ extern int PH_global_ret;
extern const struct GNUNET_CONFIGURATION_Handle *PH_cfg;
/**
+ * Total number of concurrent client connections we accept, from the
+ * `CONNECTION_LIMIT` configuration option. Divided evenly over the
+ * listen sockets that come up, because MHD's own limit is per daemon
+ * and TALER_MHD_listen_bind() starts one daemon per address.
+ *
+ * MHD's default (about 1018 per daemon) is wrong here in both
+ * directions: it is not process-wide, and it does not know that we
+ * spend file descriptors on outbound libcurl handles from the same
+ * table -- exhausting them makes *paying* clients' requests fail.
+ */
+extern unsigned int PH_connection_limit;
+
+/**
+ * Number of concurrent connections we accept from any single client
+ * address, from `PER_IP_CONNECTION_LIMIT`; 0 disables the check, which
+ * is MHD's default.
+ *
+ * Must be 0 wherever the peer address is not the client's: under
+ * `SERVE = unix` or `systemd` every client shares one peer, and behind
+ * a reverse proxy or a NAT many clients do. See the README.
+ */
+extern unsigned int PH_per_ip_connection_limit;
+
+/**
* Maximum size (in bytes) of a request body that we will buffer
* before forwarding it upstream. Requests exceeding this are
* rejected with HTTP 413. Settable via the `-u` / `--max-upload`
diff --git a/src/backend/paivana-httpd_daemon.c b/src/backend/paivana-httpd_daemon.c
@@ -409,20 +409,56 @@ mhd_log_callback (void *cls,
/**
- * Callback invoked on every listen socket to start the
- * respective MHD HTTP daemon.
+ * Listen sockets handed to us by TALER_MHD_listen_bind(), collected
+ * before any daemon is started; see #collect_socket().
+ */
+static int *lsocks;
+
+/**
+ * Length of #lsocks.
+ */
+static unsigned int lsocks_length;
+
+
+/**
+ * Callback invoked on every listen socket: remember it.
+ *
+ * The daemons are not started here because
+ * MHD_OPTION_CONNECTION_LIMIT is per daemon and cannot be changed
+ * afterwards, while the budget it has to divide is process-wide --
+ * TALER_MHD_listen_bind() starts one daemon per getaddrinfo() result,
+ * so an empty BIND_TO yields two, and giving each the whole budget
+ * would double it. We cannot know the divisor until the last socket
+ * has been handed over.
*
* @param cls unused
* @param lsock the listen socket
*/
static void
-start_daemon (void *cls,
- int lsock)
+collect_socket (void *cls,
+ int lsock)
{
- struct MHD_Daemon *mhd;
-
(void) cls;
GNUNET_assert (-1 != lsock);
+ GNUNET_array_append (lsocks,
+ lsocks_length,
+ lsock);
+}
+
+
+/**
+ * Start one MHD daemon on @a lsock, entitled to @a climit concurrent
+ * connections.
+ *
+ * @param lsock the listen socket, ownership passes to MHD on success
+ * @param climit connection limit for this daemon
+ */
+static void
+start_daemon (int lsock,
+ unsigned int climit)
+{
+ struct MHD_Daemon *mhd;
+
mhd = MHD_start_daemon (
MHD_USE_DEBUG
| MHD_ALLOW_SUSPEND_RESUME,
@@ -432,6 +468,8 @@ start_daemon (void *cls,
MHD_OPTION_LISTEN_SOCKET,
lsock,
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 16,
+ MHD_OPTION_CONNECTION_LIMIT, climit,
+ MHD_OPTION_PER_IP_CONNECTION_LIMIT, PH_per_ip_connection_limit,
MHD_OPTION_NOTIFY_COMPLETED, &mhd_completed_cb, NULL,
MHD_OPTION_URI_LOG_CALLBACK, &mhd_log_callback, NULL,
MHD_OPTION_END);
@@ -457,8 +495,27 @@ PAIVANA_HTTPD_serve_requests ()
ret = TALER_MHD_listen_bind (PH_cfg,
"paivana",
- &start_daemon,
+ &collect_socket,
NULL);
+ if (0 != lsocks_length)
+ {
+ unsigned int climit
+ = GNUNET_MAX (1,
+ PH_connection_limit / lsocks_length);
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Serving on %u listen socket(s), %u connection(s) each"
+ " (%u per client address, 0 = unlimited)\n",
+ lsocks_length,
+ climit,
+ PH_per_ip_connection_limit);
+ for (unsigned int i = 0; i < lsocks_length; i++)
+ start_daemon (lsocks[i],
+ climit);
+ GNUNET_array_grow (lsocks,
+ lsocks_length,
+ 0);
+ }
switch (ret)
{
case GNUNET_SYSERR:
diff --git a/src/backend/paivana-httpd_reverse.c b/src/backend/paivana-httpd_reverse.c
@@ -1471,6 +1471,7 @@ buffer_upload_chunk (struct HttpRequest *hr,
if (hr->io_size - hr->io_len < upload_data_size)
{
unsigned int new_size;
+ char *nbuf;
new_size = GNUNET_MIN (
GNUNET_MAX_MALLOC_CHECKED,
@@ -1479,9 +1480,27 @@ buffer_upload_chunk (struct HttpRequest *hr,
UINT_MAX),
min_size));
GNUNET_assert (new_size > hr->io_size);
- GNUNET_array_grow (hr->io_buf,
- hr->io_size,
- new_size);
+ /* Deliberately not GNUNET_array_grow(): that allocates through
+ GNUNET_xmalloc_(), which calls GNUNET_assert(0) when malloc
+ returns NULL, i.e. aborts the whole daemon -- dropping every
+ other client, including the paying ones -- because one request
+ could not be buffered. Degrade to 413 for this request instead
+ and keep serving. */
+ nbuf = GNUNET_malloc_large (new_size);
+ if (NULL == nbuf)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Could not allocate %u bytes to buffer an upload;"
+ " rejecting this request rather than terminating\n",
+ new_size);
+ return false;
+ }
+ GNUNET_memcpy (nbuf,
+ hr->io_buf,
+ hr->io_len);
+ GNUNET_free (hr->io_buf);
+ hr->io_buf = nbuf;
+ hr->io_size = new_size;
}
GNUNET_memcpy (&hr->io_buf[hr->io_len],
upload_data,