diff --git a/src/grd-control.c b/src/grd-control.c index 858ab10ada9898e7e73aeb203731be26ed903a76..284cb113f45c5053ae037beff8e8b71121d54240 100644 --- a/src/grd-control.c +++ b/src/grd-control.c @@ -33,13 +33,25 @@ main (int argc, char **argv) { g_autoptr(GApplication) app = NULL; gboolean terminate = FALSE; + gboolean headless = FALSE; + gboolean system = FALSE; + gboolean handover = FALSE; GOptionEntry entries[] = { { "terminate", 0, 0, G_OPTION_ARG_NONE, &terminate, "Terminate the daemon", NULL }, + { "headless", 0, 0, G_OPTION_ARG_NONE, &headless, + "Control headless daemon", NULL }, +#if defined(HAVE_RDP) && defined(HAVE_LIBSYSTEMD) + { "system", 0, 0, G_OPTION_ARG_NONE, &system, + "Control system daemon", NULL }, + { "handover", 0, 0, G_OPTION_ARG_NONE, &handover, + "Control handover daemon", NULL }, +#endif /* HAVE_RDP && HAVE_LIBSYSTEMD */ { NULL } }; GError *error = NULL; GOptionContext *context; + const char *app_id; context = g_option_context_new ("- control gnome-remote-desktop"); g_option_context_add_main_entries (context, entries, NULL); @@ -56,7 +68,16 @@ main (int argc, char **argv) return 1; } - app = g_application_new (GRD_DAEMON_USER_APPLICATION_ID, 0); + if (headless) + app_id = GRD_DAEMON_HEADLESS_APPLICATION_ID; + else if (system) + app_id = GRD_DAEMON_SYSTEM_APPLICATION_ID; + else if (handover) + app_id = GRD_DAEMON_HANDOVER_APPLICATION_ID; + else + app_id = GRD_DAEMON_USER_APPLICATION_ID; + + app = g_application_new (app_id, G_APPLICATION_DEFAULT_FLAGS); if (!g_application_register (app, NULL, NULL)) { g_warning ("Failed to register with application\n"); diff --git a/src/grd-daemon-system.c b/src/grd-daemon-system.c index 1e25a08b38ce348aade2a3b33bb897c4334cd844..e230ee857c1a517bfc093bcf28052237d2396c3f 100644 --- a/src/grd-daemon-system.c +++ b/src/grd-daemon-system.c @@ -35,6 +35,7 @@ #include "grd-rdp-server.h" #include "grd-session-rdp.h" #include "grd-settings.h" +#include "grd-utils.h" #define MAX_HANDOVER_WAIT_TIME_MS (30 * 1000) @@ -148,6 +149,7 @@ on_handle_take_client (GrdDBusRemoteDesktopRdpHandover *interface, fd_list, fd_variant); + grd_close_connection_and_notify (remote_client->socket_connection); g_clear_object (&remote_client->socket_connection); g_clear_handle_id (&remote_client->abort_handover_source_id, g_source_remove); @@ -524,6 +526,8 @@ static void grd_remote_client_free (GrdRemoteClient *remote_client) { g_clear_pointer (&remote_client->id, g_free); + if (remote_client->socket_connection) + grd_close_connection_and_notify (remote_client->socket_connection); g_clear_object (&remote_client->socket_connection); unregister_handover_iface (remote_client, remote_client->handover_src); unregister_handover_iface (remote_client, remote_client->handover_dst); diff --git a/src/grd-daemon.c b/src/grd-daemon.c index c082c6b5ef00010b2609f17a123237393feeeefc..7e1a0a34a6a67c98f10578dcc9e91cd923ce7be9 100644 --- a/src/grd-daemon.c +++ b/src/grd-daemon.c @@ -49,6 +49,8 @@ #define RDP_SERVER_RESTART_DELAY_MS 3000 +#define DEFAULT_MAX_PARALLEL_CONNECTIONS 10 + enum { PROP_0, @@ -92,6 +94,9 @@ typedef struct _GrdDaemonPrivate G_DEFINE_TYPE_WITH_PRIVATE (GrdDaemon, grd_daemon, G_TYPE_APPLICATION) +#define QUOTE1(a) #a +#define QUOTE(a) QUOTE1(a) + #ifdef HAVE_RDP static void maybe_start_rdp_server (GrdDaemon *daemon); #endif @@ -969,6 +974,7 @@ main (int argc, char **argv) gboolean handover = FALSE; int rdp_port = -1; int vnc_port = -1; + int max_parallel_connections = DEFAULT_MAX_PARALLEL_CONNECTIONS; GOptionEntry entries[] = { { "version", 0, 0, G_OPTION_ARG_NONE, &print_version, @@ -985,11 +991,15 @@ main (int argc, char **argv) "RDP port", 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) option_context = NULL; g_autoptr (GrdDaemon) daemon = NULL; - GError *error = NULL; + g_autoptr (GError) error = NULL; GrdRuntimeMode runtime_mode; g_set_application_name (_("GNOME Remote Desktop")); @@ -999,7 +1009,6 @@ main (int argc, char **argv) if (!g_option_context_parse (option_context, &argc, &argv, &error)) { g_printerr ("Invalid option: %s\n", error->message); - g_error_free (error); return EXIT_FAILURE; } @@ -1011,7 +1020,18 @@ main (int argc, char **argv) if (count_trues (3, headless, system, handover) > 1) { - g_printerr ("Invalid option: More than one runtime mode specified"); + g_printerr ("Invalid option: More than one runtime mode specified\n"); + return EXIT_FAILURE; + } + + if (max_parallel_connections == 0) + { + 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_FAILURE; } @@ -1048,7 +1068,6 @@ main (int argc, char **argv) if (!daemon) { g_printerr ("Failed to initialize: %s\n", error->message); - g_error_free (error); return EXIT_FAILURE; } @@ -1062,5 +1081,8 @@ main (int argc, char **argv) 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 (G_APPLICATION (daemon), argc, argv); } diff --git a/src/grd-rdp-sam.c b/src/grd-rdp-sam.c index 5898825a04fc0fb651faaf19f1ad0988a2f38218..daa3ebb0205f636cfa0aa70e5a77ab88635cc48c 100644 --- a/src/grd-rdp-sam.c +++ b/src/grd-rdp-sam.c @@ -72,11 +72,12 @@ grd_rdp_sam_create_sam_file (const char *username, { const char *grd_path = "/gnome-remote-desktop"; const char *template = "/rdp-sam-XXXXXX"; + int duped_fd; GrdRdpSAMFile *rdp_sam_file; g_autofree char *file_dir = NULL; g_autofree char *filename = NULL; g_autofree char *sam_string = NULL; - int fd; + g_autofd int fd = -1; FILE *sam_file; file_dir = g_strdup_printf ("%s%s", g_get_user_runtime_dir (), grd_path); @@ -98,20 +99,29 @@ grd_rdp_sam_create_sam_file (const char *username, return NULL; } - rdp_sam_file = g_new0 (GrdRdpSAMFile, 1); - rdp_sam_file->fd = fd; - rdp_sam_file->filename = g_steal_pointer (&filename); - - sam_string = create_sam_string (username, password); - - sam_file = fdopen (rdp_sam_file->fd, "w+"); + sam_file = fdopen (fd, "w+"); if (!sam_file) { g_warning ("[RDP] Failed to open SAM database: %s", g_strerror (errno)); - grd_rdp_sam_free_sam_file (rdp_sam_file); return NULL; } + duped_fd = dup (fd); + if (duped_fd < 0) + { + fclose (sam_file); + g_warning ("[RDP] Failed to dup fd: %s", g_strerror (errno)); + return NULL; + } + + rdp_sam_file = g_new0 (GrdRdpSAMFile, 1); + rdp_sam_file->fd = duped_fd; + rdp_sam_file->filename = g_steal_pointer (&filename); + + g_steal_fd (&fd); + + sam_string = create_sam_string (username, password); + fputs (sam_string, sam_file); fclose (sam_file); diff --git a/src/grd-rdp-server.c b/src/grd-rdp-server.c index a2ce8ff5817aabee11c463ed246f413ad32d9417..b6d523416aefc0bee6c55510233e3e610661cc6d 100644 --- a/src/grd-rdp-server.c +++ b/src/grd-rdp-server.c @@ -32,10 +32,12 @@ #include "grd-hwaccel-vulkan.h" #include "grd-rdp-routing-token.h" #include "grd-session-rdp.h" +#include "grd-throttler.h" #include "grd-utils.h" #define RDP_SERVER_N_BINDING_ATTEMPTS 10 #define RDP_SERVER_BINDING_ATTEMPT_INTERVAL_MS 500 +#define RDP_SERVER_SOCKET_BACKLOG_COUNT 5 enum { @@ -59,6 +61,8 @@ struct _GrdRdpServer { GSocketService parent; + GrdThrottler *throttler; + GList *sessions; GList *stopped_sessions; @@ -215,18 +219,17 @@ on_routing_token_peeked (GObject *source_object, } } -static gboolean -on_incoming_as_system_headless (GSocketService *service, - GSocketConnection *connection) +static void +allow_connection_peek_cb (GrdThrottler *throttler, + GSocketConnection *connection, + gpointer user_data) { - GrdRdpServer *rdp_server = GRD_RDP_SERVER (service); + GrdRdpServer *rdp_server = GRD_RDP_SERVER (user_data); grd_routing_token_peek_async (rdp_server, connection, rdp_server->cancellable, on_routing_token_peeked); - - return TRUE; } static gboolean @@ -234,12 +237,22 @@ on_incoming (GSocketService *service, GSocketConnection *connection) { GrdRdpServer *rdp_server = GRD_RDP_SERVER (service); + + grd_throttler_handle_connection (rdp_server->throttler, + connection); + return TRUE; +} + +static void +accept_connection (GrdRdpServer *rdp_server, + GSocketConnection *connection) +{ GrdSessionRdp *session_rdp; - g_debug ("New incoming RDP connection"); + g_debug ("Creating new RDP session"); if (!(session_rdp = grd_session_rdp_new (rdp_server, connection))) - return TRUE; + return; rdp_server->sessions = g_list_append (rdp_server->sessions, session_rdp); @@ -250,15 +263,35 @@ on_incoming (GSocketService *service, g_signal_connect (session_rdp, "post-connected", G_CALLBACK (on_session_post_connect), rdp_server); +} - return TRUE; +static void +allow_connection_accept_cb (GrdThrottler *throttler, + GSocketConnection *connection, + gpointer user_data) +{ + GrdRdpServer *rdp_server = GRD_RDP_SERVER (user_data); + + accept_connection (rdp_server, connection); } void grd_rdp_server_notify_incoming (GSocketService *service, GSocketConnection *connection) { - on_incoming (service, connection); + GrdRdpServer *rdp_server = GRD_RDP_SERVER (service); + GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (rdp_server->context); + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_HANDOVER: + accept_connection (rdp_server, connection); + break; + case GRD_RUNTIME_MODE_SYSTEM: + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + g_assert_not_reached (); + } } static gboolean @@ -320,6 +353,9 @@ bind_socket (GrdRdpServer *rdp_server, uint16_t selected_rdp_port = 0; gboolean negotiate_port; + g_socket_listener_set_backlog (G_SOCKET_LISTENER (rdp_server), + RDP_SERVER_SOCKET_BACKLOG_COUNT); + g_object_get (G_OBJECT (settings), "rdp-port", &rdp_port, "rdp-negotiate-port", &negotiate_port, @@ -382,16 +418,13 @@ grd_rdp_server_start (GrdRdpServer *rdp_server, switch (runtime_mode) { - case GRD_RUNTIME_MODE_SCREEN_SHARE: - case GRD_RUNTIME_MODE_HEADLESS: - g_signal_connect (rdp_server, "incoming", G_CALLBACK (on_incoming), NULL); - break; case GRD_RUNTIME_MODE_SYSTEM: - g_signal_connect (rdp_server, "incoming", - G_CALLBACK (on_incoming_as_system_headless), NULL); - g_assert (!rdp_server->cancellable); rdp_server->cancellable = g_cancellable_new (); + G_GNUC_FALLTHROUGH; + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + g_signal_connect (rdp_server, "incoming", G_CALLBACK (on_incoming), NULL); break; case GRD_RUNTIME_MODE_HANDOVER: break; @@ -424,6 +457,8 @@ grd_rdp_server_stop (GrdRdpServer *rdp_server) g_clear_handle_id (&rdp_server->cleanup_sessions_idle_id, g_source_remove); grd_rdp_server_cleanup_stopped_sessions (rdp_server); + g_clear_object (&rdp_server->throttler); + if (rdp_server->cancellable) { g_cancellable_cancel (rdp_server->cancellable); @@ -483,6 +518,7 @@ grd_rdp_server_dispose (GObject *object) g_assert (!rdp_server->binding_timeout_source_id); g_assert (!rdp_server->cleanup_sessions_idle_id); g_assert (!rdp_server->stopped_sessions); + g_assert (!rdp_server->throttler); g_assert (!rdp_server->hwaccel_nvidia); g_assert (!rdp_server->hwaccel_vulkan); @@ -493,12 +529,32 @@ grd_rdp_server_dispose (GObject *object) static void grd_rdp_server_constructed (GObject *object) { - G_OBJECT_CLASS (grd_rdp_server_parent_class)->constructed (object); -} + GrdRdpServer *rdp_server = GRD_RDP_SERVER (object); + GrdRuntimeMode runtime_mode = + grd_context_get_runtime_mode (rdp_server->context); + GrdThrottlerAllowCallback allow_callback = NULL; + + switch (runtime_mode) + { + case GRD_RUNTIME_MODE_SCREEN_SHARE: + case GRD_RUNTIME_MODE_HEADLESS: + allow_callback = allow_connection_accept_cb; + break; + case GRD_RUNTIME_MODE_SYSTEM: + allow_callback = allow_connection_peek_cb; + break; + case GRD_RUNTIME_MODE_HANDOVER: + break; + } + + if (allow_callback) + { + rdp_server->throttler = + grd_throttler_new (grd_throttler_limits_new (rdp_server->context), + allow_callback, + rdp_server); + } -static void -grd_rdp_server_init (GrdRdpServer *rdp_server) -{ rdp_server->pending_binding_attempts = RDP_SERVER_N_BINDING_ATTEMPTS; winpr_InitializeSSL (WINPR_SSL_INIT_DEFAULT); @@ -508,6 +564,13 @@ grd_rdp_server_init (GrdRdpServer *rdp_server) * Run the primitives benchmark here to save time, when initializing a session */ primitives_get (); + + G_OBJECT_CLASS (grd_rdp_server_parent_class)->constructed (object); +} + +static void +grd_rdp_server_init (GrdRdpServer *rdp_server) +{ } static void diff --git a/src/grd-session-rdp.c b/src/grd-session-rdp.c index 1d8784ec73b4b2b762af4cfe08a95cb526131033..7305827a13f1d3a416e92adc63f0b59de6ea1969 100644 --- a/src/grd-session-rdp.c +++ b/src/grd-session-rdp.c @@ -48,6 +48,7 @@ #include "grd-rdp-server.h" #include "grd-rdp-session-metrics.h" #include "grd-settings.h" +#include "grd-utils.h" #define MAX_MONITOR_COUNT_HEADLESS 16 #define MAX_MONITOR_COUNT_SCREEN_SHARE 1 @@ -1675,6 +1676,7 @@ grd_session_rdp_stop (GrdSession *session) g_clear_object (&session_rdp->renderer); peer->Close (peer); + grd_close_connection_and_notify (session_rdp->connection); g_clear_object (&session_rdp->connection); g_clear_object (&rdp_peer_context->network_autodetection); @@ -1858,6 +1860,9 @@ grd_session_rdp_dispose (GObject *object) g_clear_object (&session_rdp->layout_manager); clear_rdp_peer (session_rdp); + + if (session_rdp->connection) + grd_close_connection_and_notify (session_rdp->connection); g_clear_object (&session_rdp->connection); g_clear_object (&session_rdp->renderer); diff --git a/src/grd-session-vnc.c b/src/grd-session-vnc.c index f7e14080ec4fe7c422c9181c8d1910c9d044eeaf..7cd41e9ff32e1c541a8c12e41e0bed16fc4bd359 100644 --- a/src/grd-session-vnc.c +++ b/src/grd-session-vnc.c @@ -34,6 +34,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" @@ -827,6 +828,7 @@ grd_session_vnc_stop (GrdSession *session) grd_session_vnc_detach_source (session_vnc); + grd_close_connection_and_notify (session_vnc->connection); g_clear_object (&session_vnc->connection); g_clear_object (&session_vnc->clipboard_vnc); g_clear_pointer (&session_vnc->rfb_screen->frameBuffer, g_free); diff --git a/src/grd-settings.c b/src/grd-settings.c index 8393ace54d3a88a5f98747ee8eece1c2fef23d70..afe09eebf60e94712128c59f3f6c1c290e227f07 100644 --- a/src/grd-settings.c +++ b/src/grd-settings.c @@ -85,6 +85,8 @@ typedef struct _GrdSettingsPrivate GrdVncScreenShareMode screen_share_mode; GrdVncAuthMethod auth_method; } vnc; + + int max_parallel_connections; } GrdSettingsPrivate; G_DEFINE_TYPE_WITH_PRIVATE (GrdSettings, grd_settings, G_TYPE_OBJECT) @@ -101,6 +103,23 @@ grd_settings_get_runtime_mode (GrdSettings *settings) return priv->runtime_mode; } +void +grd_settings_override_max_parallel_connections (GrdSettings *settings, + int max_parallel_connections) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + priv->max_parallel_connections = max_parallel_connections; +} + +int +grd_settings_get_max_parallel_connections (GrdSettings *settings) +{ + GrdSettingsPrivate *priv = grd_settings_get_instance_private (settings); + + return priv->max_parallel_connections; +} + void grd_settings_override_rdp_port (GrdSettings *settings, int port) diff --git a/src/grd-settings.h b/src/grd-settings.h index 6660b9620baf3fd741797191dd9b5a72b4a327e6..94dfd3b24b39d3d397d0c4c8625402216e5d52a4 100644 --- a/src/grd-settings.h +++ b/src/grd-settings.h @@ -37,6 +37,11 @@ struct _GrdSettingsClass GrdRuntimeMode grd_settings_get_runtime_mode (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_rdp_port (GrdSettings *settings, int port); diff --git a/src/grd-throttler.c b/src/grd-throttler.c new file mode 100644 index 0000000000000000000000000000000000000000..8f19783fa16b74d94e00a05da3b26d7774c51c6d --- /dev/null +++ b/src/grd-throttler.c @@ -0,0 +1,515 @@ +/* + * 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-context.h" +#include "grd-settings.h" +#include "grd-utils.h" + +#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)) + +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; + + GrdThrottlerLimits *limits; + + 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; + + /* Prune all timestamps older than 1 second, so we can determine how many + * connections that happened the last second by looking at how many timestamps + * we have. + */ + 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_assert (peer->active_connections > 0); + g_assert (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) +{ + GrdThrottlerLimits *limits = throttler->limits; + + if (peer->active_connections >= limits->max_connections_per_peer) + return TRUE; + + if (throttler->active_connections >= limits->max_global_connections) + return TRUE; + + return FALSE; +} + +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 >= limits->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_head (queue); + + if (!connection) + { + maybe_dispose_peer (throttler, peer, &iter); + continue; + } + + if (is_new_connection_allowed (throttler, peer)) + { + g_queue_pop_head (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) +{ + GrdThrottlerLimits *limits = throttler->limits; + 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 / limits->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) +{ + GrdThrottlerLimits *limits = throttler->limits; + 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) > limits->max_pending_connections) + { + grd_throttler_deny_connection (throttler, peer->name, connection); + return; + } + + g_debug ("Delaying connection from %s", peer->name); + + g_queue_push_tail (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); +} + +void +grd_throttler_limits_set_max_global_connections (GrdThrottlerLimits *limits, + int limit) +{ + limits->max_global_connections = limit; +} + +GrdThrottlerLimits * +grd_throttler_limits_new (GrdContext *context) +{ + GrdSettings *settings = grd_context_get_settings (context); + GrdThrottlerLimits *limits; + + limits = g_new0 (GrdThrottlerLimits, 1); + 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; + + return limits; +} + +GrdThrottler * +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; +} + +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_clear_pointer (&throttler->limits, g_free); + + 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 0000000000000000000000000000000000000000..a955420298247dd6c028e3fe6e9a6e6ffc396b7c --- /dev/null +++ b/src/grd-throttler.h @@ -0,0 +1,53 @@ +/* + * 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 + +#include "grd-types.h" + +typedef struct _GrdThrottlerLimits GrdThrottlerLimits; + +#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); + +void +grd_throttler_limits_set_max_global_connections (GrdThrottlerLimits *limits, + int limit); + +GrdThrottlerLimits * +grd_throttler_limits_new (GrdContext *context); + +GrdThrottler * +grd_throttler_new (GrdThrottlerLimits *limits, + GrdThrottlerAllowCallback allow_callback, + gpointer user_data); + +#endif /* GRD_THROTTLER_H */ diff --git a/src/grd-utils.c b/src/grd-utils.c index 32739a9388cde27d7f4e44a91f25fae5d086bb93..bf20882804b55b6ea1f4bf3d492c5c655a3a64f7 100644 --- a/src/grd-utils.c +++ b/src/grd-utils.c @@ -478,3 +478,10 @@ grd_systemd_unit_get_active_state (GDBusProxy *unit_proxy, return TRUE; } + +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 30a3b9ecb471925a386483314d247606f6a69bb1..4703db8f4cf93ce2d688fea91b3ca86c6e4a549e 100644 --- a/src/grd-utils.h +++ b/src/grd-utils.h @@ -99,3 +99,23 @@ gboolean grd_systemd_get_unit (GBusType bus_type, gboolean grd_systemd_unit_get_active_state (GDBusProxy *unit_proxy, GrdSystemdUnitActiveState *active_state, GError **error); + +void grd_close_connection_and_notify (GSocketConnection *connection); + +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); +} diff --git a/src/grd-vnc-server.c b/src/grd-vnc-server.c index 832206556d2f170f2e4b04554aec5b69e661d1e7..49f1605630785c89ed3d21a494c5145b123f57de 100644 --- a/src/grd-vnc-server.c +++ b/src/grd-vnc-server.c @@ -30,6 +30,7 @@ #include "grd-context.h" #include "grd-debug.h" #include "grd-session-vnc.h" +#include "grd-throttler.h" #include "grd-utils.h" enum @@ -43,6 +44,8 @@ struct _GrdVncServer { GSocketService parent; + GrdThrottler *throttler; + GList *sessions; GList *stopped_sessions; @@ -53,6 +56,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) { @@ -103,22 +111,15 @@ 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_message ("Refusing new VNC connection: already an active session"); - return TRUE; - } + 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); @@ -126,7 +127,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; } @@ -187,6 +197,8 @@ grd_vnc_server_stop (GrdVncServer *vnc_server) grd_vnc_server_cleanup_stopped_sessions (vnc_server); g_clear_handle_id (&vnc_server->cleanup_sessions_idle_id, g_source_remove); + + g_clear_object (&vnc_server->throttler); } static void @@ -235,6 +247,7 @@ grd_vnc_server_dispose (GObject *object) g_assert (!vnc_server->sessions); g_assert (!vnc_server->stopped_sessions); g_assert (!vnc_server->cleanup_sessions_idle_id); + g_assert (!vnc_server->throttler); G_OBJECT_CLASS (grd_vnc_server_parent_class)->dispose (object); } @@ -242,11 +255,22 @@ grd_vnc_server_dispose (GObject *object) static void grd_vnc_server_constructed (GObject *object) { + GrdVncServer *vnc_server = GRD_VNC_SERVER (object); + GrdThrottlerLimits *limits; + if (grd_get_debug_flags () & GRD_DEBUG_VNC) rfbLogEnable (1); else rfbLogEnable (0); + limits = grd_throttler_limits_new (vnc_server->context); + /* 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); } diff --git a/src/meson.build b/src/meson.build index a501977ada9d65873ebcd0ccd63c7d11c3e9293c..49e0509be46e206602e63d3e3b216139e61877d1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -74,6 +74,8 @@ daemon_sources = files([ 'grd-settings-user.h', 'grd-stream.c', 'grd-stream.h', + 'grd-throttler.c', + 'grd-throttler.h', 'grd-types.h', 'grd-utils.c', 'grd-utils.h',