paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

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:
MREADME | 14++++++++++++++
Msrc/backend/paivana-httpd.c | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/paivana-httpd.h | 24++++++++++++++++++++++++
Msrc/backend/paivana-httpd_daemon.c | 71++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/backend/paivana-httpd_reverse.c | 25++++++++++++++++++++++---
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,