[PATCH] gdhcp: Make DHCP client timeouts suspend-aware

Simon Holesch <[email protected]> Tue, 13 May 2025 19:01:32 +0200
Newsgroups dev.linux.lists.connman
Message-ID <[email protected]>
From: Simon Holesch <[email protected]>

When the system is suspended, ConnMan may keep its IP address longer
than the lease time. This occurs because GLib timeouts use
CLOCK_MONOTONIC, which pauses during suspend on Linux.

This patch switches to timeouts based on CLOCK_BOOTTIME, which continues
to run during suspend.

This is an updated version of the patch reverted in commit 9aede91e
("Revert 'gdhcp: Make DHCP client timeouts suspend aware'"). It now uses
CLOCK_BOOTTIME instead of CLOCK_BOOTTIME_ALARM, so the system will not
wake up to renew the lease, but will send the renew requests right after
resume.
---
 gdhcp/client.c | 183 +++++++++++++++++++++++++++++++++++--------------
 1 file changed, 132 insertions(+), 51 deletions(-)

diff --git a/gdhcp/client.c b/gdhcp/client.c
index db8e0596..976773ac 100644
--- a/gdhcp/client.c
+++ b/gdhcp/client.c
@@ -32,6 +32,7 @@
 #include <arpa/inet.h>
 #include <sys/time.h>
 #include <resolv.h>
+#include <sys/timerfd.h>
 
 #include <netpacket/packet.h>
 #include <netinet/if_ether.h>
@@ -41,6 +42,7 @@
 #include <linux/filter.h>
 
 #include <glib.h>
+#include <glib-unix.h>
 
 #include "../src/connman.h"
 #include "../src/shared/arp.h"
@@ -160,6 +162,12 @@ struct _GDHCPClient {
 	bool request_bcast;
 };
 
+struct timeout_info {
+	int fd;
+	GSourceFunc callback;
+	gpointer user_data;
+};
+
 static inline void debug(GDHCPClient *client, const char *format, ...)
 {
 	char str[256];
@@ -176,6 +184,101 @@ static inline void debug(GDHCPClient *client, const char *format, ...)
 	va_end(ap);
 }
 
+static gboolean on_timerfd_timeout(gint fd, GIOCondition condition,
+					gpointer data)
+{
+	struct timeout_info *tmo = data;
+
+	return tmo->callback(tmo->user_data);
+}
+
+static void cleanup_timeout_info(struct timeout_info *tmo)
+{
+	if (tmo->fd > 0)
+		close(tmo->fd);
+	g_free(tmo);
+}
+
+static void on_timerfd_destroy(gpointer data)
+{
+	struct timeout_info *tmo = data;
+
+	cleanup_timeout_info(tmo);
+}
+
+static guint timeout_start_timespec(GDHCPClient *dhcp_client, struct timespec *interval,
+					GSourceFunc function, gpointer data)
+{
+	struct itimerspec timeout = {
+		.it_value = *interval,
+		.it_interval = *interval,
+	};
+	struct itimerspec *old_value = NULL;
+	struct timeout_info *tmo;
+	unsigned int id;
+	int flags = 0;
+	int err;
+
+	tmo = g_malloc0(sizeof(*tmo));
+	tmo->callback = function;
+	tmo->user_data = data;
+
+	/*
+	 * CLOCK_BOOTTIME continues to run while the system is suspended. The GLib
+	 * timeout uses CLOCK_MONOTONIC, which is paused during suspend on Linux
+	 * and is therefore unfit for network timeouts.
+	 */
+	tmo->fd = timerfd_create(CLOCK_BOOTTIME, TFD_NONBLOCK | TFD_CLOEXEC);
+	if (tmo->fd == -1) {
+		debug(dhcp_client,
+			"Error creating timerfd: %s", strerror(errno));
+		cleanup_timeout_info(tmo);
+		return 0;
+	}
+
+	err = timerfd_settime(tmo->fd, flags, &timeout, old_value);
+	if (err) {
+		debug(dhcp_client,
+			"Error setting timerfd timeout: %s", strerror(errno));
+		cleanup_timeout_info(tmo);
+		return 0;
+	}
+
+	/*
+	 * The file descriptor returned by timerfd_create can be polled and becomes
+	 * readable on timeout. Add the fd to the GLib event loop.
+	 */
+	id = g_unix_fd_add_full(G_PRIORITY_HIGH, tmo->fd, G_IO_IN,
+				on_timerfd_timeout, tmo, on_timerfd_destroy);
+	if (id == 0) {
+		debug(dhcp_client, "Error adding timerfd to event loop");
+		cleanup_timeout_info(tmo);
+	}
+	return id;
+}
+
+static guint timeout_start_seconds(GDHCPClient *dhcp_client, guint32 interval,
+					GSourceFunc function, gpointer data)
+{
+	struct timespec timeout = {
+		.tv_sec = interval,
+		.tv_nsec = 0,
+	};
+
+	return timeout_start_timespec(dhcp_client, &timeout, function, data);
+}
+
+static guint timeout_start_ms(GDHCPClient *dhcp_client, guint32 interval,
+				GSourceFunc function, gpointer data)
+{
+	struct timespec timeout = {
+		.tv_sec = interval / 1000,
+		.tv_nsec = (long)(interval % 1000) * 1000000,
+	};
+
+	return timeout_start_timespec(dhcp_client, &timeout, function, data);
+}
+
 /* Initialize the packet with the proper defaults */
 static void init_packet(GDHCPClient *dhcp_client, gpointer pkt, char type)
 {
@@ -595,11 +698,8 @@ static gboolean send_probe_packet(gpointer dhcp_data)
 	} else
 		timeout = (ANNOUNCE_WAIT * 1000);
 
-	dhcp_client->timeout = g_timeout_add_full(G_PRIORITY_HIGH,
-						 timeout,
-						 ipv4ll_probe_timeout,
-						 dhcp_client,
-						 NULL);
+	dhcp_client->timeout = timeout_start_ms(dhcp_client, timeout,
+					 ipv4ll_probe_timeout,  dhcp_client);
 	return FALSE;
 }
 
@@ -623,19 +723,17 @@ static gboolean send_announce_packet(gpointer dhcp_data)
 
 	if (dhcp_client->state == IPV4LL_DEFEND) {
 		dhcp_client->timeout =
-			g_timeout_add_seconds_full(G_PRIORITY_HIGH,
+			timeout_start_seconds(dhcp_client,
 						DEFEND_INTERVAL,
 						ipv4ll_defend_timeout,
-						dhcp_client,
-						NULL);
+						dhcp_client);
 		return TRUE;
 	} else
 		dhcp_client->timeout =
-			g_timeout_add_seconds_full(G_PRIORITY_HIGH,
+			timeout_start_seconds(dhcp_client,
 						ANNOUNCE_INTERVAL,
 						ipv4ll_announce_timeout,
-						dhcp_client,
-						NULL);
+						dhcp_client);
 	return TRUE;
 }
 
@@ -1390,11 +1488,8 @@ static void ipv4ll_start(GDHCPClient *dhcp_client)
 	timeout = __connman_util_random_delay_ms(PROBE_WAIT);
 
 	dhcp_client->retry_times++;
-	dhcp_client->timeout = g_timeout_add_full(G_PRIORITY_HIGH,
-						timeout,
-						send_probe_packet,
-						dhcp_client,
-						NULL);
+	dhcp_client->timeout = timeout_start_ms(dhcp_client, timeout,
+					send_probe_packet, dhcp_client);
 }
 
 static void ipv4ll_stop(GDHCPClient *dhcp_client)
@@ -1482,11 +1577,8 @@ static int ipv4ll_recv_arp_packet(GDHCPClient *dhcp_client)
 		timeout_ms = RATE_LIMIT_INTERVAL * 1000;
 	dhcp_client->retry_times++;
 	dhcp_client->timeout =
-		g_timeout_add_full(G_PRIORITY_HIGH,
-				timeout_ms,
-				send_probe_packet,
-				dhcp_client,
-				NULL);
+		timeout_start_ms(dhcp_client, timeout_ms, send_probe_packet,
+				dhcp_client);
 
 	return 0;
 }
@@ -1629,11 +1721,10 @@ static void start_request(GDHCPClient *dhcp_client)
 
 	send_request(dhcp_client);
 
-	dhcp_client->timeout = g_timeout_add_seconds_full(G_PRIORITY_HIGH,
+	dhcp_client->timeout = timeout_start_seconds(dhcp_client,
 							REQUEST_TIMEOUT,
 							request_timeout,
-							dhcp_client,
-							NULL);
+							dhcp_client);
 }
 
 static uint32_t get_lease(struct dhcp_packet *packet, uint16_t packet_len)
@@ -1704,11 +1795,10 @@ static gboolean continue_rebound(gpointer user_data)
 	if (dhcp_client->T2 > 60) {
 		__connman_util_get_random(&rand);
 		dhcp_client->t2_timeout =
-			g_timeout_add_full(G_PRIORITY_HIGH,
+			timeout_start_ms(dhcp_client,
 					dhcp_client->T2 * 1000 + (rand % 2000) - 1000,
 					continue_rebound,
-					dhcp_client,
-					NULL);
+					dhcp_client);
 	}
 
 	return FALSE;
@@ -1751,11 +1841,10 @@ static gboolean continue_renew (gpointer user_data)
 
 	if (dhcp_client->T1 > 60) {
 		__connman_util_get_random(&rand);
-		dhcp_client->t1_timeout = g_timeout_add_full(G_PRIORITY_HIGH,
+		dhcp_client->t1_timeout = timeout_start_ms(dhcp_client,
 				dhcp_client->T1 * 1000 + (rand % 2000) - 1000,
 				continue_renew,
-				dhcp_client,
-				NULL);
+				dhcp_client);
 	}
 
 	return FALSE;
@@ -1793,20 +1882,17 @@ static void start_bound(GDHCPClient *dhcp_client)
 	dhcp_client->T2 = dhcp_client->lease_seconds * 0.875;
 	dhcp_client->expire = dhcp_client->lease_seconds;
 
-	dhcp_client->t1_timeout = g_timeout_add_seconds_full(G_PRIORITY_HIGH,
+	dhcp_client->t1_timeout = timeout_start_seconds(dhcp_client,
 					dhcp_client->T1,
-					start_renew, dhcp_client,
-							NULL);
+					start_renew, dhcp_client);
 
-	dhcp_client->t2_timeout = g_timeout_add_seconds_full(G_PRIORITY_HIGH,
+	dhcp_client->t2_timeout = timeout_start_seconds(dhcp_client,
 					dhcp_client->T2,
-					start_rebound, dhcp_client,
-							NULL);
+					start_rebound, dhcp_client);
 
-	dhcp_client->lease_timeout= g_timeout_add_seconds_full(G_PRIORITY_HIGH,
+	dhcp_client->lease_timeout= timeout_start_seconds(dhcp_client,
 					dhcp_client->expire,
-					start_expire, dhcp_client,
-							NULL);
+					start_expire, dhcp_client);
 }
 
 static gboolean restart_dhcp_timeout(gpointer user_data)
@@ -2471,11 +2557,9 @@ static gboolean listener_event(GIOChannel *channel, GIOCondition condition,
 
 			remove_timeouts(dhcp_client);
 
-			dhcp_client->timeout = g_timeout_add_seconds_full(
-							G_PRIORITY_HIGH, 3,
-							restart_dhcp_timeout,
-							dhcp_client,
-							NULL);
+			dhcp_client->timeout =
+				timeout_start_seconds(dhcp_client, 3,
+						restart_dhcp_timeout, dhcp_client);
 		}
 
 		break;
@@ -2894,21 +2978,18 @@ int g_dhcp_client_start(GDHCPClient *dhcp_client, const char *last_address)
 		dhcp_client->state = REBOOTING;
 		send_request(dhcp_client);
 
-		dhcp_client->timeout = g_timeout_add_seconds_full(
-								G_PRIORITY_HIGH,
+		dhcp_client->timeout = timeout_start_seconds(dhcp_client,
 								REQUEST_TIMEOUT,
 								reboot_timeout,
-								dhcp_client,
-								NULL);
+								dhcp_client);
 		return 0;
 	}
 	send_discover(dhcp_client, addr);
 
-	dhcp_client->timeout = g_timeout_add_seconds_full(G_PRIORITY_HIGH,
+	dhcp_client->timeout = timeout_start_seconds(dhcp_client,
 							DISCOVER_TIMEOUT,
 							discover_timeout,
-							dhcp_client,
-							NULL);
+							dhcp_client);
 	return 0;
 }
 
-- 
2.49.0