From b00c2e3a2b73cd24413ebe685e371b701748db71 Mon Sep 17 00:00:00 2001 From: Ben Prisby Date: Wed, 25 Feb 2026 10:37:34 -0500 Subject: [PATCH 1/5] config-file, hawkbit-client: add download speed limit option Signed-off-by: Ben Prisby --- config.conf.example | 3 +++ docs/reference.rst | 5 +++++ include/config-file.h | 1 + src/config-file.c | 2 ++ src/hawkbit-client.c | 2 ++ test/test_download.py | 28 ++++++++++++++++++++++++++++ 6 files changed, 41 insertions(+) diff --git a/config.conf.example b/config.conf.example index 8a855787..b76d933a 100644 --- a/config.conf.example +++ b/config.conf.example @@ -46,6 +46,9 @@ low_speed_time = 0 # average transfer speed to be below during "low_speed_time" seconds low_speed_rate = 0 +# average download speed cap in bytes per second for rate limiting +download_speed_limit = 0 + # reboot after a successful update post_update_reboot = false diff --git a/docs/reference.rst b/docs/reference.rst index 8a88400c..85e6e298 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -133,6 +133,11 @@ Optional options: See https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_LIMIT.html. Has no effect when used with ``stream_bundle=true``. +``download_speed_limit=`` + Average download speed cap for rate-limited environments. + Defaults to ``0`` (unlimited). + See https://curl.se/libcurl/c/CURLOPT_MAX_RECV_SPEED_LARGE.html. + ``resume_downloads=`` Whether to resume aborted downloads or not. Defaults to ``false``. diff --git a/include/config-file.h b/include/config-file.h index 49826b6c..53f1d939 100644 --- a/include/config-file.h +++ b/include/config-file.h @@ -31,6 +31,7 @@ typedef struct Config_ { int retry_wait; /**< wait between retries */ int low_speed_time; /**< time to be below the speed to trigger low speed abort */ int low_speed_rate; /**< low speed limit to abort transfer */ + int download_speed_limit; /**< average download speed cap for rate limiting */ GLogLevelFlags log_level; /**< log level */ GHashTable* device; /**< Additional attributes sent to hawkBit */ gboolean send_download_authentication; /**< Send security header in download requests */ diff --git a/src/config-file.c b/src/config-file.c index b65a44a0..3107e9bb 100644 --- a/src/config-file.c +++ b/src/config-file.c @@ -328,6 +328,8 @@ Config* load_config_file(const gchar *config_file, GError **error) return NULL; if (!get_key_int(ini_file, "client", "low_speed_time", &config->low_speed_time, 60, error)) return NULL; + if (!get_key_int(ini_file, "client", "download_speed_limit", &config->download_speed_limit, 0, error)) + return NULL; if (!get_key_bool(ini_file, "client", "resume_downloads", &config->resume_downloads, FALSE, error)) return NULL; diff --git a/src/hawkbit-client.c b/src/hawkbit-client.c index a6d6aed9..13655bea 100644 --- a/src/hawkbit-client.c +++ b/src/hawkbit-client.c @@ -403,6 +403,8 @@ static gboolean get_binary(const gchar *download_url, const gchar *file, curl_of curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, hawkbit_config->low_speed_time); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, hawkbit_config->low_speed_rate); + curl_easy_setopt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, hawkbit_config->download_speed_limit); + curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, resume_from); if (hawkbit_config->send_download_authentication && diff --git a/test/test_download.py b/test/test_download.py index 38deb763..087db1c3 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -1,7 +1,9 @@ # SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix +import os import re +import time from helper import run @@ -193,3 +195,29 @@ def test_download_only_without_auth_header(hawkbit, adjust_config, assign_bundle assert status[0]['type'] == 'downloaded' # check last status message assert 'File checksum OK.' in status[0]['messages'] + +def test_download_speed_limit(hawkbit, assign_bundle, adjust_config, rauc_bundle): + """Test client-side download speed limiting.""" + bundle_size = os.path.getsize(rauc_bundle) + speed_limit = 100 * 1024 # 100KB/s + min_time = bundle_size / speed_limit * 0.8 # 20% tolerance since cap is across average speed + + config = adjust_config({'client': {'download_speed_limit': str(speed_limit)}}) + assign_bundle(params={'type': 'downloadonly'}) # Don't attempt installation that would fail + + start_time = time.time() + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r', timeout=120) + elapsed_time = time.time() - start_time + + assert 'Start downloading' in out + assert 'Download complete.' in out + assert 'File checksum OK.' in out + assert err == '' + assert exitcode == 0 + + status = hawkbit.get_action_status() + assert status[0]['type'] == 'downloaded' + + assert elapsed_time >= min_time, \ + f'Download too fast: {bundle_size} bytes in {elapsed_time:.2f}s ' \ + f'(average speed: {bundle_size/elapsed_time:.0f} B/s, limit: {speed_limit} B/s)' From 37531c8f6da8eb0d42078e7c4f9dadadda5d5a5e Mon Sep 17 00:00:00 2001 From: Ben Prisby Date: Thu, 26 Feb 2026 15:58:59 -0500 Subject: [PATCH 2/5] hawkbit-client: add note about download_speed_limit having no effect when stream_bundle is true Signed-off-by: Ben Prisby --- config.conf.example | 1 + docs/reference.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/config.conf.example b/config.conf.example index b76d933a..97c99f59 100644 --- a/config.conf.example +++ b/config.conf.example @@ -47,6 +47,7 @@ low_speed_time = 0 low_speed_rate = 0 # average download speed cap in bytes per second for rate limiting +# has no effect when stream_bundle = true download_speed_limit = 0 # reboot after a successful update diff --git a/docs/reference.rst b/docs/reference.rst index 85e6e298..9f25e763 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -137,6 +137,7 @@ Optional options: Average download speed cap for rate-limited environments. Defaults to ``0`` (unlimited). See https://curl.se/libcurl/c/CURLOPT_MAX_RECV_SPEED_LARGE.html. + Has no effect when used with ``stream_bundle=true``. ``resume_downloads=`` Whether to resume aborted downloads or not. From 59a37d7dea4f82095cf7206d1ade7f74a567891b Mon Sep 17 00:00:00 2001 From: Ben Prisby Date: Wed, 29 Apr 2026 18:49:14 -0400 Subject: [PATCH 3/5] test: use monotonic clock for download test Signed-off-by: Ben Prisby --- test/test_download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_download.py b/test/test_download.py index 087db1c3..c11f58bc 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -205,9 +205,9 @@ def test_download_speed_limit(hawkbit, assign_bundle, adjust_config, rauc_bundle config = adjust_config({'client': {'download_speed_limit': str(speed_limit)}}) assign_bundle(params={'type': 'downloadonly'}) # Don't attempt installation that would fail - start_time = time.time() + start_time = time.monotonic() out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r', timeout=120) - elapsed_time = time.time() - start_time + elapsed_time = time.monotonic() - start_time assert 'Start downloading' in out assert 'Download complete.' in out From e3262935badd08ad00b9d99b4ad98f67fbeb0f35 Mon Sep 17 00:00:00 2001 From: Ben Prisby Date: Wed, 29 Apr 2026 18:56:55 -0400 Subject: [PATCH 4/5] hawkbit-client: cast download_speed_limit to curl_off_t Signed-off-by: Ben Prisby --- src/hawkbit-client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hawkbit-client.c b/src/hawkbit-client.c index 13655bea..e39997fa 100644 --- a/src/hawkbit-client.c +++ b/src/hawkbit-client.c @@ -403,7 +403,7 @@ static gboolean get_binary(const gchar *download_url, const gchar *file, curl_of curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, hawkbit_config->low_speed_time); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, hawkbit_config->low_speed_rate); - curl_easy_setopt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, hawkbit_config->download_speed_limit); + curl_easy_setopt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t) hawkbit_config->download_speed_limit); curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, resume_from); From e088137e99f686fddfec95753af66e3487b36df9 Mon Sep 17 00:00:00 2001 From: Ben Prisby Date: Wed, 29 Apr 2026 18:59:01 -0400 Subject: [PATCH 5/5] config-file: use gint64 for download_speed_limit Signed-off-by: Ben Prisby --- include/config-file.h | 2 +- src/config-file.c | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/include/config-file.h b/include/config-file.h index 53f1d939..fa15b7fc 100644 --- a/include/config-file.h +++ b/include/config-file.h @@ -31,7 +31,7 @@ typedef struct Config_ { int retry_wait; /**< wait between retries */ int low_speed_time; /**< time to be below the speed to trigger low speed abort */ int low_speed_rate; /**< low speed limit to abort transfer */ - int download_speed_limit; /**< average download speed cap for rate limiting */ + gint64 download_speed_limit; /**< average download speed cap for rate limiting */ GLogLevelFlags log_level; /**< log level */ GHashTable* device; /**< Additional attributes sent to hawkBit */ gboolean send_download_authentication; /**< Send security header in download requests */ diff --git a/src/config-file.c b/src/config-file.c index 3107e9bb..bfc5f5c7 100644 --- a/src/config-file.c +++ b/src/config-file.c @@ -152,6 +152,46 @@ static gboolean get_key_int(GKeyFile *key_file, const gchar *group, const gchar return TRUE; } +/** + * @brief Get 64-bit integer value from key_file for key in group, default_value must be specified, + * returned in case key not found in group. + * + * @param[in] key_file GKeyFile to look value up + * @param[in] group A group name + * @param[in] key A key + * @param[out] value Output 64-bit integer value + * @param[in] default_value Return this value in case no value found + * @param[out] error Error + * @return FALSE on error (error is set), TRUE otherwise. Note that TRUE is returned if key in + * group is not found, value is set to default_value in this case. + */ +static gboolean get_key_int64(GKeyFile *key_file, const gchar *group, const gchar *key, gint64 *value, + const gint64 default_value, GError **error) +{ + GError *ierror = NULL; + gint64 val; + + g_return_val_if_fail(key_file, FALSE); + g_return_val_if_fail(group, FALSE); + g_return_val_if_fail(key, FALSE); + g_return_val_if_fail(value, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + val = g_key_file_get_int64(key_file, group, key, &ierror); + + if (g_error_matches(ierror, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { + g_clear_error(&ierror); + *value = default_value; + return TRUE; + } else if (ierror) { + g_propagate_error(error, ierror); + return FALSE; + } + + *value = val; + return TRUE; +} + /** * @brief Get GHashTable containing keys/values from group in key_file. * @@ -328,7 +368,7 @@ Config* load_config_file(const gchar *config_file, GError **error) return NULL; if (!get_key_int(ini_file, "client", "low_speed_time", &config->low_speed_time, 60, error)) return NULL; - if (!get_key_int(ini_file, "client", "download_speed_limit", &config->download_speed_limit, 0, error)) + if (!get_key_int64(ini_file, "client", "download_speed_limit", &config->download_speed_limit, 0, error)) return NULL; if (!get_key_bool(ini_file, "client", "resume_downloads", &config->resume_downloads, FALSE, error))