bwflow 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bwflow-0.1.0/PKG-INFO +113 -0
- bwflow-0.1.0/README.md +102 -0
- bwflow-0.1.0/pyproject.toml +62 -0
- bwflow-0.1.0/src/bwflow/__init__.py +3 -0
- bwflow-0.1.0/src/bwflow/budget.py +92 -0
- bwflow-0.1.0/src/bwflow/config.py +189 -0
- bwflow-0.1.0/src/bwflow/contrib/bwflow.service +42 -0
- bwflow-0.1.0/src/bwflow/daemon.py +223 -0
- bwflow-0.1.0/src/bwflow/ipc.py +107 -0
- bwflow-0.1.0/src/bwflow/main.py +266 -0
- bwflow-0.1.0/src/bwflow/report.py +90 -0
- bwflow-0.1.0/src/bwflow/status.py +57 -0
- bwflow-0.1.0/src/bwflow/tc.py +141 -0
- bwflow-0.1.0/src/bwflow/vnstat.py +183 -0
- bwflow-0.1.0/src/bwflow/vnstat_conf.py +139 -0
bwflow-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: bwflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bandwidth manager for Linux
|
|
5
|
+
Author: Blake Axon
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Dist: typer>=0.24.1
|
|
8
|
+
Requires-Dist: rich>=14.3.3
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# bwflow
|
|
13
|
+
|
|
14
|
+
Egress bandwidth manager for Linux. Tracks monthly traffic via [vnstat](https://humdi.net/vnstat/) and enforces rate limits using `tc` when your budget runs out.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
At the start of each month, bwflow credits a configurable **burst allowance** (e.g. 50 GB) immediately. The remaining budget drips in continuously over the month. If egress exceeds the available tokens, a `tc` TBF rate limit is applied to your network interface. When the next month starts, the limit is lifted automatically.
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
| Package | Purpose |
|
|
23
|
+
| ----------------- | ------------------------------------ |
|
|
24
|
+
| `vnstat ≥ 2.6` | Traffic accounting (source of truth) |
|
|
25
|
+
| `iproute2` (`tc`) | Applies rate limits to the interface |
|
|
26
|
+
| Python ≥ 3.13 | Runtime |
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Ubuntu / Debian
|
|
30
|
+
sudo apt install vnstat iproute2
|
|
31
|
+
|
|
32
|
+
# Amazon Linux / RHEL
|
|
33
|
+
sudo yum install vnstat iproute2
|
|
34
|
+
|
|
35
|
+
# Alpine
|
|
36
|
+
apk add vnstat iproute2
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Enable the vnstat daemon:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
sudo systemctl enable --now vnstat
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install bwflow
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Verify:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bwflow --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
**1. Initialise** (creates `/etc/bwflow/config.toml`, offers to patch `vnstat.conf`):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
sudo bwflow init
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**2. Review the config:**
|
|
66
|
+
|
|
67
|
+
```toml
|
|
68
|
+
# /etc/bwflow/config.toml
|
|
69
|
+
|
|
70
|
+
[bandwidth]
|
|
71
|
+
monthly_budget_gb = 100 # total monthly egress budget
|
|
72
|
+
pre_charge_gb = 50 # burst tokens available from day 1
|
|
73
|
+
billing_day = 1 # reset on the 1st of each month
|
|
74
|
+
|
|
75
|
+
[network]
|
|
76
|
+
interface = "eth0" # interface to monitor and throttle
|
|
77
|
+
|
|
78
|
+
[daemon]
|
|
79
|
+
poll_interval_seconds = 15
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**3. Install and start the systemd service:**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
sudo cp /usr/local/lib/python3.x/site-packages/bwflow/contrib/bwflow.service \
|
|
86
|
+
/etc/systemd/system/
|
|
87
|
+
# or from a source checkout:
|
|
88
|
+
sudo cp contrib/bwflow.service /etc/systemd/system/
|
|
89
|
+
|
|
90
|
+
sudo systemctl daemon-reload
|
|
91
|
+
sudo systemctl enable --now bwflow
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**4. Check logs:**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
journalctl -u bwflow -f
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Commands
|
|
101
|
+
|
|
102
|
+
| Command | Description |
|
|
103
|
+
| --------------------------- | -------------------------------------------- |
|
|
104
|
+
| `sudo bwflow init` | First-time setup: config + vnstat.conf patch |
|
|
105
|
+
| `sudo bwflow run` | Start the daemon (called by systemd) |
|
|
106
|
+
| `bwflow report` | Show monthly traffic report |
|
|
107
|
+
| `sudo bwflow run --verbose` | Debug logging (shows every 15 s tick) |
|
|
108
|
+
|
|
109
|
+
## Notes
|
|
110
|
+
|
|
111
|
+
- `billing_day` must be `1` in v0.1 (calendar-month only). Arbitrary billing days are planned.
|
|
112
|
+
- bwflow requires `UseUTC 1` in `/etc/vnstat.conf` on non-UTC systems. `bwflow init` offers to patch this automatically.
|
|
113
|
+
- Rate limiting uses Linux `tc` TBF (Token Bucket Filter) and requires `CAP_NET_ADMIN`. The simplest setup is to run as root; see `contrib/bwflow.service` for a least-privilege alternative.
|
bwflow-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# bwflow
|
|
2
|
+
|
|
3
|
+
Egress bandwidth manager for Linux. Tracks monthly traffic via [vnstat](https://humdi.net/vnstat/) and enforces rate limits using `tc` when your budget runs out.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
At the start of each month, bwflow credits a configurable **burst allowance** (e.g. 50 GB) immediately. The remaining budget drips in continuously over the month. If egress exceeds the available tokens, a `tc` TBF rate limit is applied to your network interface. When the next month starts, the limit is lifted automatically.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
| Package | Purpose |
|
|
12
|
+
| ----------------- | ------------------------------------ |
|
|
13
|
+
| `vnstat ≥ 2.6` | Traffic accounting (source of truth) |
|
|
14
|
+
| `iproute2` (`tc`) | Applies rate limits to the interface |
|
|
15
|
+
| Python ≥ 3.13 | Runtime |
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Ubuntu / Debian
|
|
19
|
+
sudo apt install vnstat iproute2
|
|
20
|
+
|
|
21
|
+
# Amazon Linux / RHEL
|
|
22
|
+
sudo yum install vnstat iproute2
|
|
23
|
+
|
|
24
|
+
# Alpine
|
|
25
|
+
apk add vnstat iproute2
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Enable the vnstat daemon:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
sudo systemctl enable --now vnstat
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install bwflow
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Verify:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bwflow --help
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
**1. Initialise** (creates `/etc/bwflow/config.toml`, offers to patch `vnstat.conf`):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sudo bwflow init
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**2. Review the config:**
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
# /etc/bwflow/config.toml
|
|
58
|
+
|
|
59
|
+
[bandwidth]
|
|
60
|
+
monthly_budget_gb = 100 # total monthly egress budget
|
|
61
|
+
pre_charge_gb = 50 # burst tokens available from day 1
|
|
62
|
+
billing_day = 1 # reset on the 1st of each month
|
|
63
|
+
|
|
64
|
+
[network]
|
|
65
|
+
interface = "eth0" # interface to monitor and throttle
|
|
66
|
+
|
|
67
|
+
[daemon]
|
|
68
|
+
poll_interval_seconds = 15
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**3. Install and start the systemd service:**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
sudo cp /usr/local/lib/python3.x/site-packages/bwflow/contrib/bwflow.service \
|
|
75
|
+
/etc/systemd/system/
|
|
76
|
+
# or from a source checkout:
|
|
77
|
+
sudo cp contrib/bwflow.service /etc/systemd/system/
|
|
78
|
+
|
|
79
|
+
sudo systemctl daemon-reload
|
|
80
|
+
sudo systemctl enable --now bwflow
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**4. Check logs:**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
journalctl -u bwflow -f
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Commands
|
|
90
|
+
|
|
91
|
+
| Command | Description |
|
|
92
|
+
| --------------------------- | -------------------------------------------- |
|
|
93
|
+
| `sudo bwflow init` | First-time setup: config + vnstat.conf patch |
|
|
94
|
+
| `sudo bwflow run` | Start the daemon (called by systemd) |
|
|
95
|
+
| `bwflow report` | Show monthly traffic report |
|
|
96
|
+
| `sudo bwflow run --verbose` | Debug logging (shows every 15 s tick) |
|
|
97
|
+
|
|
98
|
+
## Notes
|
|
99
|
+
|
|
100
|
+
- `billing_day` must be `1` in v0.1 (calendar-month only). Arbitrary billing days are planned.
|
|
101
|
+
- bwflow requires `UseUTC 1` in `/etc/vnstat.conf` on non-UTC systems. `bwflow init` offers to patch this automatically.
|
|
102
|
+
- Rate limiting uses Linux `tc` TBF (Token Bucket Filter) and requires `CAP_NET_ADMIN`. The simplest setup is to run as root; see `contrib/bwflow.service` for a least-privilege alternative.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.10.6"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bwflow"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Bandwidth manager for Linux"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Blake Axon" },
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"typer>=0.24.1",
|
|
17
|
+
"rich>=14.3.3",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
bwflow = "bwflow.main:app"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=9.0.2",
|
|
26
|
+
"pytest-mock>=3.15.1",
|
|
27
|
+
"ruff>=0.15.4",
|
|
28
|
+
"ty>=0.0.19",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 79
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
select = ["ALL"]
|
|
36
|
+
ignore = [
|
|
37
|
+
"D203",
|
|
38
|
+
"D213",
|
|
39
|
+
"E501",
|
|
40
|
+
"COM812",
|
|
41
|
+
"FBT001",
|
|
42
|
+
"FBT003",
|
|
43
|
+
"PLC0415",
|
|
44
|
+
"PLR0913",
|
|
45
|
+
]
|
|
46
|
+
fixable = ["ALL"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.per-file-ignores]
|
|
49
|
+
# ignore unused imports in __init__.py files
|
|
50
|
+
"__init__.py" = ["F401"]
|
|
51
|
+
# Ignore missing type annotations in tests
|
|
52
|
+
"tests/**/*.py" = [
|
|
53
|
+
"ANN",
|
|
54
|
+
"D103",
|
|
55
|
+
"INP001",
|
|
56
|
+
"PLR2004",
|
|
57
|
+
"S101",
|
|
58
|
+
"SLF001",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.uv]
|
|
62
|
+
package = true
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Token bucket math: calculate remaining monthly egress budget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import calendar
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from bwflow.config import BwflowConfig
|
|
12
|
+
|
|
13
|
+
# 1 GB = 10^9 bytes (SI units, matching CSP billing conventions).
|
|
14
|
+
GB = 1_000_000_000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BudgetState:
|
|
19
|
+
"""Current budget calculation output for daemon decision making."""
|
|
20
|
+
|
|
21
|
+
tokens_bytes: int # Remaining bytes (may be negative when over budget).
|
|
22
|
+
throttle_rate_kbps: (
|
|
23
|
+
int # Rate cap to apply when throttled (kbits/sec for tc).
|
|
24
|
+
)
|
|
25
|
+
period_start: datetime # UTC start of the current billing period.
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def is_exhausted(self) -> bool:
|
|
29
|
+
"""True when the token bucket is empty (budget exceeded)."""
|
|
30
|
+
return self.tokens_bytes <= 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def calculate(
|
|
34
|
+
cfg: BwflowConfig,
|
|
35
|
+
monthly_tx_bytes: int,
|
|
36
|
+
now: datetime | None = None,
|
|
37
|
+
) -> BudgetState:
|
|
38
|
+
"""Calculate the current token bucket state.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
cfg: bwflow configuration.
|
|
42
|
+
monthly_tx_bytes: Current billing period's egress in bytes (from vnstat).
|
|
43
|
+
now: Current UTC time. Defaults to ``datetime.now(UTC)``.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
BudgetState with remaining tokens and throttle parameters.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
if now is None:
|
|
50
|
+
now = datetime.now(UTC)
|
|
51
|
+
|
|
52
|
+
period_start = _period_start(cfg, now)
|
|
53
|
+
total_seconds = _seconds_in_month(period_start)
|
|
54
|
+
elapsed_seconds = (now - period_start).total_seconds()
|
|
55
|
+
|
|
56
|
+
pre_charge_bytes = cfg.pre_charge_gb * GB
|
|
57
|
+
drip_total_bytes = cfg.drip_gb * GB
|
|
58
|
+
drip_rate_bps = drip_total_bytes / total_seconds # bytes / second
|
|
59
|
+
|
|
60
|
+
drip_accrued = min(drip_rate_bps * elapsed_seconds, drip_total_bytes)
|
|
61
|
+
tokens = int(pre_charge_bytes + drip_accrued - monthly_tx_bytes)
|
|
62
|
+
|
|
63
|
+
# Throttle rate: drip rate expressed in kbits/sec (tc's unit).
|
|
64
|
+
throttle_kbps = max(1, int(drip_rate_bps * 8 / 1000))
|
|
65
|
+
|
|
66
|
+
return BudgetState(
|
|
67
|
+
tokens_bytes=tokens,
|
|
68
|
+
throttle_rate_kbps=throttle_kbps,
|
|
69
|
+
period_start=period_start,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Internal helpers
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _period_start(cfg: BwflowConfig, now: datetime) -> datetime:
|
|
79
|
+
"""Return the UTC datetime of the current billing period start."""
|
|
80
|
+
day = cfg.billing_day
|
|
81
|
+
if now.day >= day:
|
|
82
|
+
return datetime(now.year, now.month, day, tzinfo=UTC)
|
|
83
|
+
# billing started last month
|
|
84
|
+
if now.month == 1:
|
|
85
|
+
return datetime(now.year - 1, 12, day, tzinfo=UTC)
|
|
86
|
+
return datetime(now.year, now.month - 1, day, tzinfo=UTC)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _seconds_in_month(period_start: datetime) -> float:
|
|
90
|
+
"""Total seconds in the calendar month that *period_start* falls in."""
|
|
91
|
+
days = calendar.monthrange(period_start.year, period_start.month)[1]
|
|
92
|
+
return float(days * 86400)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""bwflow configuration: schema, defaults, and /etc/bwflow/config.toml management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tomllib
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from bwflow import vnstat
|
|
11
|
+
|
|
12
|
+
CONFIG_DIR = Path("/etc/bwflow")
|
|
13
|
+
CONFIG_PATH = CONFIG_DIR / "config.toml"
|
|
14
|
+
|
|
15
|
+
_CONFIG_TEMPLATE = """\
|
|
16
|
+
[bandwidth]
|
|
17
|
+
# Total monthly egress budget in GB.
|
|
18
|
+
monthly_budget_gb = {monthly_budget_gb}
|
|
19
|
+
|
|
20
|
+
# Pre-charged burst tokens credited at the start of each billing cycle.
|
|
21
|
+
# The remainder ({drip_gb} GB) drips in continuously over the month.
|
|
22
|
+
pre_charge_gb = {pre_charge_gb}
|
|
23
|
+
|
|
24
|
+
# Day-of-month when the budget resets.
|
|
25
|
+
# Note: only 1 is supported currently (calendar-month reset).
|
|
26
|
+
billing_day = {billing_day}
|
|
27
|
+
|
|
28
|
+
[network]
|
|
29
|
+
# Network interface to monitor and throttle.
|
|
30
|
+
# Run `ip link show` or `vnstat` to list available interfaces.
|
|
31
|
+
interface = "{interface}"
|
|
32
|
+
|
|
33
|
+
[daemon]
|
|
34
|
+
# How often (in seconds) the daemon polls vnstat and adjusts tc rules.
|
|
35
|
+
poll_interval_seconds = {poll_interval_seconds}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Config dataclass
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class BwflowConfig:
|
|
46
|
+
"""Runtime configuration loaded from /etc/bwflow/config.toml."""
|
|
47
|
+
|
|
48
|
+
monthly_budget_gb: int = 100
|
|
49
|
+
pre_charge_gb: int = 50
|
|
50
|
+
billing_day: int = 1
|
|
51
|
+
interface: str = "eth0"
|
|
52
|
+
poll_interval_seconds: int = 15
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def drip_gb(self) -> int:
|
|
56
|
+
"""Return the monthly budget portion that accrues over time."""
|
|
57
|
+
return self.monthly_budget_gb - self.pre_charge_gb
|
|
58
|
+
|
|
59
|
+
def to_toml_str(self) -> str:
|
|
60
|
+
"""Serialize this config to the bwflow TOML file format."""
|
|
61
|
+
return _CONFIG_TEMPLATE.format(
|
|
62
|
+
monthly_budget_gb=self.monthly_budget_gb,
|
|
63
|
+
pre_charge_gb=self.pre_charge_gb,
|
|
64
|
+
drip_gb=self.drip_gb,
|
|
65
|
+
billing_day=self.billing_day,
|
|
66
|
+
interface=self.interface,
|
|
67
|
+
poll_interval_seconds=self.poll_interval_seconds,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Public API
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def detect_default_interface() -> str:
|
|
77
|
+
"""Return the first interface reported by vnstat, or 'eth0' as fallback."""
|
|
78
|
+
try:
|
|
79
|
+
stats = vnstat.get_monthly_stats(limit=1)
|
|
80
|
+
if stats:
|
|
81
|
+
return stats[0].name
|
|
82
|
+
except RuntimeError:
|
|
83
|
+
pass
|
|
84
|
+
return "eth0"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_default_config() -> BwflowConfig:
|
|
88
|
+
"""Build a BwflowConfig with sensible defaults, auto-detecting the interface."""
|
|
89
|
+
return BwflowConfig(interface=detect_default_interface())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check_root() -> None:
|
|
93
|
+
"""Raise PermissionError if the process is not running as root."""
|
|
94
|
+
if os.geteuid() != 0:
|
|
95
|
+
msg = (
|
|
96
|
+
"bwflow init must be run as root to write to /etc/bwflow/.\n"
|
|
97
|
+
"Re-run with: sudo bwflow init"
|
|
98
|
+
)
|
|
99
|
+
raise PermissionError(
|
|
100
|
+
msg,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def write_config(cfg: BwflowConfig, *, force: bool = False) -> None:
|
|
105
|
+
"""Write cfg to /etc/bwflow/config.toml.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
cfg: Config to serialise.
|
|
109
|
+
force: Overwrite an existing config file without prompting.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
FileExistsError: If the config already exists and force is False.
|
|
113
|
+
PermissionError: Propagated from the OS if the process lacks write access.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
CONFIG_DIR.mkdir(mode=0o755, parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
if CONFIG_PATH.exists() and not force:
|
|
119
|
+
msg = (
|
|
120
|
+
f"{CONFIG_PATH} already exists.\n"
|
|
121
|
+
"Edit it manually, or re-run with --force to overwrite."
|
|
122
|
+
)
|
|
123
|
+
raise FileExistsError(
|
|
124
|
+
msg,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
CONFIG_PATH.write_text(cfg.to_toml_str(), encoding="utf-8")
|
|
128
|
+
# Owner-readable/writable; group+other readable — same as typical /etc files.
|
|
129
|
+
CONFIG_PATH.chmod(0o644)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def config_exists() -> bool:
|
|
133
|
+
"""Return True when the default config file is present."""
|
|
134
|
+
return CONFIG_PATH.exists()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def load_config(path: Path = CONFIG_PATH) -> BwflowConfig:
|
|
138
|
+
"""Load and validate /etc/bwflow/config.toml into a BwflowConfig.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
FileNotFoundError: If the config file does not exist.
|
|
142
|
+
ValueError: If required keys are missing or values are invalid.
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
if not path.exists():
|
|
146
|
+
msg = f"{path} not found. Run: sudo bwflow init"
|
|
147
|
+
raise FileNotFoundError(msg)
|
|
148
|
+
|
|
149
|
+
with path.open("rb") as f:
|
|
150
|
+
data = tomllib.load(f)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
bw = data["bandwidth"]
|
|
154
|
+
net = data["network"]
|
|
155
|
+
dm = data["daemon"]
|
|
156
|
+
|
|
157
|
+
cfg = BwflowConfig(
|
|
158
|
+
monthly_budget_gb=int(bw["monthly_budget_gb"]),
|
|
159
|
+
pre_charge_gb=int(bw["pre_charge_gb"]),
|
|
160
|
+
billing_day=int(bw["billing_day"]),
|
|
161
|
+
interface=str(net["interface"]),
|
|
162
|
+
poll_interval_seconds=int(dm["poll_interval_seconds"]),
|
|
163
|
+
)
|
|
164
|
+
except KeyError as exc:
|
|
165
|
+
msg = f"Missing required config key: {exc}"
|
|
166
|
+
raise ValueError(msg) from exc
|
|
167
|
+
except (TypeError, ValueError) as exc:
|
|
168
|
+
msg = f"Invalid config value: {exc}"
|
|
169
|
+
raise ValueError(msg) from exc
|
|
170
|
+
|
|
171
|
+
# Basic sanity checks.
|
|
172
|
+
if cfg.billing_day != 1:
|
|
173
|
+
msg = (
|
|
174
|
+
"billing_day != 1 is not yet supported.\n"
|
|
175
|
+
"vnstat only provides calendar-month totals, so any other value\n"
|
|
176
|
+
"would understate usage and delay throttling incorrectly.\n"
|
|
177
|
+
"Set billing_day = 1 for now."
|
|
178
|
+
)
|
|
179
|
+
raise ValueError(
|
|
180
|
+
msg,
|
|
181
|
+
)
|
|
182
|
+
if cfg.pre_charge_gb > cfg.monthly_budget_gb:
|
|
183
|
+
msg = "pre_charge_gb must be less or equal than monthly_budget_gb."
|
|
184
|
+
raise ValueError(msg)
|
|
185
|
+
if cfg.poll_interval_seconds < 1:
|
|
186
|
+
msg = "poll_interval_seconds must be >= 1."
|
|
187
|
+
raise ValueError(msg)
|
|
188
|
+
|
|
189
|
+
return cfg
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=bwflow bandwidth rate-limit daemon
|
|
3
|
+
Documentation=https://github.com/blakeaxon/bwflow
|
|
4
|
+
# Start after the network is up and vnstat has started.
|
|
5
|
+
After=network.target vnstat.service
|
|
6
|
+
Wants=vnstat.service
|
|
7
|
+
|
|
8
|
+
[Service]
|
|
9
|
+
Type=simple
|
|
10
|
+
|
|
11
|
+
# Verify the binary path with: which bwflow
|
|
12
|
+
# Common locations: /usr/local/bin/bwflow (pip/pipx) or /usr/bin/bwflow (distro package)
|
|
13
|
+
ExecStart=/usr/local/bin/bwflow run
|
|
14
|
+
|
|
15
|
+
# Restart automatically on unexpected exits (e.g. crash).
|
|
16
|
+
# Does NOT restart on clean exit (SIGTERM from 'systemctl stop').
|
|
17
|
+
Restart=on-failure
|
|
18
|
+
RestartSec=10s
|
|
19
|
+
|
|
20
|
+
# --- Privilege model ---
|
|
21
|
+
# Option A (simple): run as root.
|
|
22
|
+
User=root
|
|
23
|
+
|
|
24
|
+
# Option B (recommended for production): dedicated system user with only
|
|
25
|
+
# the capability needed to call tc. Uncomment after creating the user:
|
|
26
|
+
# useradd --system --no-create-home --shell /sbin/nologin bwflow
|
|
27
|
+
# setcap cap_net_admin+eip /usr/local/bin/bwflow
|
|
28
|
+
#
|
|
29
|
+
# User=bwflow
|
|
30
|
+
# AmbientCapabilities=CAP_NET_ADMIN
|
|
31
|
+
# CapabilityBoundingSet=CAP_NET_ADMIN
|
|
32
|
+
# NoNewPrivileges=yes
|
|
33
|
+
|
|
34
|
+
# --- Logging ---
|
|
35
|
+
# Logs go to journald automatically. View with:
|
|
36
|
+
# journalctl -u bwflow -f
|
|
37
|
+
StandardOutput=journal
|
|
38
|
+
StandardError=journal
|
|
39
|
+
SyslogIdentifier=bwflow
|
|
40
|
+
|
|
41
|
+
[Install]
|
|
42
|
+
WantedBy=multi-user.target
|