Skip to content

Commit

Permalink
Add support for global and per-process download and upload minimum
Browse files Browse the repository at this point in the history
  • Loading branch information
cryzed committed Jan 31, 2020
1 parent b7884fd commit 8339143
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 16 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]

Expand Down
73 changes: 64 additions & 9 deletions traffictoll/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -160,62 +190,87 @@ 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(
ingress_interface,
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(
ingress_interface,
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(
egress_interface,
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(
egress_interface,
egress_qdisc_id,
egress_root_class_id,
upload_rate,
upload_minimum_rate,
upload_priority,
)
class_ids[_TrafficType.Egress][name] = ingress_class_id
Expand Down
16 changes: 11 additions & 5 deletions traffictoll/tc.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -155,6 +158,7 @@ def tc_setup(
ifb_device_qdisc_id,
ifb_device_root_class_id,
download_rate,
download_minimum_rate,
default_priority,
)
run(
Expand All @@ -178,6 +182,7 @@ def tc_setup(
interface_qdisc_id,
interface_root_class_id,
upload_rate,
upload_minimum_rate,
default_priority,
)
run(
Expand All @@ -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)
Expand All @@ -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

Expand Down

0 comments on commit 8339143

Please sign in to comment.