diff --git a/SOURCES/connection-throttling.patch b/SOURCES/connection-throttling.patch new file mode 100644 index 0000000..50aa191 --- /dev/null +++ b/SOURCES/connection-throttling.patch @@ -0,0 +1,1355 @@ +From 1ee5b053496fcd9de7990d05e8180d548e8ce0da Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Mon, 26 May 2025 16:09:45 +0200 +Subject: [PATCH 1/7] daemon: Use GError auto pointer + +--- + src/grd-daemon.c | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +diff --git a/src/grd-daemon.c b/src/grd-daemon.c +index 6f7bb460..f5595005 100644 +--- a/src/grd-daemon.c ++++ b/src/grd-daemon.c +@@ -292,7 +292,7 @@ main (int argc, char **argv) + }; + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GApplication) app = NULL; +- GError *error = NULL; ++ g_autoptr (GError) error = NULL; + + g_set_application_name (_("GNOME Remote Desktop")); + +@@ -301,7 +301,6 @@ main (int argc, char **argv) + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("Invalid option: %s\n", error->message); +- g_error_free (error); + return EXIT_FAILURE; + } + +-- +2.49.0 + + +From 29e0f5834079069fd445e4a0b643c671eb404dc2 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Wed, 28 May 2025 22:55:20 +0200 +Subject: [PATCH 2/7] utils: Add some time conversion helpers + +Useful when converting between second, microseconds, etc. +--- + src/grd-utils.h | 44 ++++++++++++++++++++++++++++++++++++++++++++ + src/meson.build | 1 + + 2 files changed, 45 insertions(+) + create mode 100644 src/grd-utils.h + +diff --git a/src/grd-utils.h b/src/grd-utils.h +new file mode 100644 +index 00000000..9fdb020a +--- /dev/null ++++ b/src/grd-utils.h +@@ -0,0 +1,44 @@ ++/* ++ * Copyright (C) 2022 Pascal Nowack ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, write to the Free Software ++ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA ++ * 02111-1307, USA. ++ */ ++ ++#ifndef GRD_UTILS_H ++#define GRD_UTILS_H ++ ++#include ++#include ++ ++static inline int64_t ++us (int64_t us) ++{ ++ return us; ++} ++ ++static inline int64_t ++ms2us (int64_t ms) ++{ ++ return us (ms * 1000); ++} ++ ++static inline int64_t ++s2us (uint64_t s) ++{ ++ return ms2us (s * 1000); ++} ++ ++#endif /* GRD_UTILS_H */ +diff --git a/src/meson.build b/src/meson.build +index 9d2f1cea..a2d738ac 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -15,6 +15,7 @@ daemon_sources = files([ + 'grd-stream.c', + 'grd-stream.h', + 'grd-types.h', ++ 'grd-utils.h', + 'grd-vnc-cursor.c', + 'grd-vnc-cursor.h', + 'grd-vnc-pipewire-stream.c', +-- +2.49.0 + + +From 72ea71aa2a4696ff51b22d86f11adc9f0dbff1d8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Mon, 30 Jun 2025 16:16:31 +0200 +Subject: [PATCH 3/7] utils: Add helper to close connection and notify + +Closing doesn't notify that it was closed, so lets add a helper that +does so, that we then can uses to rely on the closed property being up +to date. +--- + src/grd-utils.c | 32 ++++++++++++++++++++++++++++++++ + src/grd-utils.h | 2 ++ + src/meson.build | 1 + + 3 files changed, 35 insertions(+) + create mode 100644 src/grd-utils.c + +diff --git a/src/grd-utils.c b/src/grd-utils.c +new file mode 100644 +index 00000000..772d6fc4 +--- /dev/null ++++ b/src/grd-utils.c +@@ -0,0 +1,32 @@ ++/* ++ * Copyright (C) 2010 Intel Corp. ++ * Copyright (C) 2014 Jonas Ådahl ++ * Copyright (C) 2016-2022 Red Hat Inc. ++ * Copyright (C) 2022 Pascal Nowack ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, write to the Free Software ++ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA ++ * 02111-1307, USA. ++ */ ++ ++#include "config.h" ++ ++#include "grd-utils.h" ++ ++void ++grd_close_connection_and_notify (GSocketConnection *connection) ++{ ++ g_io_stream_close (G_IO_STREAM (connection), NULL, NULL); ++ g_object_notify (G_OBJECT (connection), "closed"); ++} +diff --git a/src/grd-utils.h b/src/grd-utils.h +index 9fdb020a..6fb81717 100644 +--- a/src/grd-utils.h ++++ b/src/grd-utils.h +@@ -23,6 +23,8 @@ + #include + #include + ++void grd_close_connection_and_notify (GSocketConnection *connection); ++ + static inline int64_t + us (int64_t us) + { +diff --git a/src/meson.build b/src/meson.build +index a2d738ac..de6b8cff 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -15,6 +15,7 @@ daemon_sources = files([ + 'grd-stream.c', + 'grd-stream.h', + 'grd-types.h', ++ 'grd-utils.c', + 'grd-utils.h', + 'grd-vnc-cursor.c', + 'grd-vnc-cursor.h', +-- +2.49.0 + + +From 5104e01271fadbefcc277b46cf49c15f977727fa Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Wed, 28 May 2025 22:40:38 +0200 +Subject: [PATCH 4/7] Introduce throttler class + +This handles the throttling of incoming connections, and is intended to +act as a first line of defence against denial of service attacks. It +implements the following: + + * It limits the global number of concurrent active connections + * It limits the number of concurrent active connections per peer + (roughly IP address) + * It limits the number of new connections per second coming from a peer + * It maintains a limited number of pending connections that is waiting + to be handled + +This avoids running into situations where an uncontrolled number of +connection attempts causes resources to be exhausted, e.g. number of +file descriptor exceeding the process limit. + +The limits are currently hard coded. + +Related: CVE-2025-5024 +--- + src/grd-throttler.c | 467 ++++++++++++++++++++++++++++++++++++++++++++ + src/grd-throttler.h | 41 ++++ + src/meson.build | 2 + + 3 files changed, 510 insertions(+) + create mode 100644 src/grd-throttler.c + create mode 100644 src/grd-throttler.h + +diff --git a/src/grd-throttler.c b/src/grd-throttler.c +new file mode 100644 +index 00000000..e584433a +--- /dev/null ++++ b/src/grd-throttler.c +@@ -0,0 +1,467 @@ ++/* ++ * Copyright (C) 2025 Red Hat ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, write to the Free Software ++ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA ++ * 02111-1307, USA. ++ */ ++ ++#include "config.h" ++ ++#include "grd-throttler.h" ++ ++#include "grd-utils.h" ++ ++#define MAX_GLOBAL_CONNECTIONS 10 ++#define MAX_CONNECTIONS_PER_PEER 5 ++#define MAX_PENDING_CONNECTIONS 5 ++#define MAX_ATTEMPTS_PER_SECOND 5 ++ ++#define PRUNE_TIME_CUTOFF_US (s2us (1)) ++ ++typedef struct _GrdPeer ++{ ++ GrdThrottler *throttler; ++ char *name; ++ int active_connections; ++ GArray *connect_timestamps; ++ GQueue *delayed_connections; ++ int64_t last_accept_us; ++} GrdPeer; ++ ++struct _GrdThrottler ++{ ++ GObject parent; ++ ++ int active_connections; ++ ++ GrdThrottlerAllowCallback allow_callback; ++ gpointer user_data; ++ ++ GHashTable *peers; ++ ++ GSource *delayed_connections_source; ++}; ++ ++G_DEFINE_TYPE (GrdThrottler, grd_throttler, G_TYPE_OBJECT) ++ ++static GQuark quark_remote_address; ++ ++static void ++maybe_queue_timeout (GrdThrottler *throttler); ++ ++static void ++prune_old_timestamps (GArray *timestamps, ++ int64_t now_us) ++{ ++ size_t i; ++ ++ if (!timestamps) ++ return; ++ ++ for (i = 0; i < timestamps->len; i++) ++ { ++ int64_t ts_us = g_array_index (timestamps, int64_t, i); ++ ++ if (now_us - ts_us < PRUNE_TIME_CUTOFF_US) ++ break; ++ } ++ g_array_remove_range (timestamps, 0, i); ++} ++ ++static void ++maybe_dispose_peer (GrdThrottler *throttler, ++ GrdPeer *peer, ++ GHashTableIter *iter) ++{ ++ if (peer->active_connections > 0) ++ return; ++ ++ if (peer->connect_timestamps && peer->connect_timestamps->len > 0) ++ return; ++ ++ if (!g_queue_is_empty (peer->delayed_connections)) ++ return; ++ ++ if (iter) ++ g_hash_table_iter_remove (iter); ++ else ++ g_hash_table_remove (throttler->peers, peer->name); ++} ++ ++static void ++grd_throttler_register_connection (GrdThrottler *throttler, ++ GrdPeer *peer) ++{ ++ int64_t now_us; ++ ++ peer->active_connections++; ++ throttler->active_connections++; ++ ++ if (!peer->connect_timestamps) ++ peer->connect_timestamps = g_array_new (FALSE, FALSE, sizeof (int64_t)); ++ ++ now_us = g_get_monotonic_time (); ++ ++ prune_old_timestamps (peer->connect_timestamps, now_us); ++ g_array_append_val (peer->connect_timestamps, now_us); ++} ++ ++static void ++grd_throttler_unregister_connection (GrdThrottler *throttler, ++ GSocketConnection *connection, ++ GrdPeer *peer) ++{ ++ g_return_if_fail (peer->active_connections > 0); ++ g_return_if_fail (throttler->active_connections > 0); ++ ++ peer->active_connections--; ++ throttler->active_connections--; ++ ++ maybe_dispose_peer (throttler, peer, NULL); ++ maybe_queue_timeout (throttler); ++} ++ ++static void ++grd_throttler_deny_connection (GrdThrottler *throttler, ++ const char *peer_name, ++ GSocketConnection *connection) ++{ ++ g_debug ("Denying connection from %s", peer_name); ++ g_io_stream_close (G_IO_STREAM (connection), NULL, NULL); ++} ++ ++static void ++on_connection_closed_changed (GSocketConnection *connection, ++ GParamSpec *pspec, ++ GrdThrottler *throttler) ++{ ++ const char *peer_name; ++ GrdPeer *peer; ++ ++ g_assert (g_io_stream_is_closed (G_IO_STREAM (connection))); ++ ++ peer_name = g_object_get_qdata (G_OBJECT (connection), quark_remote_address); ++ peer = g_hash_table_lookup (throttler->peers, peer_name); ++ grd_throttler_unregister_connection (throttler, connection, peer); ++} ++ ++static void ++grd_throttler_allow_connection (GrdThrottler *throttler, ++ GSocketConnection *connection, ++ GrdPeer *peer) ++{ ++ g_debug ("Accepting connection from %s", peer->name); ++ ++ throttler->allow_callback (throttler, connection, throttler->user_data); ++ ++ peer->last_accept_us = g_get_monotonic_time (); ++ ++ g_object_set_qdata_full (G_OBJECT (connection), quark_remote_address, ++ g_strdup (peer->name), g_free); ++ grd_throttler_register_connection (throttler, peer); ++ g_signal_connect (connection, "notify::closed", ++ G_CALLBACK (on_connection_closed_changed), throttler); ++} ++ ++static gboolean ++source_dispatch (GSource *source, ++ GSourceFunc callback, ++ gpointer user_data) ++{ ++ g_source_set_ready_time (source, -1); ++ ++ return callback (user_data); ++} ++ ++static GSourceFuncs source_funcs = ++{ ++ .dispatch = source_dispatch, ++}; ++ ++static void ++prune_closed_connections (GQueue *queue) ++{ ++ GList *l; ++ ++ l = queue->head; ++ while (l) ++ { ++ GSocketConnection *connection = G_SOCKET_CONNECTION (l->data); ++ GList *l_next = l->next; ++ ++ if (g_io_stream_is_closed (G_IO_STREAM (connection))) ++ { ++ g_queue_delete_link (queue, l); ++ g_object_unref (connection); ++ } ++ ++ l = l_next; ++ } ++} ++ ++static gboolean ++is_connection_limit_reached (GrdThrottler *throttler, ++ GrdPeer *peer) ++{ ++ if (peer->active_connections >= MAX_CONNECTIONS_PER_PEER) ++ return TRUE; ++ ++ if (throttler->active_connections >= MAX_GLOBAL_CONNECTIONS) ++ return TRUE; ++ ++ return FALSE; ++} ++ ++static gboolean ++is_new_connection_allowed (GrdThrottler *throttler, ++ GrdPeer *peer) ++{ ++ if (is_connection_limit_reached (throttler, peer)) ++ return FALSE; ++ ++ if (peer->connect_timestamps && ++ peer->connect_timestamps->len >= MAX_ATTEMPTS_PER_SECOND) ++ return FALSE; ++ ++ return TRUE; ++} ++ ++static gboolean ++dispatch_delayed_connections (gpointer user_data) ++{ ++ GrdThrottler *throttler = GRD_THROTTLER (user_data); ++ GHashTableIter iter; ++ gpointer key, value; ++ ++ g_hash_table_iter_init (&iter, throttler->peers); ++ while (g_hash_table_iter_next (&iter, &key, &value)) ++ { ++ GrdPeer *peer = value; ++ GQueue *queue = peer->delayed_connections; ++ GSocketConnection *connection; ++ ++ prune_closed_connections (queue); ++ connection = g_queue_peek_tail (queue); ++ ++ if (!connection) ++ { ++ maybe_dispose_peer (throttler, peer, &iter); ++ continue; ++ } ++ ++ if (is_new_connection_allowed (throttler, peer)) ++ { ++ g_queue_pop_tail (queue); ++ grd_throttler_allow_connection (throttler, connection, peer); ++ g_object_unref (connection); ++ } ++ } ++ ++ maybe_queue_timeout (throttler); ++ ++ return G_SOURCE_CONTINUE; ++} ++ ++static void ++ensure_delayed_connections_source (GrdThrottler *throttler) ++{ ++ if (throttler->delayed_connections_source) ++ return; ++ ++ throttler->delayed_connections_source = g_source_new (&source_funcs, ++ sizeof (GSource)); ++ g_source_set_callback (throttler->delayed_connections_source, ++ dispatch_delayed_connections, throttler, NULL); ++ g_source_attach (throttler->delayed_connections_source, NULL); ++ g_source_unref (throttler->delayed_connections_source); ++} ++ ++static void ++maybe_queue_timeout (GrdThrottler *throttler) ++{ ++ GHashTableIter iter; ++ gpointer key, value; ++ int64_t next_timeout_us = INT64_MAX; ++ ++ ++ g_hash_table_iter_init (&iter, throttler->peers); ++ while (g_hash_table_iter_next (&iter, &key, &value)) ++ { ++ GrdPeer *peer = value; ++ ++ if (is_connection_limit_reached (throttler, peer)) ++ continue; ++ ++ if (g_queue_is_empty (peer->delayed_connections)) ++ continue; ++ ++ next_timeout_us = MIN (next_timeout_us, ++ peer->last_accept_us + ++ G_USEC_PER_SEC / MAX_ATTEMPTS_PER_SECOND); ++ } ++ ++ ++ if (next_timeout_us == INT64_MAX) ++ next_timeout_us = -1; ++ ++ if (next_timeout_us >= 0) ++ { ++ ensure_delayed_connections_source (throttler); ++ g_source_set_ready_time (throttler->delayed_connections_source, ++ next_timeout_us); ++ } ++} ++ ++static void ++maybe_delay_connection (GrdThrottler *throttler, ++ GSocketConnection *connection, ++ GrdPeer *peer, ++ int64_t now_us) ++{ ++ GQueue *delayed_connections; ++ ++ delayed_connections = peer->delayed_connections; ++ if (!delayed_connections) ++ { ++ delayed_connections = g_queue_new (); ++ peer->delayed_connections = delayed_connections; ++ } ++ ++ if (g_queue_get_length (delayed_connections) > MAX_PENDING_CONNECTIONS) ++ { ++ grd_throttler_deny_connection (throttler, peer->name, connection); ++ return; ++ } ++ ++ g_debug ("Delaying connection from %s", peer->name); ++ ++ g_queue_push_head (delayed_connections, g_object_ref (connection)); ++ maybe_queue_timeout (throttler); ++} ++ ++static GrdPeer * ++ensure_peer (GrdThrottler *throttler, ++ const char *peer_name) ++{ ++ GrdPeer *peer; ++ ++ peer = g_hash_table_lookup (throttler->peers, peer_name); ++ if (peer) ++ return peer; ++ ++ peer = g_new0 (GrdPeer, 1); ++ peer->throttler = throttler; ++ peer->name = g_strdup (peer_name); ++ peer->delayed_connections = g_queue_new (); ++ ++ g_hash_table_insert (throttler->peers, ++ g_strdup (peer_name), peer); ++ ++ return peer; ++} ++ ++void ++grd_throttler_handle_connection (GrdThrottler *throttler, ++ GSocketConnection *connection) ++{ ++ g_autoptr (GError) error = NULL; ++ g_autoptr (GSocketAddress) remote_address = NULL; ++ GInetAddress *inet_address; ++ g_autofree char *peer_name = NULL; ++ GrdPeer *peer; ++ int64_t now_us; ++ ++ remote_address = g_socket_connection_get_remote_address (connection, &error); ++ if (!remote_address) ++ { ++ g_warning ("Failed to get remote address: %s", error->message); ++ grd_throttler_deny_connection (throttler, "unknown peer", connection); ++ return; ++ } ++ ++ inet_address = ++ g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (remote_address)); ++ peer_name = g_inet_address_to_string (inet_address); ++ ++ g_debug ("New incoming connection from %s", peer_name); ++ ++ peer = ensure_peer (throttler, peer_name); ++ ++ prune_closed_connections (peer->delayed_connections); ++ ++ now_us = g_get_monotonic_time (); ++ prune_old_timestamps (peer->connect_timestamps, now_us); ++ if (is_new_connection_allowed (throttler, peer) && ++ g_queue_get_length (peer->delayed_connections) == 0) ++ { ++ grd_throttler_allow_connection (throttler, connection, peer); ++ return; ++ } ++ ++ maybe_delay_connection (throttler, connection, peer, now_us); ++} ++ ++GrdThrottler * ++grd_throttler_new (GrdThrottlerAllowCallback allow_callback, ++ gpointer user_data) ++{ ++ GrdThrottler *throttler; ++ ++ throttler = g_object_new (GRD_TYPE_THROTTLER, NULL); ++ throttler->allow_callback = allow_callback; ++ throttler->user_data = user_data; ++ ++ return throttler; ++} ++ ++static void ++grd_peer_free (GrdPeer *peer) ++{ ++ if (peer->delayed_connections) ++ g_queue_free_full (peer->delayed_connections, g_object_unref); ++ g_clear_pointer (&peer->connect_timestamps, g_array_unref); ++ g_clear_pointer (&peer->name, g_free); ++ g_free (peer); ++} ++ ++static void ++grd_throttler_finalize (GObject *object) ++{ ++ GrdThrottler *throttler = GRD_THROTTLER(object); ++ ++ g_clear_pointer (&throttler->delayed_connections_source, g_source_destroy); ++ g_clear_pointer (&throttler->peers, g_hash_table_unref); ++ ++ G_OBJECT_CLASS (grd_throttler_parent_class)->finalize (object); ++} ++ ++static void ++grd_throttler_init (GrdThrottler *throttler) ++{ ++ throttler->peers = ++ g_hash_table_new_full (g_str_hash, g_str_equal, ++ g_free, (GDestroyNotify) grd_peer_free); ++} ++ ++static void ++grd_throttler_class_init (GrdThrottlerClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ ++ object_class->finalize = grd_throttler_finalize; ++ ++ quark_remote_address = ++ g_quark_from_static_string ("grd-remote-address-string"); ++} +diff --git a/src/grd-throttler.h b/src/grd-throttler.h +new file mode 100644 +index 00000000..d57d653a +--- /dev/null ++++ b/src/grd-throttler.h +@@ -0,0 +1,41 @@ ++/* ++ * Copyright (C) 2025 Red Hat ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, write to the Free Software ++ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA ++ * 02111-1307, USA. ++ */ ++ ++#ifndef GRD_THROTTLER_H ++#define GRD_THROTTLER_H ++ ++#include ++#include ++ ++#define GRD_TYPE_THROTTLER (grd_throttler_get_type()) ++G_DECLARE_FINAL_TYPE (GrdThrottler, grd_throttler, GRD, THROTTLER, GObject) ++ ++typedef void (* GrdThrottlerAllowCallback) (GrdThrottler *throttler, ++ GSocketConnection *connection, ++ gpointer user_data); ++ ++void ++grd_throttler_handle_connection (GrdThrottler *throttler, ++ GSocketConnection *connection); ++ ++GrdThrottler * ++grd_throttler_new (GrdThrottlerAllowCallback allow_callback, ++ gpointer user_data); ++ ++#endif /* GRD_THROTTLER_H */ +diff --git a/src/meson.build b/src/meson.build +index de6b8cff..26a1039c 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -14,6 +14,8 @@ daemon_sources = files([ + 'grd-settings.h', + 'grd-stream.c', + 'grd-stream.h', ++ 'grd-throttler.c', ++ 'grd-throttler.h', + 'grd-types.h', + 'grd-utils.c', + 'grd-utils.h', +-- +2.49.0 + + +From c1a2a397f80ea4df6f8d7e7bd116c90ff6f9bb70 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Fri, 30 May 2025 16:54:00 +0200 +Subject: [PATCH 5/7] throttler: Introduce limits struct + +This will be used by the server implementation to customize throttling +limits. This will be used by the VNC server to limit to one global +session at a time, replacing it's own existing condition. +--- + src/grd-throttler.c | 64 +++++++++++++++++++++++++++++++++++++-------- + src/grd-throttler.h | 14 ++++++++-- + 2 files changed, 65 insertions(+), 13 deletions(-) + +diff --git a/src/grd-throttler.c b/src/grd-throttler.c +index e584433a..0429a089 100644 +--- a/src/grd-throttler.c ++++ b/src/grd-throttler.c +@@ -23,10 +23,18 @@ + + #include "grd-utils.h" + +-#define MAX_GLOBAL_CONNECTIONS 10 +-#define MAX_CONNECTIONS_PER_PEER 5 +-#define MAX_PENDING_CONNECTIONS 5 +-#define MAX_ATTEMPTS_PER_SECOND 5 ++#define DEFAULT_MAX_GLOBAL_CONNECTIONS 10 ++#define DEFAULT_MAX_CONNECTIONS_PER_PEER 5 ++#define DEFAULT_MAX_PENDING_CONNECTIONS 5 ++#define DEFAULT_MAX_ATTEMPTS_PER_SECOND 10 ++ ++struct _GrdThrottlerLimits ++{ ++ int max_global_connections; ++ int max_connections_per_peer; ++ int max_pending_connections; ++ int max_attempts_per_second; ++}; + + #define PRUNE_TIME_CUTOFF_US (s2us (1)) + +@@ -44,6 +52,8 @@ struct _GrdThrottler + { + GObject parent; + ++ GrdThrottlerLimits *limits; ++ + int active_connections; + + GrdThrottlerAllowCallback allow_callback; +@@ -215,10 +225,12 @@ static gboolean + is_connection_limit_reached (GrdThrottler *throttler, + GrdPeer *peer) + { +- if (peer->active_connections >= MAX_CONNECTIONS_PER_PEER) ++ GrdThrottlerLimits *limits = throttler->limits; ++ ++ if (peer->active_connections >= limits->max_connections_per_peer) + return TRUE; + +- if (throttler->active_connections >= MAX_GLOBAL_CONNECTIONS) ++ if (throttler->active_connections >= limits->max_global_connections) + return TRUE; + + return FALSE; +@@ -228,11 +240,13 @@ static gboolean + is_new_connection_allowed (GrdThrottler *throttler, + GrdPeer *peer) + { ++ GrdThrottlerLimits *limits = throttler->limits; ++ + if (is_connection_limit_reached (throttler, peer)) + return FALSE; + + if (peer->connect_timestamps && +- peer->connect_timestamps->len >= MAX_ATTEMPTS_PER_SECOND) ++ peer->connect_timestamps->len >= limits->max_attempts_per_second) + return FALSE; + + return TRUE; +@@ -291,6 +305,7 @@ ensure_delayed_connections_source (GrdThrottler *throttler) + static void + maybe_queue_timeout (GrdThrottler *throttler) + { ++ GrdThrottlerLimits *limits = throttler->limits; + GHashTableIter iter; + gpointer key, value; + int64_t next_timeout_us = INT64_MAX; +@@ -309,7 +324,7 @@ maybe_queue_timeout (GrdThrottler *throttler) + + next_timeout_us = MIN (next_timeout_us, + peer->last_accept_us + +- G_USEC_PER_SEC / MAX_ATTEMPTS_PER_SECOND); ++ G_USEC_PER_SEC / limits->max_attempts_per_second); + } + + +@@ -330,6 +345,7 @@ maybe_delay_connection (GrdThrottler *throttler, + GrdPeer *peer, + int64_t now_us) + { ++ GrdThrottlerLimits *limits = throttler->limits; + GQueue *delayed_connections; + + delayed_connections = peer->delayed_connections; +@@ -339,7 +355,7 @@ maybe_delay_connection (GrdThrottler *throttler, + peer->delayed_connections = delayed_connections; + } + +- if (g_queue_get_length (delayed_connections) > MAX_PENDING_CONNECTIONS) ++ if (g_queue_get_length (delayed_connections) > limits->max_pending_connections) + { + grd_throttler_deny_connection (throttler, peer->name, connection); + return; +@@ -413,15 +429,40 @@ grd_throttler_handle_connection (GrdThrottler *throttler, + maybe_delay_connection (throttler, connection, peer, now_us); + } + ++void ++grd_throttler_limits_set_max_global_connections (GrdThrottlerLimits *limits, ++ int limit) ++{ ++ limits->max_global_connections = limit; ++} ++ ++GrdThrottlerLimits * ++grd_throttler_limits_new (void) ++{ ++ GrdThrottlerLimits *limits; ++ ++ limits = g_new0 (GrdThrottlerLimits, 1); ++ limits->max_global_connections = DEFAULT_MAX_GLOBAL_CONNECTIONS; ++ limits->max_connections_per_peer = DEFAULT_MAX_CONNECTIONS_PER_PEER; ++ limits->max_pending_connections = DEFAULT_MAX_PENDING_CONNECTIONS; ++ limits->max_attempts_per_second = DEFAULT_MAX_ATTEMPTS_PER_SECOND; ++ ++ return limits; ++} ++ + GrdThrottler * +-grd_throttler_new (GrdThrottlerAllowCallback allow_callback, +- gpointer user_data) ++grd_throttler_new (GrdThrottlerLimits *limits, ++ GrdThrottlerAllowCallback allow_callback, ++ gpointer user_data) + { + GrdThrottler *throttler; + ++ g_assert (limits); ++ + throttler = g_object_new (GRD_TYPE_THROTTLER, NULL); + throttler->allow_callback = allow_callback; + throttler->user_data = user_data; ++ throttler->limits = limits; + + return throttler; + } +@@ -443,6 +484,7 @@ grd_throttler_finalize (GObject *object) + + g_clear_pointer (&throttler->delayed_connections_source, g_source_destroy); + g_clear_pointer (&throttler->peers, g_hash_table_unref); ++ g_clear_pointer (&throttler->limits, g_free); + + G_OBJECT_CLASS (grd_throttler_parent_class)->finalize (object); + } +diff --git a/src/grd-throttler.h b/src/grd-throttler.h +index d57d653a..9e72b8f1 100644 +--- a/src/grd-throttler.h ++++ b/src/grd-throttler.h +@@ -23,6 +23,8 @@ + #include + #include + ++typedef struct _GrdThrottlerLimits GrdThrottlerLimits; ++ + #define GRD_TYPE_THROTTLER (grd_throttler_get_type()) + G_DECLARE_FINAL_TYPE (GrdThrottler, grd_throttler, GRD, THROTTLER, GObject) + +@@ -34,8 +36,16 @@ void + grd_throttler_handle_connection (GrdThrottler *throttler, + GSocketConnection *connection); + ++void ++grd_throttler_limits_set_max_global_connections (GrdThrottlerLimits *limits, ++ int limit); ++ ++GrdThrottlerLimits * ++grd_throttler_limits_new (void); ++ + GrdThrottler * +-grd_throttler_new (GrdThrottlerAllowCallback allow_callback, +- gpointer user_data); ++grd_throttler_new (GrdThrottlerLimits *limits, ++ GrdThrottlerAllowCallback allow_callback, ++ gpointer user_data); + + #endif /* GRD_THROTTLER_H */ +-- +2.49.0 + + +From 70a97969a131bd64f0e7e529c592fa6a7ecbb6b6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Fri, 30 May 2025 16:55:46 +0200 +Subject: [PATCH 6/7] vnc-server: Hook up VNC server to the throttler + +This allows us to replace existing single session limitation with +limiting the throttler to one accepted connection at any given time. +--- + src/grd-session-vnc.c | 2 ++ + src/grd-vnc-server.c | 45 +++++++++++++++++++++++++++++++++++-------- + 2 files changed, 39 insertions(+), 8 deletions(-) + +diff --git a/src/grd-session-vnc.c b/src/grd-session-vnc.c +index 7950d1e0..fccbf741 100644 +--- a/src/grd-session-vnc.c ++++ b/src/grd-session-vnc.c +@@ -32,6 +32,7 @@ + #include "grd-prompt.h" + #include "grd-settings.h" + #include "grd-stream.h" ++#include "grd-utils.h" + #include "grd-vnc-server.h" + #include "grd-vnc-pipewire-stream.h" + +@@ -775,6 +776,7 @@ grd_session_vnc_stop (GrdSession *session) + + grd_session_vnc_pause (session_vnc); + ++ grd_close_connection_and_notify (session_vnc->connection); + g_clear_object (&session_vnc->connection); + g_clear_pointer (&session_vnc->rfb_screen->frameBuffer, g_free); + g_clear_pointer (&session_vnc->rfb_screen, rfbScreenCleanup); +diff --git a/src/grd-vnc-server.c b/src/grd-vnc-server.c +index f9c68dba..f7240d37 100644 +--- a/src/grd-vnc-server.c ++++ b/src/grd-vnc-server.c +@@ -30,9 +30,10 @@ + + #include "grd-context.h" + #include "grd-session-vnc.h" ++#include "grd-throttler.h" ++#include "grd-utils.h" + #include "grd-vnc-tls.h" + +- + enum + { + PROP_0, +@@ -44,6 +45,8 @@ struct _GrdVncServer + { + GSocketService parent; + ++ GrdThrottler *throttler; ++ + GList *sessions; + + GList *stopped_sessions; +@@ -54,6 +57,11 @@ struct _GrdVncServer + + G_DEFINE_TYPE (GrdVncServer, grd_vnc_server, G_TYPE_SOCKET_SERVICE); + ++static void ++allow_connection_cb (GrdThrottler *throttler, ++ GSocketConnection *connection, ++ gpointer user_data); ++ + GrdContext * + grd_vnc_server_get_context (GrdVncServer *vnc_server) + { +@@ -104,23 +112,24 @@ on_session_stopped (GrdSession *session, GrdVncServer *vnc_server) + } + } + +-static gboolean +-on_incoming (GSocketService *service, +- GSocketConnection *connection) ++static void ++allow_connection_cb (GrdThrottler *throttler, ++ GSocketConnection *connection, ++ gpointer user_data) + { +- GrdVncServer *vnc_server = GRD_VNC_SERVER (service); ++ GrdVncServer *vnc_server = GRD_VNC_SERVER (user_data); + GrdSessionVnc *session_vnc; + +- g_debug ("New incoming VNC connection"); +- + if (vnc_server->sessions) + { + /* TODO: Add the rfbScreen instance to GrdVncServer to support multiple + * sessions. */ + g_debug ("Refusing new VNC connection: already an active session"); +- return TRUE; ++ return; + } + ++ g_debug ("Creating new VNC session"); ++ + session_vnc = grd_session_vnc_new (vnc_server, connection); + vnc_server->sessions = g_list_append (vnc_server->sessions, session_vnc); + grd_context_add_session (vnc_server->context, GRD_SESSION (session_vnc)); +@@ -128,7 +137,16 @@ on_incoming (GSocketService *service, + g_signal_connect (session_vnc, "stopped", + G_CALLBACK (on_session_stopped), + vnc_server); ++} + ++static gboolean ++on_incoming (GSocketService *service, ++ GSocketConnection *connection) ++{ ++ GrdVncServer *vnc_server = GRD_VNC_SERVER (service); ++ ++ grd_throttler_handle_connection (vnc_server->throttler, ++ connection); + return TRUE; + } + +@@ -252,6 +270,8 @@ grd_vnc_server_dispose (GObject *object) + vnc_server->sessions = NULL; + } + ++ g_clear_object (&vnc_server->throttler); ++ + G_OBJECT_CLASS (grd_vnc_server_parent_class)->dispose (object); + } + +@@ -277,6 +297,15 @@ grd_vnc_server_constructed (GObject *object) + static void + grd_vnc_server_init (GrdVncServer *vnc_server) + { ++ GrdThrottlerLimits *limits; ++ ++ limits = grd_throttler_limits_new (); ++ /* TODO: Add the rfbScreen instance to GrdVncServer to support multiple ++ * sessions. */ ++ grd_throttler_limits_set_max_global_connections (limits, 1); ++ vnc_server->throttler = grd_throttler_new (limits, ++ allow_connection_cb, ++ vnc_server); + } + + static void +-- +2.49.0 + + +From 0f23aed843dacb1dceeb88cbeb9f545117d1c42d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Mon, 30 Jun 2025 16:01:37 +0200 +Subject: [PATCH 7/7] throttler: Allow overriding hard coded parallel + connections limit + +Overriding is done via an argument passed to the daemon. This allows +using the same daemon with a larger amount of users, which one would +achieve by overriding the relevant systemd service file by both adding +the --max-parallel-connections argument, as well as bumping the limit of +number of open file descriptors to something adequate. +--- + src/grd-daemon.c | 32 +++++++++++++++++++++++++++----- + src/grd-settings.c | 15 +++++++++++++++ + src/grd-settings.h | 5 +++++ + src/grd-throttler.c | 9 ++++++--- + src/grd-throttler.h | 4 +++- + src/grd-vnc-server.c | 17 +++++++++-------- + 6 files changed, 65 insertions(+), 17 deletions(-) + +diff --git a/src/grd-daemon.c b/src/grd-daemon.c +index f5595005..e00304d8 100644 +--- a/src/grd-daemon.c ++++ b/src/grd-daemon.c +@@ -35,6 +35,8 @@ + #include "grd-session.h" + #include "grd-vnc-server.h" + ++#define DEFAULT_MAX_PARALLEL_CONNECTIONS 10 ++ + struct _GrdDaemon + { + GApplication parent; +@@ -50,6 +52,9 @@ struct _GrdDaemon + + G_DEFINE_TYPE (GrdDaemon, grd_daemon, G_TYPE_APPLICATION) + ++#define QUOTE1(a) #a ++#define QUOTE(a) QUOTE1(a) ++ + static gboolean + is_daemon_ready (GrdDaemon *daemon) + { +@@ -282,17 +287,23 @@ main (int argc, char **argv) + { + gboolean print_version = FALSE; + int vnc_port = -1; ++ int max_parallel_connections = DEFAULT_MAX_PARALLEL_CONNECTIONS; + + GOptionEntry entries[] = { + { "version", 0, 0, G_OPTION_ARG_NONE, &print_version, + "Print version", NULL }, + { "vnc-port", 0, 0, G_OPTION_ARG_INT, &vnc_port, + "VNC port", NULL }, ++ { "max-parallel-connections", 0, 0, ++ G_OPTION_ARG_INT, &max_parallel_connections, ++ "Max number of parallel connections (0 for unlimited, " ++ "default: " QUOTE(DEFAULT_MAX_PARALLEL_CONNECTIONS) ")", NULL }, + { NULL } + }; + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GApplication) app = NULL; + g_autoptr (GError) error = NULL; ++ GrdSettings *settings; + + g_set_application_name (_("GNOME Remote Desktop")); + +@@ -317,13 +328,24 @@ main (int argc, char **argv) + + add_actions (app); + +- if (vnc_port != -1) ++ if (max_parallel_connections == 0) + { +- GrdSettings *settings; +- +- settings = grd_context_get_settings (GRD_DAEMON (app)->context); +- grd_settings_override_vnc_port (settings, vnc_port); ++ max_parallel_connections = INT_MAX; + } ++ else if (max_parallel_connections < 0) ++ { ++ g_printerr ("Invalid number of max parallel connections: %d\n", ++ max_parallel_connections); ++ return EXIT_SUCCESS; ++ } ++ ++ settings = grd_context_get_settings (GRD_DAEMON (app)->context); ++ ++ if (vnc_port != -1) ++ grd_settings_override_vnc_port (settings, vnc_port); ++ ++ grd_settings_override_max_parallel_connections (settings, ++ max_parallel_connections); + + return g_application_run (app, argc, argv); + } +diff --git a/src/grd-settings.c b/src/grd-settings.c +index 7324310b..52cb1702 100644 +--- a/src/grd-settings.c ++++ b/src/grd-settings.c +@@ -50,6 +50,8 @@ struct _GrdSettings + int port; + GrdVncEncryption encryption; + } vnc; ++ ++ int max_parallel_connections; + }; + + G_DEFINE_TYPE (GrdSettings, grd_settings, G_TYPE_OBJECT) +@@ -75,6 +77,19 @@ grd_settings_get_vnc_port (GrdSettings *settings) + return settings->vnc.port; + } + ++void ++grd_settings_override_max_parallel_connections (GrdSettings *settings, ++ int max_parallel_connections) ++{ ++ settings->max_parallel_connections = max_parallel_connections; ++} ++ ++int ++grd_settings_get_max_parallel_connections (GrdSettings *settings) ++{ ++ return settings->max_parallel_connections; ++} ++ + void + grd_settings_override_vnc_port (GrdSettings *settings, + int port) +diff --git a/src/grd-settings.h b/src/grd-settings.h +index 0575ec1e..999581f4 100644 +--- a/src/grd-settings.h ++++ b/src/grd-settings.h +@@ -35,6 +35,11 @@ const SecretSchema * cc_grd_vnc_password_get_schema (void); + + int grd_settings_get_vnc_port (GrdSettings *settings); + ++void grd_settings_override_max_parallel_connections (GrdSettings *settings, ++ int max_parallel_connections); ++ ++int grd_settings_get_max_parallel_connections (GrdSettings *settings); ++ + void grd_settings_override_vnc_port (GrdSettings *settings, + int port); + +diff --git a/src/grd-throttler.c b/src/grd-throttler.c +index 0429a089..9ec31b35 100644 +--- a/src/grd-throttler.c ++++ b/src/grd-throttler.c +@@ -21,9 +21,10 @@ + + #include "grd-throttler.h" + ++#include "grd-context.h" ++#include "grd-settings.h" + #include "grd-utils.h" + +-#define DEFAULT_MAX_GLOBAL_CONNECTIONS 10 + #define DEFAULT_MAX_CONNECTIONS_PER_PEER 5 + #define DEFAULT_MAX_PENDING_CONNECTIONS 5 + #define DEFAULT_MAX_ATTEMPTS_PER_SECOND 10 +@@ -437,12 +438,14 @@ grd_throttler_limits_set_max_global_connections (GrdThrottlerLimits *limits, + } + + GrdThrottlerLimits * +-grd_throttler_limits_new (void) ++grd_throttler_limits_new (GrdContext *context) + { ++ GrdSettings *settings = grd_context_get_settings (context); + GrdThrottlerLimits *limits; + + limits = g_new0 (GrdThrottlerLimits, 1); +- limits->max_global_connections = DEFAULT_MAX_GLOBAL_CONNECTIONS; ++ limits->max_global_connections = ++ grd_settings_get_max_parallel_connections (settings); + limits->max_connections_per_peer = DEFAULT_MAX_CONNECTIONS_PER_PEER; + limits->max_pending_connections = DEFAULT_MAX_PENDING_CONNECTIONS; + limits->max_attempts_per_second = DEFAULT_MAX_ATTEMPTS_PER_SECOND; +diff --git a/src/grd-throttler.h b/src/grd-throttler.h +index 9e72b8f1..a9554202 100644 +--- a/src/grd-throttler.h ++++ b/src/grd-throttler.h +@@ -23,6 +23,8 @@ + #include + #include + ++#include "grd-types.h" ++ + typedef struct _GrdThrottlerLimits GrdThrottlerLimits; + + #define GRD_TYPE_THROTTLER (grd_throttler_get_type()) +@@ -41,7 +43,7 @@ grd_throttler_limits_set_max_global_connections (GrdThrottlerLimits *limits, + int limit); + + GrdThrottlerLimits * +-grd_throttler_limits_new (void); ++grd_throttler_limits_new (GrdContext *context); + + GrdThrottler * + grd_throttler_new (GrdThrottlerLimits *limits, +diff --git a/src/grd-vnc-server.c b/src/grd-vnc-server.c +index f7240d37..bddc6909 100644 +--- a/src/grd-vnc-server.c ++++ b/src/grd-vnc-server.c +@@ -280,6 +280,7 @@ grd_vnc_server_constructed (GObject *object) + { + GrdVncServer *vnc_server = GRD_VNC_SERVER (object); + GrdSettings *settings = grd_context_get_settings (vnc_server->context); ++ GrdThrottlerLimits *limits; + + if (grd_context_get_debug_flags (vnc_server->context) & GRD_DEBUG_VNC) + rfbLogEnable (1); +@@ -291,21 +292,21 @@ grd_vnc_server_constructed (GObject *object) + vnc_server); + sync_encryption_settings (vnc_server); + +- G_OBJECT_CLASS (grd_vnc_server_parent_class)->constructed (object); +-} +- +-static void +-grd_vnc_server_init (GrdVncServer *vnc_server) +-{ +- GrdThrottlerLimits *limits; ++ limits = grd_throttler_limits_new (vnc_server->context); + +- limits = grd_throttler_limits_new (); + /* TODO: Add the rfbScreen instance to GrdVncServer to support multiple + * sessions. */ + grd_throttler_limits_set_max_global_connections (limits, 1); + vnc_server->throttler = grd_throttler_new (limits, + allow_connection_cb, + vnc_server); ++ ++ G_OBJECT_CLASS (grd_vnc_server_parent_class)->constructed (object); ++} ++ ++static void ++grd_vnc_server_init (GrdVncServer *vnc_server) ++{ + } + + static void +-- +2.49.0 + diff --git a/SPECS/gnome-remote-desktop.spec b/SPECS/gnome-remote-desktop.spec index 3590d90..f858cab 100644 --- a/SPECS/gnome-remote-desktop.spec +++ b/SPECS/gnome-remote-desktop.spec @@ -2,7 +2,7 @@ Name: gnome-remote-desktop Version: 0.1.8 -Release: 3%{?dist} +Release: 4%{?dist} Summary: GNOME Remote Desktop screen share service License: GPLv2+ @@ -23,6 +23,9 @@ Patch5: 0001-vnc-pipewire-stream-Remove-assert.patch # Cursor only frame fixes (#1837406) Patch6: cursor-only-frame-fixes.patch +# Connection throttling (RHEL-92791) +Patch7: connection-throttling.patch + BuildRequires: git BuildRequires: gcc BuildRequires: meson >= 0.36.0 @@ -81,6 +84,10 @@ GNOME desktop environment. %changelog +* Thu Jul 03 2025 Jonas Ådahl - 0.1.8-4 +- Backport connection throttling + Resolves: RHEL-92791 + * Wed Jul 15 2020 Jonas Ådahl - 0.1.8-3 - Backport cursor only frame fixes Related: #1837406