From 833914381c6d4f185b8cc14042760af4d0d9266d Mon Sep 17 00:00:00 2001 From: cryzed Date: Fri, 31 Jan 2020 22:48:51 +0100 Subject: [PATCH] Add support for global and per-process download and upload minimum --- README.md | 18 +++++++++++- example.yaml | 16 ++++++++++ pyproject.toml | 2 +- traffictoll/cli.py | 73 ++++++++++++++++++++++++++++++++++++++++------ traffictoll/tc.py | 16 ++++++---- 5 files changed, 109 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e13e186..f7c16e7 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,15 @@ is best explained by example: download: 5mbps upload: 1mbps +# Guaranteed download and upload rates for all global traffic that is not shaped as part +# of a matched process by TrafficToll. The idea here is to leave enough "guaranteed" +# bandwidth to all applications not defined in "processes", so that they are not starved +# to a bandwidth, by processes with higher priority, that would cause the other IP to +# drop the connection. These are the default values, if omitted. Keep in mind that this +# doesn't reserve the bandwidth -- if this traffic is not made use of, it's available +# to processes with higher priority. +download-minimum: 100kbps +upload-minimum: 10kbps # A list of processes you want to match and their respective settings processes: @@ -129,7 +138,7 @@ processes: # The process that actually creates network traffic for electron-based applications # is not uniquely identifiable. Instead we match a uniquely identifiable parent # process, in this case "riot-desktop", and set recursive to True. This instructs - # TrafficToll to traffic shape the connections of the matched process and all it's + # TrafficToll to traffic shape the connections of the matched process and all its # descendants recursive: True match: @@ -141,6 +150,13 @@ processes: # explicitly specifies them will automatically be the lowest: in this case 2, the # same as "Discord", our lowest priority process. + # Since the download and upload priority of this process is the lowest, make sure + # that its connections don't starve when processes with higher priority use up all + # the available bandwidth. These are the default values for each process and will be + # applied if omitted. + download-minimum: 10kbps + upload-minimum: 1kbps + # JDownloader 2 obviously has its own bandwidth limiting, this is just here as an # example to show that matching on something else than the executable's name and # path is possible diff --git a/example.yaml b/example.yaml index a6f20b0..1e7d6be 100644 --- a/example.yaml +++ b/example.yaml @@ -18,6 +18,15 @@ download: 5mbps upload: 1mbps +# Guaranteed download and upload rates for all global traffic that is not shaped as part +# of a matched process by TrafficToll. The idea here is to leave enough "guaranteed" +# bandwidth to all applications not defined in "processes", so that they are not starved +# to a bandwidth, by processes with higher priority, that would cause the other IP to +# drop the connection. These are the default values, if omitted. Keep in mind that this +# doesn't reserve the bandwidth -- if this traffic is not made use of, it's available +# to processes with higher priority. +download-minimum: 100kbps +upload-minimum: 10kbps # A list of processes you want to match and their respective settings processes: @@ -113,6 +122,13 @@ processes: # explicitly specifies them will automatically be the lowest: in this case 2, the # same as "Discord", our lowest priority process. + # Since the download and upload priority of this process is the lowest, make sure + # that its connections don't starve when processes with higher priority use up all + # the available bandwidth. These are the default values for each process and will be + # applied if omitted. + download-minimum: 10kbps + upload-minimum: 1kbps + # JDownloader 2 obviously has its own bandwidth limiting, this is just here as an # example to show that matching on something else than the executable's name and # path is possible diff --git a/pyproject.toml b/pyproject.toml index 5ac7532..18b9bf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "TrafficToll" -version = "1.2.0" +version = "1.3.0" description = "NetLimiter-like bandwidth limiting and QoS for Linux" authors = ["cryzed "] diff --git a/traffictoll/cli.py b/traffictoll/cli.py index dca4a49..875f296 100644 --- a/traffictoll/cli.py +++ b/traffictoll/cli.py @@ -20,6 +20,10 @@ ) CONFIG_ENCODING = "UTF-8" +GLOBAL_MINIMUM_DOWNLOAD_RATE = "100kbps" +GLOBAL_MINIMUM_UPLOAD_RATE = "10kbps" +MINIMUM_DOWNLOAD_RATE = "10kbps" +MINIMUM_UPLOAD_RATE = "1kbps" class _TrafficType(enum.Enum): @@ -85,31 +89,57 @@ def main(arguments: argparse.Namespace) -> None: lowest_priority += 1 + config_global_download_minimum_rate = config.get("download-minimum") + global_download_minimum_rate = ( + GLOBAL_MINIMUM_DOWNLOAD_RATE + if config_global_download_minimum_rate is None + else config_global_download_minimum_rate + ) + config_global_upload_minimum_rate = config.get("upload-minimum") + global_upload_minimum_rate = ( + GLOBAL_MINIMUM_UPLOAD_RATE + if config_global_upload_minimum_rate is None + else config_global_upload_minimum_rate + ) + if config_global_download_rate is not None: logger.info( - "Setting up global class with max download rate: {} and priority: {}", + "Setting up global class with max download rate: {} (minimum: {}) and " + "priority: {}", global_download_rate, + global_download_minimum_rate, lowest_priority, ) else: logger.info( - "Setting up global class with unlimited download rate and priority: {}", + "Setting up global class with unlimited download rate (minimum: {}) and " + "priority: {}", lowest_priority, + global_download_minimum_rate, ) if config_global_upload_rate is not None: logger.info( - "Setting up global class with max upload rate: {} and priority: {}", + "Setting up global class with max upload rate: {} (minimum: {}) and " + "priority: {}", global_upload_rate, + global_upload_minimum_rate, lowest_priority, ) else: logger.info( - "Setting up global class with unlimited upload rate and priority: {}", + "Setting up global class with unlimited upload rate (minimum: {}) and " + "priority: {}", lowest_priority, + global_upload_minimum_rate, ) ingress, egress = tc_setup( - arguments.device, global_download_rate, global_upload_rate, lowest_priority, + arguments.device, + global_download_rate, + global_download_minimum_rate, + global_upload_rate, + global_upload_minimum_rate, + lowest_priority, ) ingress_interface, ingress_qdisc_id, ingress_root_class_id = ingress egress_interface, egress_qdisc_id, egress_root_class_id = egress @@ -160,11 +190,26 @@ def main(arguments: argparse.Namespace) -> None: else config_upload_priority ) + config_download_minimum_rate = process.get("download-minimum") + download_minimum_rate = ( + MINIMUM_DOWNLOAD_RATE + if config_download_minimum_rate is None + else config_download_minimum_rate + ) + config_upload_minimum_rate = process.get("upload-minimum") + upload_minimum_rate = ( + MINIMUM_UPLOAD_RATE + if config_upload_minimum_rate is None + else config_upload_minimum_rate + ) + if config_download_rate is not None: logger.info( - "Setting up class for: {!r} with max download rate: {} and priority: {}", + "Setting up class for: {!r} with max download rate: {} (minimum: {}) " + "and priority: {}", name, download_rate, + download_minimum_rate, download_priority, ) egress_class_id = tc_add_htb_class( @@ -172,13 +217,16 @@ def main(arguments: argparse.Namespace) -> None: ingress_qdisc_id, ingress_root_class_id, download_rate, + download_minimum_rate, download_priority, ) class_ids[_TrafficType.Ingress][name] = egress_class_id elif config_download_priority is not None: logger.info( - "Setting up class for: {!r} with unlimited download rate and priority: {}", + "Setting up class for: {!r} with unlimited download rate (minimum: {}) " + "and priority: {}", name, + download_minimum_rate, download_priority, ) egress_class_id = tc_add_htb_class( @@ -186,15 +234,18 @@ def main(arguments: argparse.Namespace) -> None: ingress_qdisc_id, ingress_root_class_id, download_rate, + download_minimum_rate, download_priority, ) class_ids[_TrafficType.Ingress][name] = egress_class_id if config_upload_rate is not None: logger.info( - "Setting up class for: {!r} with max upload rate: {} and priority: {}", + "Setting up class for: {!r} with max upload rate: {} (minimum: {}) and " + "priority: {}", name, upload_rate, + upload_minimum_rate, upload_priority, ) ingress_class_id = tc_add_htb_class( @@ -202,13 +253,16 @@ def main(arguments: argparse.Namespace) -> None: egress_qdisc_id, egress_root_class_id, upload_rate, + upload_minimum_rate, upload_priority, ) class_ids[_TrafficType.Egress][name] = ingress_class_id elif config_upload_priority is not None: logger.info( - "Setting up class for: {!r} with unlimited upload rate and priority: {}", + "Setting up class for: {!r} with unlimited upload rate (minimum: {}) " + "and priority: {}", name, + upload_minimum_rate, upload_priority, ) ingress_class_id = tc_add_htb_class( @@ -216,6 +270,7 @@ def main(arguments: argparse.Namespace) -> None: egress_qdisc_id, egress_root_class_id, upload_rate, + upload_minimum_rate, upload_priority, ) class_ids[_TrafficType.Egress][name] = ingress_class_id diff --git a/traffictoll/tc.py b/traffictoll/tc.py index 4a4cebe..a03d65c 100644 --- a/traffictoll/tc.py +++ b/traffictoll/tc.py @@ -1,13 +1,14 @@ import atexit import re import subprocess -from typing import Iterable, Optional, Tuple, Set +from typing import Iterable, Optional, Tuple, Set, Union import psutil from loguru import logger from .utils import run +MIN_RATE = 8 # "TC store rates as a 32-bit unsigned integer in bps internally, so we can specify a # max rate of 4294967295 bps" (source: `$ man tc`) MAX_RATE = 4294967295 @@ -127,8 +128,10 @@ def _get_free_class_id(interface: str, qdisc_id: int) -> int: def tc_setup( interface: str, - download_rate: int = MAX_RATE, - upload_rate: int = MAX_RATE, + download_rate: Union[int, str] = MAX_RATE, + download_minimum_rate: Union[int, str] = MIN_RATE, + upload_rate: Union[int, str] = MAX_RATE, + upload_minimum_rate: Union[int, str] = MIN_RATE, default_priority: int = 0, ) -> Tuple[Tuple[str, int, int], Tuple[str, int, int]]: # Set up IFB device @@ -155,6 +158,7 @@ def tc_setup( ifb_device_qdisc_id, ifb_device_root_class_id, download_rate, + download_minimum_rate, default_priority, ) run( @@ -178,6 +182,7 @@ def tc_setup( interface_qdisc_id, interface_root_class_id, upload_rate, + upload_minimum_rate, default_priority, ) run( @@ -195,7 +200,8 @@ def tc_add_htb_class( interface: str, parent_qdisc_id: int, parent_class_id: int, - rate: int, + ceil: Union[int, str] = MAX_RATE, + rate: Union[int, str] = MIN_RATE, priority: int = 0, ): class_id = _get_free_class_id(interface, parent_qdisc_id) @@ -204,7 +210,7 @@ def tc_add_htb_class( # specify a rate higher than the global rate run( f"tc class add dev {interface} parent {parent_qdisc_id}:{parent_class_id} " - f"classid {parent_qdisc_id}:{class_id} htb rate 8 ceil {rate} prio {priority}" + f"classid {parent_qdisc_id}:{class_id} htb rate {rate} ceil {ceil} prio {priority}" ) return class_id