Compare commits
91
Commits
v0.4.0
...
36e3c640d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36e3c640d2 | ||
|
|
8fb051d48f | ||
|
|
a0535357c7 | ||
|
|
91d773c77e | ||
|
|
083d464067 | ||
|
|
f0eae4b2ce | ||
|
|
a6067910ad | ||
|
|
ffd7b0beb7 | ||
|
|
377fb3be2d | ||
|
|
d765bef002 | ||
|
|
86b13f948e | ||
|
|
4abf4bfcb8 | ||
|
|
1a49c9adce | ||
|
|
8c831e4287 | ||
|
|
6a90446b56 | ||
|
|
1ad47eaeb0 | ||
|
|
a27b95e9ca | ||
|
|
be6546b443 | ||
|
|
605ec6ef81 | ||
|
|
7e0f434251 | ||
|
|
3c688089a1 | ||
|
|
a6478c9bd4 | ||
|
|
f51d90bff6 | ||
|
|
a653bcf279 | ||
|
|
af07915441 | ||
|
|
39add2b2e0 | ||
|
|
c0a50c68f9 | ||
|
|
8bacb102b7 | ||
|
|
bc86a2cd53 | ||
|
|
c6338b596e | ||
|
|
7f557f26d4 | ||
|
|
c1bcb22b23 | ||
|
|
51377f947f | ||
|
|
dc4ce1159f | ||
|
|
288d1afdb9 | ||
|
|
bf0027a0b3 | ||
|
|
49b700d4b2 | ||
|
|
f6fa7b8814 | ||
|
|
cd160e67fe | ||
|
|
bab8278bdd | ||
|
|
c9cecb6945 | ||
|
|
b45ae4b6d6 | ||
|
|
b762901470 | ||
|
|
cd6d631afa | ||
|
|
b07dc99bf5 | ||
|
|
ae9193580c | ||
|
|
50f0dcb4dd | ||
|
|
9811592f13 | ||
|
|
c5d673fcb8 | ||
|
|
b56c8b72cc | ||
|
|
8b0c37bc83 | ||
|
|
694cad1fac | ||
|
|
908fbfdf04 | ||
|
|
2dacde8a3f | ||
|
|
371b3efcdc | ||
|
|
81d6ebe3a8 | ||
|
|
8ccbd30582 | ||
|
|
ae3cd96313 | ||
|
|
3d1c14d312 | ||
|
|
7955046426 | ||
|
|
30e3281178 | ||
|
|
f3f033cf0f | ||
|
|
4aaf8fa100 | ||
|
|
dfa11be236 | ||
|
|
58d536bb22 | ||
|
|
6f16f3a93c | ||
|
|
1b959d52ed | ||
|
|
8c29d7b251 | ||
|
|
80adb368d4 | ||
|
|
6ae19b619a | ||
|
|
7250050f63 | ||
|
|
3322e82b6c | ||
|
|
ea19c6d68b | ||
|
|
a81d208c0d | ||
|
|
e22c100e57 | ||
|
|
72fe0230ee | ||
|
|
6690402a34 | ||
|
|
3ec53a1224 | ||
|
|
7d5c2f0275 | ||
|
|
45da0c030e | ||
|
|
33169182be | ||
|
|
2aea1bea86 | ||
|
|
d0e3394f03 | ||
|
|
7c40990e93 | ||
|
|
a739d1a5e2 | ||
|
|
11837195de | ||
|
|
706d93f146 | ||
|
|
7b0eb942ea | ||
|
|
5daccecc1d | ||
|
|
2c336799b8 | ||
|
|
8739ed4419 |
No files matched your search
@@ -5,6 +5,7 @@ on:
|
|||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -15,8 +16,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Install Mold
|
- name: Install Mold
|
||||||
run: sudo apt install mold
|
run: sudo apt install mold
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --verbose
|
run: cargo build --verbose
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Run DevSkim scanner
|
- name: Run DevSkim scanner
|
||||||
uses: microsoft/DevSkim-Action@v1
|
uses: microsoft/DevSkim-Action@v1
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
|
||||||
# If you want to copy this config, highly suggest pinning this version to a release rather than tracking the main branch
|
# If you want to copy this config, highly suggest pinning this version to a release rather than tracking the main branch
|
||||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.3"
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.5.1"
|
||||||
with:
|
with:
|
||||||
# Just scan the root directory and docs, since everything else is fixtures
|
# Just scan the root directory and docs, since everything else is fixtures
|
||||||
scan-args: |-
|
scan-args: |-
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||||
# If you want to copy this config, highly suggest pinning this version to a release rather than tracking the main branch
|
# If you want to copy this config, highly suggest pinning this version to a release rather than tracking the main branch
|
||||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.3"
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.5.1"
|
||||||
with:
|
with:
|
||||||
# Just scan the root directory and docs, since everything else is fixtures
|
# Just scan the root directory and docs, since everything else is fixtures
|
||||||
scan-args: |-
|
scan-args: |-
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1
|
uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1
|
||||||
|
|||||||
Generated
+429
-191
@@ -11,82 +11,59 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ansi_term"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.102"
|
version = "1.0.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atty"
|
|
||||||
version = "0.2.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
|
||||||
dependencies = [
|
|
||||||
"hermit-abi",
|
|
||||||
"libc",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.58.1"
|
version = "0.72.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f"
|
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"clap",
|
"itertools",
|
||||||
"env_logger",
|
|
||||||
"lazy_static",
|
|
||||||
"lazycell",
|
|
||||||
"log",
|
"log",
|
||||||
"peeking_take_while",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"shlex",
|
"shlex 1.3.0",
|
||||||
"which",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "2.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bumpalo"
|
||||||
version = "2.11.0"
|
version = "3.20.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.56"
|
version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex 2.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cexpr"
|
name = "cexpr"
|
||||||
version = "0.4.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom",
|
||||||
]
|
]
|
||||||
@@ -108,43 +85,60 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap"
|
|
||||||
version = "2.34.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
|
|
||||||
dependencies = [
|
|
||||||
"ansi_term",
|
|
||||||
"atty",
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"strsim",
|
|
||||||
"textwrap",
|
|
||||||
"unicode-width",
|
|
||||||
"vec_map",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.57"
|
version = "0.1.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
|
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "config"
|
||||||
version = "0.8.4"
|
version = "0.15.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
|
checksum = "790374c727a4cb20877dd2cc99ce7011cbe03b0b8df083acccd37df0d161304d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atty",
|
"pathdiff",
|
||||||
"humantime",
|
"serde_core",
|
||||||
"log",
|
"toml",
|
||||||
"regex",
|
"winnow",
|
||||||
"termcolor",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
||||||
|
dependencies = [
|
||||||
|
"csv-core",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-core"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -157,9 +151,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
@@ -167,6 +164,29 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flume"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"spin",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -174,46 +194,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hashbrown"
|
||||||
version = "0.1.19"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "humantime"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icmp-socket2"
|
name = "icmp-socket2"
|
||||||
version = "0.4.2"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e57405ef902d5506111426bb8a2498f10979c19273bcdadc852ae03d45d19d8"
|
checksum = "86fc005b72eb9dbc1df87318cf906a415d4f4c0d5eea42849b29dd710cdf670d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"socket2",
|
"socket2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "indexmap"
|
||||||
version = "1.5.0"
|
version = "2.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazycell"
|
name = "itertools"
|
||||||
version = "1.3.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.182"
|
version = "0.2.189"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
@@ -227,11 +261,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libuci-sys"
|
name = "libuci-sys"
|
||||||
version = "1.0.5"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/Lochnair/rust-uci.git?rev=fcfedd204fb76928ef617bf9631b041c816e03d4#fcfedd204fb76928ef617bf9631b041c816e03d4"
|
||||||
checksum = "544c17966d9131e80b3616783974a31b526197ca5b56948bb130eeb6c04fbaf1"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen",
|
"bindgen",
|
||||||
|
"cc",
|
||||||
"cmake",
|
"cmake",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -242,39 +276,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "lock_api"
|
||||||
version = "0.4.29"
|
version = "0.4.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maybe-async"
|
name = "maybe-async"
|
||||||
version = "0.2.10"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
|
checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "netlink-bindings"
|
name = "netlink-bindings"
|
||||||
version = "0.2.4"
|
version = "0.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e639b3a966932d8ab55cfc1e606e5421aef3d30a2610284535b854f11f6e4b44"
|
checksum = "2a2a0dd8beef757878b977fd3d662c82a93dac4bf7632f42577ca8f8f4d701a0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "netlink-socket2"
|
name = "netlink-socket2"
|
||||||
version = "0.2.4"
|
version = "0.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ffef37f26cf649b9f473ccc00fa52d419511225a7b3a2093bf16dc8673ab9cf1"
|
checksum = "5c7ddb0ea7aa692947d5105160acb7c75c17bc5ec6d5a6263440ca7db4be9a34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"maybe-async",
|
"maybe-async",
|
||||||
@@ -283,43 +332,65 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "5.1.3"
|
version = "7.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b"
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"version_check",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "once_cell"
|
||||||
version = "0.1.2"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.107"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.44"
|
version = "1.0.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "r-efi"
|
||||||
version = "1.12.3"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -329,9 +400,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.14"
|
version = "0.4.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -340,26 +411,26 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.9"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-uci"
|
name = "rust-uci"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/Lochnair/rust-uci.git?rev=fcfedd204fb76928ef617bf9631b041c816e03d4#fcfedd204fb76928ef617bf9631b041c816e03d4"
|
||||||
checksum = "c664edb3ec081c1f2746960b04861354bbeaafdbb962fb8789d6ded5153ac3bb"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"libuci-sys",
|
"libuci-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "2.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
@@ -367,13 +438,83 @@ version = "1.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -381,21 +522,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "shlex"
|
||||||
version = "0.6.3"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.9.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqm-autorate-rust"
|
name = "sqm-autorate-rust"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"config",
|
||||||
|
"csv",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
"flume",
|
||||||
"icmp-socket2",
|
"icmp-socket2",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@@ -403,26 +562,39 @@ dependencies = [
|
|||||||
"netlink-socket2",
|
"netlink-socket2",
|
||||||
"rust-uci",
|
"rust-uci",
|
||||||
"rustix",
|
"rustix",
|
||||||
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"uci-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.119"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-triple"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@@ -432,33 +604,88 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "textwrap"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.18"
|
version = "2.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.18"
|
version = "2.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trybuild"
|
||||||
|
version = "1.0.120"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"target-triple",
|
||||||
|
"termcolor",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uci-derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.4",
|
||||||
|
"trybuild",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -468,47 +695,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "wasm-bindgen"
|
||||||
version = "0.1.14"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vec_map"
|
|
||||||
version = "0.8.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "which"
|
|
||||||
version = "3.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.3.9"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi-i686-pc-windows-gnu",
|
"quote",
|
||||||
"winapi-x86_64-pc-windows-gnu",
|
"wasm-bindgen-macro-support",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-i686-pc-windows-gnu"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.4.0"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
@@ -519,12 +748,6 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -539,3 +762,18 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
+21
-5
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sqm-autorate-rust"
|
name = "sqm-autorate-rust"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@@ -20,18 +20,34 @@ strip = "symbols"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
icmp-socket2 = "0.4.2"
|
icmp-socket2 = "0.6.0"
|
||||||
netlink-bindings = { version = "0.2", features = ["rt-link", "tc"] }
|
|
||||||
netlink-socket2 = "0.2"
|
|
||||||
fastrand = "2"
|
fastrand = "2"
|
||||||
rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] }
|
rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] }
|
||||||
rust-uci = { version = "0.1", optional = true }
|
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
flume = { version = "0.12.0", default-features = false, features = ["eventual-fairness", "select"] }
|
||||||
|
config = { version = "0.15.25", default-features = false }
|
||||||
|
csv = "1.4.0"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
uci-derive = { path = "crates/uci-derive" }
|
||||||
|
|
||||||
[dependencies.log]
|
[dependencies.log]
|
||||||
features = ["std"]
|
features = ["std"]
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
netlink-bindings = { version = "0.3.3", features = ["rt-link", "tc"] }
|
||||||
|
netlink-socket2 = { version = "0.3.3" }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
rust-uci = { git = "https://github.com/Lochnair/rust-uci.git", rev = "fcfedd204fb76928ef617bf9631b041c816e03d4", optional = true, default-features = false, }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["toml", "uci-vendored"]
|
||||||
|
toml = ["config/toml"]
|
||||||
uci = ["dep:rust-uci"]
|
uci = ["dep:rust-uci"]
|
||||||
|
uci-vendored = ["uci", "rust-uci/vendored"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/uci-derive"]
|
||||||
|
resolver = "3"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[target.x86_64-unknown-freebsd]
|
||||||
|
image.name = "ghcr.io/cross-rs/x86_64-unknown-freebsd:local"
|
||||||
|
image.toolchain = ["linux/arm64=aarch64-unknown-linux-gnu"]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ARG TARGET
|
||||||
|
|
||||||
|
FROM ghcr.io/rust-cross/rust-musl-cross:${TARGET}
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
Vendored
+6
-3
@@ -12,9 +12,9 @@ pipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
agent {
|
agent {
|
||||||
docker {
|
dockerfile {
|
||||||
image "ghcr.io/rust-cross/rust-musl-cross:${TARGET}"
|
filename 'Dockerfile.ci'
|
||||||
alwaysPull true
|
additionalBuildArgs "--pull --build-arg TARGET=${TARGET}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +44,9 @@ pipeline {
|
|||||||
echo "----------------------------------"
|
echo "----------------------------------"
|
||||||
|
|
||||||
# 5. Check Toolchain & Build
|
# 5. Check Toolchain & Build
|
||||||
|
# Force standalone static artifacts across the entire CI matrix.
|
||||||
|
export RUSTFLAGS="${RUSTFLAGS:+$RUSTFLAGS }-C target-feature=+crt-static"
|
||||||
|
|
||||||
# We grep for "nightly" in the version string (e.g., "rustc 1.95.0-nightly...")
|
# We grep for "nightly" in the version string (e.g., "rustc 1.95.0-nightly...")
|
||||||
if rustc -V | grep -q "nightly"; then
|
if rustc -V | grep -q "nightly"; then
|
||||||
echo "Detected Nightly toolchain. Enabling '-Z build-std' to support panic=abort..."
|
echo "Detected Nightly toolchain. Enabling '-Z build-std' to support panic=abort..."
|
||||||
|
|||||||
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let uci_enabled = env::var_os("CARGO_FEATURE_UCI").is_some();
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
let target = env::var("TARGET").unwrap_or_else(|_| "unknown".into());
|
||||||
|
|
||||||
|
if uci_enabled && target_os != "linux" {
|
||||||
|
println!(
|
||||||
|
"cargo:warning=UCI support on {target} is intended for development \
|
||||||
|
and testing; Linux/OpenWrt remains the production-supported platform"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match target_os.as_str() {
|
||||||
|
"linux" => {}
|
||||||
|
"macos" => println!(
|
||||||
|
"cargo::warning=traffic control is unavailable on macOS; requested rate changes will be calculated but not applied"
|
||||||
|
),
|
||||||
|
_ => println!(
|
||||||
|
"cargo::warning=target `{target}` has no native interface-statistics or traffic-control backend; synthetic interface load will be used and requested rate changes will not be applied"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo::rerun-if-changed=build.rs");
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
targets=(
|
||||||
|
aarch64-apple-darwin
|
||||||
|
x86_64-unknown-linux-gnu
|
||||||
|
x86_64-unknown-linux-musl
|
||||||
|
aarch64-unknown-linux-musl
|
||||||
|
x86_64-unknown-freebsd
|
||||||
|
)
|
||||||
|
|
||||||
|
for target in "${targets[@]}"; do
|
||||||
|
echo
|
||||||
|
echo "==> Checking ${target}"
|
||||||
|
cargo check \
|
||||||
|
--workspace \
|
||||||
|
--all-targets \
|
||||||
|
--target "${target}"
|
||||||
|
done
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "uci-derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "3", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
trybuild = "1"
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::ext::IdentExt;
|
||||||
|
use syn::{Data, DeriveInput, Field, Fields, FieldsNamed, LitStr, Token, parse_macro_input};
|
||||||
|
|
||||||
|
/// Generates `UciSectionSchema` from a settings struct.
|
||||||
|
///
|
||||||
|
/// Every named field is mapped one-to-one:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// Rust field name -> UCI option name
|
||||||
|
/// Rust field name -> config-rs field name
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_derive(UciSection)]
|
||||||
|
pub fn derive_uci_section(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
match expand_uci_section(&input) {
|
||||||
|
Ok(tokens) => tokens.into(),
|
||||||
|
Err(error) => error.to_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates `UciConfigSchema` from the root settings struct.
|
||||||
|
///
|
||||||
|
/// Required container attribute:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[uci(package = "sqm-autorate-rust")]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Every field must specify its UCI section:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[uci(section)]
|
||||||
|
/// network: NetworkSettings,
|
||||||
|
///
|
||||||
|
/// #[uci(section = "advanced_settings")]
|
||||||
|
/// advanced: AdvancedSettings,
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_derive(UciConfig, attributes(uci))]
|
||||||
|
pub fn derive_uci_config(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
match expand_uci_config(&input) {
|
||||||
|
Ok(tokens) => tokens.into(),
|
||||||
|
Err(error) => error.to_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_uci_section(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
let struct_name = &input.ident;
|
||||||
|
let fields = require_named_fields(input)?;
|
||||||
|
|
||||||
|
let mappings = fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
|
let field_ident = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("named fields always have identifiers");
|
||||||
|
|
||||||
|
let field_name = field_ident.unraw().to_string();
|
||||||
|
let field_name = LitStr::new(&field_name, field_ident.span());
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
crate::settings::uci_schema::UciOptionMapping {
|
||||||
|
uci_option: #field_name,
|
||||||
|
config_field: #field_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#[automatically_derived]
|
||||||
|
impl #impl_generics
|
||||||
|
crate::settings::uci_schema::UciSectionSchema
|
||||||
|
for #struct_name #type_generics
|
||||||
|
#where_clause
|
||||||
|
{
|
||||||
|
const UCI_OPTIONS: &'static [
|
||||||
|
crate::settings::uci_schema::UciOptionMapping
|
||||||
|
] = &[
|
||||||
|
#(#mappings),*
|
||||||
|
];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_uci_config(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
let struct_name = &input.ident;
|
||||||
|
let fields = require_named_fields(input)?;
|
||||||
|
let package = parse_package_name(input)?;
|
||||||
|
|
||||||
|
let mut sections = Vec::with_capacity(fields.named.len());
|
||||||
|
|
||||||
|
for field in &fields.named {
|
||||||
|
let field_ident = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("named fields always have identifiers");
|
||||||
|
|
||||||
|
let field_type = &field.ty;
|
||||||
|
|
||||||
|
let config_section_name = field_ident.unraw().to_string();
|
||||||
|
let config_section_name = LitStr::new(&config_section_name, field_ident.span());
|
||||||
|
|
||||||
|
let uci_section_name = parse_section_name(field)?;
|
||||||
|
|
||||||
|
sections.push(quote! {
|
||||||
|
crate::settings::uci_schema::UciSectionMapping {
|
||||||
|
uci_section: #uci_section_name,
|
||||||
|
config_section: #config_section_name,
|
||||||
|
options: <
|
||||||
|
#field_type as
|
||||||
|
crate::settings::uci_schema::UciSectionSchema
|
||||||
|
>::UCI_OPTIONS,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#[automatically_derived]
|
||||||
|
impl #impl_generics
|
||||||
|
crate::settings::uci_schema::UciConfigSchema
|
||||||
|
for #struct_name #type_generics
|
||||||
|
#where_clause
|
||||||
|
{
|
||||||
|
const UCI_PACKAGE: &'static str = #package;
|
||||||
|
|
||||||
|
const UCI_SECTIONS: &'static [
|
||||||
|
crate::settings::uci_schema::UciSectionMapping
|
||||||
|
] = &[
|
||||||
|
#(#sections),*
|
||||||
|
];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_named_fields(input: &DeriveInput) -> syn::Result<&FieldsNamed> {
|
||||||
|
match &input.data {
|
||||||
|
Data::Struct(data) => match &data.fields {
|
||||||
|
Fields::Named(fields) => Ok(fields),
|
||||||
|
|
||||||
|
Fields::Unnamed(_) | Fields::Unit => Err(syn::Error::new_spanned(
|
||||||
|
input,
|
||||||
|
"this derive only supports structs with named fields",
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
|
||||||
|
Data::Enum(_) | Data::Union(_) => Err(syn::Error::new_spanned(
|
||||||
|
input,
|
||||||
|
"this derive only supports structs",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_package_name(input: &DeriveInput) -> syn::Result<LitStr> {
|
||||||
|
let mut package = None;
|
||||||
|
|
||||||
|
for attribute in &input.attrs {
|
||||||
|
if !attribute.path().is_ident("uci") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute.parse_nested_meta(|meta| {
|
||||||
|
if meta.path.is_ident("package") {
|
||||||
|
if package.is_some() {
|
||||||
|
return Err(meta.error("duplicate `package` attribute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: LitStr = meta.value()?.parse()?;
|
||||||
|
|
||||||
|
if value.value().is_empty() {
|
||||||
|
return Err(syn::Error::new(value.span(), "`package` cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
package = Some(value);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(meta.error("expected `package = \"...\"`"))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
package.ok_or_else(|| {
|
||||||
|
syn::Error::new_spanned(
|
||||||
|
&input.ident,
|
||||||
|
"UciConfig requires `#[uci(package = \"...\")]`",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section_name(field: &Field) -> syn::Result<LitStr> {
|
||||||
|
let field_ident = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("named fields always have identifiers");
|
||||||
|
|
||||||
|
let default_name = field_ident.unraw().to_string();
|
||||||
|
let default_name = LitStr::new(&default_name, field_ident.span());
|
||||||
|
|
||||||
|
let mut section = None;
|
||||||
|
|
||||||
|
for attribute in &field.attrs {
|
||||||
|
if !attribute.path().is_ident("uci") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute.parse_nested_meta(|meta| {
|
||||||
|
if meta.path.is_ident("section") {
|
||||||
|
if section.is_some() {
|
||||||
|
return Err(meta.error("duplicate `section` attribute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.input.peek(Token![=]) {
|
||||||
|
let value: LitStr = meta.value()?.parse()?;
|
||||||
|
|
||||||
|
if value.value().is_empty() {
|
||||||
|
return Err(syn::Error::new(value.span(), "`section` cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
section = Some(value);
|
||||||
|
} else {
|
||||||
|
section = Some(default_name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(meta.error("expected `section` or `section = \"...\"`"))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.ok_or_else(|| {
|
||||||
|
syn::Error::new_spanned(
|
||||||
|
field,
|
||||||
|
"UciConfig fields require `#[uci(section)]` \
|
||||||
|
or `#[uci(section = \"...\")]`",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,5 +2,18 @@
|
|||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:recommended"
|
"config:recommended"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "Keep netlink-bindings and netlink-socket2 updates together",
|
||||||
|
"matchManagers": [
|
||||||
|
"cargo"
|
||||||
|
],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"netlink-bindings",
|
||||||
|
"netlink-socket2"
|
||||||
|
],
|
||||||
|
"groupName": "netlink dependencies"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+394
-107
@@ -1,30 +1,81 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
use crate::Config;
|
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::pinger::PingReply;
|
use crate::pinger::PingReply;
|
||||||
use crate::util::MutexExt;
|
use crate::settings::Settings;
|
||||||
use log::info;
|
use flume::{Receiver, Sender};
|
||||||
|
use log::{debug, info};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::{Receiver, Sender};
|
use std::time::Duration;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
const STALE_AFTER: Duration = Duration::from_secs(30);
|
||||||
pub struct ReflectorStats {
|
const ANOMALY_THRESHOLD_MS: f64 = 5_000.0;
|
||||||
pub down_ewma: f64,
|
const BASELINE_HALF_LIFE_SECS: f64 = 135.0;
|
||||||
pub up_ewma: f64,
|
const RECENT_HALF_LIFE_SECS: f64 = 0.4;
|
||||||
pub last_receive_time_s: Instant,
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct EwmaStats {
|
||||||
|
pub down: f64,
|
||||||
|
pub up: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EwmaStats {
|
||||||
|
fn from_reply(reply: &PingReply) -> Self {
|
||||||
|
Self {
|
||||||
|
down: reply.down_time,
|
||||||
|
up: reply.up_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, sample: Self, factor: f64) {
|
||||||
|
self.down = self.down * factor + sample.down * (1.0 - factor);
|
||||||
|
self.up = self.up * factor + sample.up * (1.0 - factor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct ReflectorState {
|
||||||
|
pub baseline: EwmaStats,
|
||||||
|
pub recent: EwmaStats,
|
||||||
|
pub last_receive_at: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReflectorState {
|
||||||
|
fn new(sample: EwmaStats, received_at: Instant) -> Self {
|
||||||
|
Self {
|
||||||
|
baseline: sample,
|
||||||
|
recent: sample,
|
||||||
|
last_receive_at: received_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self, sample: EwmaStats, received_at: Instant) {
|
||||||
|
*self = Self::new(sample, received_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ControlSnapshot {
|
||||||
|
pub generated_at: Instant,
|
||||||
|
pub reflectors: HashMap<IpAddr, ReflectorState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Baseliner {
|
pub struct Baseliner {
|
||||||
pub config: Config,
|
pub settings: Settings,
|
||||||
pub owd_baseline: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
|
||||||
pub owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
|
||||||
pub reselect_trigger: Sender<bool>,
|
pub reselect_trigger: Sender<bool>,
|
||||||
pub start_time: Instant,
|
pub start_time: Instant,
|
||||||
pub stats_rx: Receiver<PingReply>,
|
pub stats_rx: Receiver<PingReply>,
|
||||||
|
pub control_tx: Sender<ControlSnapshot>,
|
||||||
|
pub selection_tx: Option<Sender<ControlSnapshot>>,
|
||||||
pub baseline_metrics: MetricsSender,
|
pub baseline_metrics: MetricsSender,
|
||||||
pub event_metrics: MetricsSender,
|
pub event_metrics: MetricsSender,
|
||||||
}
|
}
|
||||||
@@ -33,127 +84,363 @@ fn ewma_factor(tick: f64, dur: f64) -> f64 {
|
|||||||
((0.5_f64).ln() / (dur / tick)).exp()
|
((0.5_f64).ln() / (dur / tick)).exp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_reflector_state(
|
||||||
|
state: &mut ReflectorState,
|
||||||
|
sample: EwmaStats,
|
||||||
|
received_at: Instant,
|
||||||
|
start_time: Instant,
|
||||||
|
slow_factor: f64,
|
||||||
|
fast_factor: f64,
|
||||||
|
) -> bool {
|
||||||
|
if received_at.saturating_duration_since(state.last_receive_at) > STALE_AFTER {
|
||||||
|
state.reset(sample, received_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample.up > state.baseline.up + ANOMALY_THRESHOLD_MS
|
||||||
|
|| sample.down > state.baseline.down + ANOMALY_THRESHOLD_MS
|
||||||
|
{
|
||||||
|
state.last_receive_at = start_time;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.baseline.update(sample, slow_factor);
|
||||||
|
state.recent.update(sample, fast_factor);
|
||||||
|
|
||||||
|
state.baseline.down = state.baseline.down.min(state.recent.down);
|
||||||
|
state.baseline.up = state.baseline.up.min(state.recent.up);
|
||||||
|
state.last_receive_at = received_at;
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_reply(
|
||||||
|
reflectors: &mut HashMap<IpAddr, ReflectorState>,
|
||||||
|
reply: &PingReply,
|
||||||
|
start_time: Instant,
|
||||||
|
slow_factor: f64,
|
||||||
|
fast_factor: f64,
|
||||||
|
) -> (ReflectorState, bool) {
|
||||||
|
let sample = EwmaStats::from_reply(reply);
|
||||||
|
let state = reflectors
|
||||||
|
.entry(reply.reflector)
|
||||||
|
.or_insert_with(|| ReflectorState::new(sample, reply.last_receive_time_s));
|
||||||
|
let anomaly = update_reflector_state(
|
||||||
|
state,
|
||||||
|
sample,
|
||||||
|
reply.last_receive_time_s,
|
||||||
|
start_time,
|
||||||
|
slow_factor,
|
||||||
|
fast_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
(*state, anomaly)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_snapshot(
|
||||||
|
reflectors: &HashMap<IpAddr, ReflectorState>,
|
||||||
|
generated_at: Instant,
|
||||||
|
) -> ControlSnapshot {
|
||||||
|
ControlSnapshot {
|
||||||
|
generated_at,
|
||||||
|
reflectors: reflectors.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Baseliner {
|
impl Baseliner {
|
||||||
pub fn run(&self) -> anyhow::Result<()> {
|
pub fn run(&self) -> anyhow::Result<()> {
|
||||||
/*
|
let mut reflectors = HashMap::<IpAddr, ReflectorState>::new();
|
||||||
* 135 seconds to decay to 50% for the slow factor and
|
|
||||||
* 0.4 seconds to decay to 50% for the fast factor.
|
let slow_factor = ewma_factor(
|
||||||
* The fast one can be adjusted to tune, try anything from 0.01 to 3.0 to get more or less sensitivity
|
self.settings.advanced_settings.tick_interval,
|
||||||
* with more sensitivity we respond faster to bloat, but are at risk from triggering due to lag spikes that
|
BASELINE_HALF_LIFE_SECS,
|
||||||
* aren't bloat related, with less sensitivity (bigger numbers) we smooth through quick spikes
|
);
|
||||||
* but take longer to respond to real bufferbloat
|
let fast_factor = ewma_factor(
|
||||||
*/
|
self.settings.advanced_settings.tick_interval,
|
||||||
let slow_factor = ewma_factor(self.config.tick_interval, 135.0);
|
RECENT_HALF_LIFE_SECS,
|
||||||
let fast_factor = ewma_factor(self.config.tick_interval, 0.4);
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||||
info!("Baseliner shutting down");
|
info!("Baseliner shutting down");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let time_data = self.stats_rx.recv()?;
|
|
||||||
|
|
||||||
let mut owd_baseline_map = self.owd_baseline.lock_anyhow()?;
|
let reply = self.stats_rx.recv()?;
|
||||||
let mut owd_recent_map = self.owd_recent.lock_anyhow()?;
|
let reflector = reply.reflector;
|
||||||
|
let (state, anomaly) = process_reply(
|
||||||
|
&mut reflectors,
|
||||||
|
&reply,
|
||||||
|
self.start_time,
|
||||||
|
slow_factor,
|
||||||
|
fast_factor,
|
||||||
|
);
|
||||||
|
|
||||||
let owd_baseline_new = ReflectorStats {
|
if anomaly {
|
||||||
down_ewma: time_data.down_time,
|
|
||||||
up_ewma: time_data.up_time,
|
|
||||||
last_receive_time_s: time_data.last_receive_time_s,
|
|
||||||
};
|
|
||||||
|
|
||||||
let owd_recent_new = ReflectorStats {
|
|
||||||
down_ewma: time_data.down_time,
|
|
||||||
up_ewma: time_data.up_time,
|
|
||||||
last_receive_time_s: time_data.last_receive_time_s,
|
|
||||||
};
|
|
||||||
|
|
||||||
let owd_baseline = owd_baseline_map
|
|
||||||
.entry(time_data.reflector)
|
|
||||||
.or_insert(owd_baseline_new);
|
|
||||||
|
|
||||||
let owd_recent = owd_recent_map
|
|
||||||
.entry(time_data.reflector)
|
|
||||||
.or_insert(owd_recent_new);
|
|
||||||
|
|
||||||
if time_data
|
|
||||||
.last_receive_time_s
|
|
||||||
.duration_since(owd_baseline.last_receive_time_s)
|
|
||||||
.as_secs_f64()
|
|
||||||
> 30.0
|
|
||||||
|| time_data
|
|
||||||
.last_receive_time_s
|
|
||||||
.duration_since(owd_recent.last_receive_time_s)
|
|
||||||
.as_secs_f64()
|
|
||||||
> 30.0
|
|
||||||
{
|
|
||||||
owd_baseline.down_ewma = time_data.down_time;
|
|
||||||
owd_baseline.up_ewma = time_data.up_time;
|
|
||||||
owd_baseline.last_receive_time_s = time_data.last_receive_time_s;
|
|
||||||
owd_recent.down_ewma = time_data.down_time;
|
|
||||||
owd_recent.up_ewma = time_data.up_time;
|
|
||||||
owd_recent.last_receive_time_s = time_data.last_receive_time_s;
|
|
||||||
}
|
|
||||||
|
|
||||||
owd_baseline.last_receive_time_s = time_data.last_receive_time_s;
|
|
||||||
owd_recent.last_receive_time_s = time_data.last_receive_time_s;
|
|
||||||
|
|
||||||
// if this reflection is more than 5 seconds higher than baseline... mark it no good and trigger a reselection
|
|
||||||
if time_data.up_time > owd_baseline.up_ewma + 5000.0
|
|
||||||
|| time_data.down_time > owd_baseline.down_ewma + 5000.0
|
|
||||||
{
|
|
||||||
// mark the data as bad by setting the receive time to the time autorate was started
|
|
||||||
owd_baseline.last_receive_time_s = self.start_time;
|
|
||||||
owd_recent.last_receive_time_s = self.start_time;
|
|
||||||
info!(
|
info!(
|
||||||
"Reflector {} has OWD > 5 seconds more than baseline, triggering reselection",
|
"Reflector {} has OWD > 5 seconds more than baseline, triggering reselection",
|
||||||
time_data.reflector
|
reflector
|
||||||
);
|
);
|
||||||
self.event_metrics.send(Metric::Event {
|
self.event_metrics.send(Metric::Event {
|
||||||
name: "reselection",
|
name: "reselection",
|
||||||
reason: "anomaly",
|
reason: "anomaly",
|
||||||
reflector: Some(time_data.reflector),
|
reflector: Some(reflector),
|
||||||
tags: &[],
|
tags: &[],
|
||||||
});
|
});
|
||||||
// If reselection is disabled this would trigger an error
|
let _ = self.reselect_trigger.try_send(true);
|
||||||
// so just ignore the result
|
|
||||||
let _ = self.reselect_trigger.send(true);
|
|
||||||
} else {
|
|
||||||
owd_baseline.down_ewma = owd_baseline.down_ewma * slow_factor
|
|
||||||
+ (1.0 - slow_factor) * time_data.down_time;
|
|
||||||
owd_baseline.up_ewma =
|
|
||||||
owd_baseline.up_ewma * slow_factor + (1.0 - slow_factor) * time_data.up_time;
|
|
||||||
|
|
||||||
owd_recent.down_ewma =
|
|
||||||
owd_recent.down_ewma * fast_factor + (1.0 - fast_factor) * time_data.down_time;
|
|
||||||
owd_recent.up_ewma =
|
|
||||||
owd_recent.up_ewma * fast_factor + (1.0 - fast_factor) * time_data.up_time;
|
|
||||||
|
|
||||||
if owd_baseline.down_ewma > owd_recent.down_ewma {
|
|
||||||
owd_baseline.down_ewma = owd_recent.down_ewma;
|
|
||||||
}
|
|
||||||
|
|
||||||
if owd_baseline.up_ewma > owd_recent.up_ewma {
|
|
||||||
owd_baseline.up_ewma = owd_recent.up_ewma;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.baseline_metrics.send(Metric::Baseline {
|
self.baseline_metrics.send(Metric::Baseline {
|
||||||
reflector: time_data.reflector,
|
reflector,
|
||||||
baseline_up_ewma: owd_baseline.up_ewma,
|
baseline_up_ewma: state.baseline.up,
|
||||||
baseline_down_ewma: owd_baseline.down_ewma,
|
baseline_down_ewma: state.baseline.down,
|
||||||
recent_up_ewma: owd_recent.up_ewma,
|
recent_up_ewma: state.recent.up,
|
||||||
recent_down_ewma: owd_recent.down_ewma,
|
recent_down_ewma: state.recent.down,
|
||||||
});
|
});
|
||||||
|
|
||||||
info!(
|
debug!(
|
||||||
"Reflector {} up baseline = {} down baseline = {}",
|
"Reflector {} up baseline = {} down baseline = {}",
|
||||||
time_data.reflector, owd_baseline.up_ewma, owd_baseline.down_ewma
|
reflector, state.baseline.up, state.baseline.down
|
||||||
);
|
);
|
||||||
info!(
|
debug!(
|
||||||
"Reflector {} up recent = {} down recent = {}",
|
"Reflector {} up recent = {} down recent = {}",
|
||||||
time_data.reflector, owd_recent.up_ewma, owd_recent.down_ewma
|
reflector, state.recent.up, state.recent.down
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = control_snapshot(&reflectors, Instant::now());
|
||||||
|
if let Some(selection_tx) = &self.selection_tx {
|
||||||
|
let _ = selection_tx.send(snapshot.clone());
|
||||||
|
}
|
||||||
|
self.control_tx.send(snapshot)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::settings::MeasurementType;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn state(
|
||||||
|
baseline_down: f64,
|
||||||
|
baseline_up: f64,
|
||||||
|
recent_down: f64,
|
||||||
|
recent_up: f64,
|
||||||
|
last_receive_at: Instant,
|
||||||
|
) -> ReflectorState {
|
||||||
|
ReflectorState {
|
||||||
|
baseline: EwmaStats {
|
||||||
|
down: baseline_down,
|
||||||
|
up: baseline_up,
|
||||||
|
},
|
||||||
|
recent: EwmaStats {
|
||||||
|
down: recent_down,
|
||||||
|
up: recent_up,
|
||||||
|
},
|
||||||
|
last_receive_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample(down: f64, up: f64) -> EwmaStats {
|
||||||
|
EwmaStats { down, up }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reply(reflector: IpAddr, down_time: f64, up_time: f64, received_at: Instant) -> PingReply {
|
||||||
|
PingReply {
|
||||||
|
reflector,
|
||||||
|
measurement_type: MeasurementType::Icmp,
|
||||||
|
seq: 0,
|
||||||
|
rtt: down_time + up_time,
|
||||||
|
current_time: 0,
|
||||||
|
down_time,
|
||||||
|
up_time,
|
||||||
|
originate_timestamp: 0,
|
||||||
|
receive_timestamp: 0,
|
||||||
|
transmit_timestamp: 0,
|
||||||
|
last_receive_time_s: received_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_close(actual: f64, expected: f64) {
|
||||||
|
assert!(
|
||||||
|
(actual - expected).abs() < 1e-9,
|
||||||
|
"expected {expected}, got {actual}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn updates_ewmas() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let receive_time = start + Duration::from_secs(1);
|
||||||
|
let mut state = state(100.0, 100.0, 100.0, 100.0, start);
|
||||||
|
let anomaly = update_reflector_state(
|
||||||
|
&mut state,
|
||||||
|
sample(200.0, 300.0),
|
||||||
|
receive_time,
|
||||||
|
start,
|
||||||
|
0.9,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!anomaly);
|
||||||
|
assert_close(state.baseline.down, 110.0);
|
||||||
|
assert_close(state.baseline.up, 120.0);
|
||||||
|
assert_close(state.recent.down, 150.0);
|
||||||
|
assert_close(state.recent.up, 200.0);
|
||||||
|
assert_eq!(state.last_receive_at, receive_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_baseline_at_or_below_recent() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut state = state(200.0, 200.0, 100.0, 100.0, start);
|
||||||
|
let anomaly = update_reflector_state(
|
||||||
|
&mut state,
|
||||||
|
sample(100.0, 100.0),
|
||||||
|
start + Duration::from_secs(1),
|
||||||
|
start,
|
||||||
|
0.9,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!anomaly);
|
||||||
|
assert_close(state.baseline.down, 100.0);
|
||||||
|
assert_close(state.baseline.up, 100.0);
|
||||||
|
assert_close(state.recent.down, 100.0);
|
||||||
|
assert_close(state.recent.up, 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resets_stats_after_a_long_gap() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let receive_time = start + Duration::from_secs(31);
|
||||||
|
let mut state = state(100.0, 200.0, 300.0, 400.0, start);
|
||||||
|
let anomaly = update_reflector_state(
|
||||||
|
&mut state,
|
||||||
|
sample(500.0, 600.0),
|
||||||
|
receive_time,
|
||||||
|
start,
|
||||||
|
0.9,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!anomaly);
|
||||||
|
assert_close(state.baseline.down, 500.0);
|
||||||
|
assert_close(state.baseline.up, 600.0);
|
||||||
|
assert_close(state.recent.down, 500.0);
|
||||||
|
assert_close(state.recent.up, 600.0);
|
||||||
|
assert_eq!(state.last_receive_at, receive_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_reset_at_exactly_thirty_seconds() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let receive_time = start + STALE_AFTER;
|
||||||
|
let mut state = state(100.0, 200.0, 100.0, 200.0, start);
|
||||||
|
let anomaly = update_reflector_state(
|
||||||
|
&mut state,
|
||||||
|
sample(200.0, 300.0),
|
||||||
|
receive_time,
|
||||||
|
start,
|
||||||
|
0.9,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!anomaly);
|
||||||
|
assert_close(state.baseline.down, 110.0);
|
||||||
|
assert_close(state.baseline.up, 210.0);
|
||||||
|
assert_close(state.recent.down, 150.0);
|
||||||
|
assert_close(state.recent.up, 250.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn marks_anomalies_for_reselection() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut state = state(100.0, 200.0, 300.0, 400.0, start);
|
||||||
|
let anomaly = update_reflector_state(
|
||||||
|
&mut state,
|
||||||
|
sample(5_101.0, 200.0),
|
||||||
|
start + Duration::from_secs(1),
|
||||||
|
start,
|
||||||
|
0.9,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(anomaly);
|
||||||
|
assert_close(state.baseline.down, 100.0);
|
||||||
|
assert_close(state.baseline.up, 200.0);
|
||||||
|
assert_close(state.recent.down, 300.0);
|
||||||
|
assert_close(state.recent.up, 400.0);
|
||||||
|
assert_eq!(state.last_receive_at, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_mark_exact_anomaly_threshold() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let receive_time = start + Duration::from_secs(1);
|
||||||
|
let mut state = state(100.0, 200.0, 100.0, 200.0, start);
|
||||||
|
let anomaly = update_reflector_state(
|
||||||
|
&mut state,
|
||||||
|
sample(5_100.0, 5_200.0),
|
||||||
|
receive_time,
|
||||||
|
start,
|
||||||
|
0.9,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!anomaly);
|
||||||
|
assert_eq!(state.last_receive_at, receive_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn processed_reply_updates_combined_reflector_state() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let received_at = start + Duration::from_secs(1);
|
||||||
|
let reflector = "192.0.2.1".parse().unwrap();
|
||||||
|
let mut reflectors = HashMap::new();
|
||||||
|
reflectors.insert(reflector, state(100.0, 100.0, 100.0, 100.0, start));
|
||||||
|
|
||||||
|
let (updated, anomaly) = process_reply(
|
||||||
|
&mut reflectors,
|
||||||
|
&reply(reflector, 200.0, 300.0, received_at),
|
||||||
|
start,
|
||||||
|
0.9,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!anomaly);
|
||||||
|
assert_close(updated.baseline.down, 110.0);
|
||||||
|
assert_close(updated.baseline.up, 120.0);
|
||||||
|
assert_close(updated.recent.down, 150.0);
|
||||||
|
assert_close(updated.recent.up, 200.0);
|
||||||
|
assert_eq!(updated.last_receive_at, received_at);
|
||||||
|
assert_close(reflectors[&reflector].recent.up, 200.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_contains_updated_combined_state() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let received_at = start + Duration::from_secs(1);
|
||||||
|
let generated_at = received_at + Duration::from_millis(1);
|
||||||
|
let reflector = "192.0.2.1".parse().unwrap();
|
||||||
|
let mut reflectors = HashMap::new();
|
||||||
|
|
||||||
|
let (updated, anomaly) = process_reply(
|
||||||
|
&mut reflectors,
|
||||||
|
&reply(reflector, 20.0, 30.0, received_at),
|
||||||
|
start,
|
||||||
|
0.9,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
let snapshot = control_snapshot(&reflectors, generated_at);
|
||||||
|
|
||||||
|
assert!(!anomaly);
|
||||||
|
assert_eq!(snapshot.generated_at, generated_at);
|
||||||
|
assert_close(
|
||||||
|
snapshot.reflectors[&reflector].baseline.down,
|
||||||
|
updated.baseline.down,
|
||||||
|
);
|
||||||
|
assert_close(snapshot.reflectors[&reflector].recent.up, updated.recent.up);
|
||||||
|
assert_eq!(snapshot.reflectors[&reflector].last_receive_at, received_at);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-413
@@ -1,413 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use log::Level;
|
|
||||||
#[cfg(feature = "uci")]
|
|
||||||
use log::warn;
|
|
||||||
#[cfg(feature = "uci")]
|
|
||||||
use rust_uci::Uci;
|
|
||||||
use std::fmt;
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::BufRead;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{env, io};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum ConfigError {
|
|
||||||
#[error("Invalid measurement type")]
|
|
||||||
InvalidMeasurementType(String),
|
|
||||||
#[error("Couldn't parse value for key: `{0}`: invalid value")]
|
|
||||||
ParseError(String),
|
|
||||||
#[error("No config value found for key: `{0}`")]
|
|
||||||
MissingValue(String),
|
|
||||||
#[error("Reflector list not found at: {0}")]
|
|
||||||
ReflectorListNotFound(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
|
||||||
where
|
|
||||||
P: AsRef<Path>,
|
|
||||||
{
|
|
||||||
let file = File::open(filename)?;
|
|
||||||
Ok(io::BufReader::new(file).lines())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub enum MeasurementType {
|
|
||||||
Icmp = 1,
|
|
||||||
IcmpTimestamps,
|
|
||||||
Ntp,
|
|
||||||
TcpTimestamps,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for MeasurementType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
MeasurementType::Icmp => write!(f, "icmp"),
|
|
||||||
MeasurementType::IcmpTimestamps => write!(f, "icmp-timestamps"),
|
|
||||||
MeasurementType::Ntp => write!(f, "ntp"),
|
|
||||||
MeasurementType::TcpTimestamps => write!(f, "tcp-timestamps"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for MeasurementType {
|
|
||||||
type Err = ConfigError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
return match s.to_lowercase().as_str() {
|
|
||||||
"icmp" => Ok(MeasurementType::Icmp),
|
|
||||||
"icmp-timestamps" => Ok(MeasurementType::IcmpTimestamps),
|
|
||||||
"ntp" => Ok(MeasurementType::Ntp),
|
|
||||||
"tcp-timestamps" => Ok(MeasurementType::TcpTimestamps),
|
|
||||||
&_ => Err(ConfigError::InvalidMeasurementType(s.to_string())),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub enum ObservabilityProtocol {
|
|
||||||
Udp,
|
|
||||||
Tcp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ObservabilityProtocol {
|
|
||||||
type Err = ConfigError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.to_lowercase().as_str() {
|
|
||||||
"udp" => Ok(ObservabilityProtocol::Udp),
|
|
||||||
"tcp" => Ok(ObservabilityProtocol::Tcp),
|
|
||||||
_ => Err(ConfigError::InvalidMeasurementType(s.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
// Network section
|
|
||||||
pub download_interface: String,
|
|
||||||
pub upload_interface: String,
|
|
||||||
pub download_base_kbits: f64,
|
|
||||||
pub download_min_percent: f64,
|
|
||||||
pub download_min_kbits: f64,
|
|
||||||
pub upload_base_kbits: f64,
|
|
||||||
pub upload_min_percent: f64,
|
|
||||||
pub upload_min_kbits: f64,
|
|
||||||
|
|
||||||
// Output section
|
|
||||||
pub log_level: Level,
|
|
||||||
pub speed_hist_file: String,
|
|
||||||
pub stats_file: String,
|
|
||||||
pub suppress_statistics: bool,
|
|
||||||
|
|
||||||
// Observability section
|
|
||||||
pub observability_enabled: bool,
|
|
||||||
pub observability_protocol: ObservabilityProtocol,
|
|
||||||
pub observability_host: Option<String>,
|
|
||||||
pub observability_port: u16,
|
|
||||||
pub observability_batch_size: usize,
|
|
||||||
pub observability_batch_timeout_ms: u64,
|
|
||||||
pub observability_export_ping_metrics: bool,
|
|
||||||
pub observability_export_rate_metrics: bool,
|
|
||||||
pub observability_export_baseline_metrics: bool,
|
|
||||||
pub observability_export_events: bool,
|
|
||||||
pub observability_host_tag: String,
|
|
||||||
|
|
||||||
// Advanced section
|
|
||||||
pub download_delay_ms: f64,
|
|
||||||
pub high_load_level: f64,
|
|
||||||
pub min_change_interval: f64,
|
|
||||||
pub measurement_type: MeasurementType,
|
|
||||||
pub num_reflectors: u8,
|
|
||||||
pub peer_reselection_time: u64,
|
|
||||||
pub reflector_list_file: String,
|
|
||||||
pub speed_hist_size: u32,
|
|
||||||
pub tick_interval: f64,
|
|
||||||
pub upload_delay_ms: f64,
|
|
||||||
|
|
||||||
// Monitoring / dry-run
|
|
||||||
pub dry_run: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
let config = Self {
|
|
||||||
// Network section
|
|
||||||
download_base_kbits: Self::get::<f64>(
|
|
||||||
"SQMA_DOWNLOAD_BASE_KBITS",
|
|
||||||
"sqm-autorate.@network[0].download_base_kbits",
|
|
||||||
None,
|
|
||||||
)?,
|
|
||||||
download_interface: Self::get::<String>(
|
|
||||||
"SQMA_DOWNLOAD_INTERFACE",
|
|
||||||
"sqm-autorate.@network[0].download_interface",
|
|
||||||
None,
|
|
||||||
)?,
|
|
||||||
download_min_percent: 0.0, // placeholder, computed below
|
|
||||||
download_min_kbits: 0.0, // placeholder, computed below
|
|
||||||
upload_base_kbits: Self::get::<f64>(
|
|
||||||
"SQMA_UPLOAD_BASE_KBITS",
|
|
||||||
"sqm-autorate.@network[0].upload_base_kbits",
|
|
||||||
None,
|
|
||||||
)?,
|
|
||||||
upload_interface: Self::get::<String>(
|
|
||||||
"SQMA_UPLOAD_INTERFACE",
|
|
||||||
"sqm-autorate.@network[0].upload_interface",
|
|
||||||
None,
|
|
||||||
)?,
|
|
||||||
upload_min_percent: 0.0, // placeholder, computed below
|
|
||||||
upload_min_kbits: 0.0, // placeholder, computed below
|
|
||||||
// Output section
|
|
||||||
log_level: Self::get::<Level>(
|
|
||||||
"SQMA_LOG_LEVEL",
|
|
||||||
"sqm-autorate.@output[0].log_level",
|
|
||||||
Some(Level::Error),
|
|
||||||
)?,
|
|
||||||
speed_hist_file: Self::get::<String>(
|
|
||||||
"SQMA_SPEED_HIST_FILE",
|
|
||||||
"sqm-autorate.@output[0].speed_hist_file",
|
|
||||||
Some("/tmp/sqm-speedhist.csv".parse()?),
|
|
||||||
)?,
|
|
||||||
stats_file: Self::get::<String>(
|
|
||||||
"SQMA_STATS_FILE",
|
|
||||||
"sqm-autorate.@output[0].stats_file",
|
|
||||||
Some("/tmp/sqm-autorate.csv".parse()?),
|
|
||||||
)?,
|
|
||||||
suppress_statistics: Self::get::<bool>(
|
|
||||||
"SQMA_SUPPRESS_STATISTICS",
|
|
||||||
"sqm-autorate.@output[0].suppress_statistics",
|
|
||||||
Some(false),
|
|
||||||
)?,
|
|
||||||
|
|
||||||
// Observability section
|
|
||||||
observability_enabled: Self::get::<bool>(
|
|
||||||
"SQMA_OBSERVABILITY_ENABLED",
|
|
||||||
"sqm-autorate.@observability[0].enabled",
|
|
||||||
Some(false),
|
|
||||||
)?,
|
|
||||||
observability_protocol: Self::get::<ObservabilityProtocol>(
|
|
||||||
"SQMA_OBSERVABILITY_PROTOCOL",
|
|
||||||
"sqm-autorate.@observability[0].protocol",
|
|
||||||
Some(ObservabilityProtocol::Udp),
|
|
||||||
)?,
|
|
||||||
observability_host: Self::get_optional::<String>(
|
|
||||||
"SQMA_OBSERVABILITY_HOST",
|
|
||||||
"sqm-autorate.@observability[0].host",
|
|
||||||
),
|
|
||||||
observability_port: Self::get::<u16>(
|
|
||||||
"SQMA_OBSERVABILITY_PORT",
|
|
||||||
"sqm-autorate.@observability[0].port",
|
|
||||||
Some(8089),
|
|
||||||
)?,
|
|
||||||
observability_batch_size: Self::get::<usize>(
|
|
||||||
"SQMA_OBSERVABILITY_BATCH_SIZE",
|
|
||||||
"sqm-autorate.@observability[0].batch_size",
|
|
||||||
Some(25),
|
|
||||||
)?,
|
|
||||||
observability_batch_timeout_ms: Self::get::<u64>(
|
|
||||||
"SQMA_OBSERVABILITY_BATCH_TIMEOUT_MS",
|
|
||||||
"sqm-autorate.@observability[0].batch_timeout_ms",
|
|
||||||
Some(100),
|
|
||||||
)?,
|
|
||||||
observability_export_ping_metrics: Self::get::<bool>(
|
|
||||||
"SQMA_OBSERVABILITY_EXPORT_PING_METRICS",
|
|
||||||
"sqm-autorate.@observability[0].export_ping_metrics",
|
|
||||||
Some(false),
|
|
||||||
)?,
|
|
||||||
observability_export_rate_metrics: Self::get::<bool>(
|
|
||||||
"SQMA_OBSERVABILITY_EXPORT_RATE_METRICS",
|
|
||||||
"sqm-autorate.@observability[0].export_rate_metrics",
|
|
||||||
Some(true),
|
|
||||||
)?,
|
|
||||||
observability_export_baseline_metrics: Self::get::<bool>(
|
|
||||||
"SQMA_OBSERVABILITY_EXPORT_BASELINE_METRICS",
|
|
||||||
"sqm-autorate.@observability[0].export_baseline_metrics",
|
|
||||||
Some(false),
|
|
||||||
)?,
|
|
||||||
observability_export_events: Self::get::<bool>(
|
|
||||||
"SQMA_OBSERVABILITY_EXPORT_EVENTS",
|
|
||||||
"sqm-autorate.@observability[0].export_events",
|
|
||||||
Some(true),
|
|
||||||
)?,
|
|
||||||
observability_host_tag: Self::get_host_tag(),
|
|
||||||
|
|
||||||
// Advanced section
|
|
||||||
download_delay_ms: Self::get::<f64>(
|
|
||||||
"SQMA_DOWNLOAD_DELAY_MS",
|
|
||||||
"sqm-autorate.@advanced_settings[0].download_delay_ms",
|
|
||||||
Some(15.0),
|
|
||||||
)?,
|
|
||||||
high_load_level: Self::get::<f64>(
|
|
||||||
"SQMA_HIGH_LOAD_LEVEL",
|
|
||||||
"sqm-autorate.@advanced_settings[0].high_load_level",
|
|
||||||
Some(0.8),
|
|
||||||
)?,
|
|
||||||
measurement_type: Self::get::<MeasurementType>(
|
|
||||||
"SQMA_MEASUREMENT_TYPE",
|
|
||||||
"sqm-autorate.@advanced_settings[0].measurement_type",
|
|
||||||
Some(MeasurementType::IcmpTimestamps),
|
|
||||||
)?,
|
|
||||||
min_change_interval: Self::get::<f64>(
|
|
||||||
"SQMA_MIN_CHANGE_INTERVAL",
|
|
||||||
"sqm-autorate.@advanced_settings[0].min_change_interval",
|
|
||||||
Some(0.5),
|
|
||||||
)?,
|
|
||||||
num_reflectors: Self::get::<u8>(
|
|
||||||
"SQMA_NUM_REFLECTORS",
|
|
||||||
"sqm-autorate.@advanced_settings[0].num_reflectors",
|
|
||||||
Some(5),
|
|
||||||
)?,
|
|
||||||
peer_reselection_time: Self::get::<u64>(
|
|
||||||
"SQMA_PEER_RESELECTION_TIME",
|
|
||||||
"sqm-autorate.@advanced_settings[0].peer_reselection_time",
|
|
||||||
Some(15),
|
|
||||||
)?,
|
|
||||||
reflector_list_file: Self::get::<String>(
|
|
||||||
"SQMA_REFLECTOR_LIST_FILE",
|
|
||||||
"sqm-autorate.@advanced_settings[0].reflector_list_file",
|
|
||||||
Some("/etc/sqm-autorate/reflectors-icmp.csv".parse()?),
|
|
||||||
)?,
|
|
||||||
speed_hist_size: Self::get::<u32>(
|
|
||||||
"SQMA_SPEED_HIST_SIZE",
|
|
||||||
"sqm-autorate.@advanced_settings[0].speed_hist_size",
|
|
||||||
Some(100),
|
|
||||||
)?,
|
|
||||||
tick_interval: Self::get::<f64>(
|
|
||||||
"SQMA_TICK_INTERVAL",
|
|
||||||
"sqm-autorate.@advanced_settings[0].tick_interval",
|
|
||||||
Some(0.5),
|
|
||||||
)?,
|
|
||||||
upload_delay_ms: Self::get::<f64>(
|
|
||||||
"SQMA_UPLOAD_DELAY_MS",
|
|
||||||
"sqm-autorate.@advanced_settings[0].upload_delay_ms",
|
|
||||||
Some(15.0),
|
|
||||||
)?,
|
|
||||||
dry_run: Self::get::<bool>(
|
|
||||||
"SQMA_DRY_RUN",
|
|
||||||
"sqm-autorate.@advanced_settings[0].dry_run",
|
|
||||||
Some(false),
|
|
||||||
)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut config = config;
|
|
||||||
|
|
||||||
config.download_min_percent = Self::get::<f64>(
|
|
||||||
"SQMA_DOWNLOAD_MIN_PERCENT",
|
|
||||||
"sqm-autorate.@network[0].download_min_percent",
|
|
||||||
Some(20.0),
|
|
||||||
)?
|
|
||||||
.clamp(1.0, 80.0);
|
|
||||||
|
|
||||||
config.upload_min_percent = Self::get::<f64>(
|
|
||||||
"SQMA_UPLOAD_MIN_PERCENT",
|
|
||||||
"sqm-autorate.@network[0].upload_min_percent",
|
|
||||||
Some(20.0),
|
|
||||||
)?
|
|
||||||
.clamp(1.0, 80.0);
|
|
||||||
|
|
||||||
config.download_min_kbits =
|
|
||||||
(config.download_base_kbits * config.download_min_percent / 100.0).floor();
|
|
||||||
config.upload_min_kbits =
|
|
||||||
(config.upload_base_kbits * config.upload_min_percent / 100.0).floor();
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get<T: FromStr>(env_key: &str, uci_key: &str, default: Option<T>) -> Result<T, ConfigError> {
|
|
||||||
match Self::get_value(env_key, uci_key) {
|
|
||||||
Some(val) => match val.parse::<T>() {
|
|
||||||
Ok(parsed_val) => Ok(parsed_val),
|
|
||||||
// Ran into an compilation error while trying to return the
|
|
||||||
// error as-is, so using my own error type to indicate something went wrong while parsing
|
|
||||||
Err(_) => Err(ConfigError::ParseError(env_key.to_string())),
|
|
||||||
},
|
|
||||||
None => match default {
|
|
||||||
Some(val) => Ok(val),
|
|
||||||
None => Err(ConfigError::MissingValue(env_key.to_string())),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_host_tag() -> String {
|
|
||||||
if let Ok(val) = env::var("SQMA_OBSERVABILITY_HOST_TAG") {
|
|
||||||
if !val.is_empty() {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// rustix can give us the hostname without shelling out
|
|
||||||
rustix::system::uname()
|
|
||||||
.nodename()
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_optional<T: FromStr>(env_key: &str, uci_key: &str) -> Option<T> {
|
|
||||||
Self::get_value(env_key, uci_key).and_then(|val| val.parse::<T>().ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_value(env_key: &str, uci_key: &str) -> Option<String> {
|
|
||||||
if let Ok(val) = env::var(env_key) {
|
|
||||||
return Some(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(val) = Self::get_from_uci(uci_key) {
|
|
||||||
return Some(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "uci")]
|
|
||||||
fn get_from_uci(key: &str) -> Option<String> {
|
|
||||||
let mut uci = match Uci::new() {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error opening UCI instance: {}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return match uci.get(key) {
|
|
||||||
Ok(val) => Some(val),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Problem getting config from UCI: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "uci"))]
|
|
||||||
fn get_from_uci(_: &str) -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_reflectors(&self) -> Result<Vec<IpAddr>> {
|
|
||||||
let lines = read_lines(self.reflector_list_file.clone()).map_err(|e| {
|
|
||||||
ConfigError::ReflectorListNotFound(
|
|
||||||
self.reflector_list_file.clone() + ": " + e.to_string().as_str(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut reflectors: Vec<IpAddr> = Vec::with_capacity(50);
|
|
||||||
|
|
||||||
let mut first = true;
|
|
||||||
|
|
||||||
for line in lines {
|
|
||||||
if first {
|
|
||||||
first = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = line?;
|
|
||||||
let columns: Vec<&str> = line.split(',').collect();
|
|
||||||
reflectors.push(IpAddr::from_str(columns[0])?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(reflectors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-1
@@ -1,5 +1,9 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
|
use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
|
||||||
use rustix::thread::ClockId;
|
use rustix::time::ClockId;
|
||||||
|
|
||||||
use crate::time::Time;
|
use crate::time::Time;
|
||||||
|
|
||||||
|
|||||||
+312
-172
@@ -1,74 +1,196 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
extern crate core;
|
extern crate core;
|
||||||
|
|
||||||
mod baseliner;
|
mod baseliner;
|
||||||
mod config;
|
|
||||||
mod log;
|
mod log;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod netlink;
|
|
||||||
mod pinger;
|
mod pinger;
|
||||||
mod pinger_icmp;
|
mod pinger_icmp;
|
||||||
mod pinger_icmp_ts;
|
mod pinger_icmp_ts;
|
||||||
|
mod platform;
|
||||||
mod ratecontroller;
|
mod ratecontroller;
|
||||||
mod reflector_selector;
|
mod reflector_selector;
|
||||||
|
mod settings;
|
||||||
mod time;
|
mod time;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::baseliner::{Baseliner, ReflectorStats};
|
use crate::baseliner::Baseliner;
|
||||||
use crate::metrics::{Metric, Metrics, MetricsSender};
|
use crate::metrics::{Metric, Metrics, MetricsSender};
|
||||||
use ::log::{debug, info};
|
use crate::pinger::{InFlightProbeCache, PingListener, PingSender};
|
||||||
|
use crate::pinger_icmp::{PingerICMPEchoListener, PingerICMPEchoSender};
|
||||||
|
use crate::pinger_icmp_ts::{PingerICMPTimestampListener, PingerICMPTimestampSender};
|
||||||
|
use crate::platform::{TrafficControlBackend, traffic_control_backend, warn_platform_limitations};
|
||||||
|
use crate::ratecontroller::Ratecontroller;
|
||||||
|
use crate::reflector_selector::ReflectorSelector;
|
||||||
|
use crate::settings::MeasurementType;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use ::log::{info, warn};
|
||||||
|
use flume::RecvTimeoutError;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||||
use std::sync::mpsc::{channel, sync_channel, RecvTimeoutError};
|
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use std::time::Instant;
|
|
||||||
use std::{process, thread};
|
use std::{process, thread};
|
||||||
|
|
||||||
|
struct ReflectorSetup {
|
||||||
|
peers: Arc<RwLock<Vec<IpAddr>>>,
|
||||||
|
pool: Vec<IpAddr>,
|
||||||
|
reselection_enabled: bool,
|
||||||
|
active_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
type PingListenerBox = Box<dyn PingListener + Send>;
|
||||||
|
type PingSenderBox = Box<dyn PingSender + Send>;
|
||||||
|
|
||||||
pub static SHUTDOWN: AtomicBool = AtomicBool::new(false);
|
pub static SHUTDOWN: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
extern "C" fn signal_handler(_: libc::c_int) {
|
extern "C" fn signal_handler(_: libc::c_int) {
|
||||||
SHUTDOWN.store(true, Ordering::Relaxed);
|
SHUTDOWN.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::config::{Config, MeasurementType};
|
|
||||||
use crate::netlink::Netlink;
|
|
||||||
use crate::pinger::{PingListener, PingSender};
|
|
||||||
use crate::pinger_icmp::{PingerICMPEchoListener, PingerICMPEchoSender};
|
|
||||||
use crate::pinger_icmp_ts::{PingerICMPTimestampListener, PingerICMPTimestampSender};
|
|
||||||
use crate::ratecontroller::{Ratecontroller, StatsDirection};
|
|
||||||
use crate::reflector_selector::ReflectorSelector;
|
|
||||||
|
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
const RESELECTION_CANDIDATE_BURST: usize = 20;
|
||||||
|
const INFLIGHT_CAPACITY_DUPLICATE_FACTOR: usize = 2;
|
||||||
|
const INFLIGHT_CAPACITY_MIN: usize = 256;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn compute_inflight_probe_capacity(
|
||||||
println!("Starting sqm-autorate version {}", VERSION);
|
settings: &Settings,
|
||||||
|
active_reflector_count: usize,
|
||||||
|
reselection_enabled: bool,
|
||||||
|
) -> usize {
|
||||||
|
let tick_interval_ms = (settings.advanced_settings.tick_interval * 1000.0).max(1.0);
|
||||||
|
let expected_path_delay_ms = (settings.advanced_settings.download_delay_ms
|
||||||
|
+ settings.advanced_settings.upload_delay_ms)
|
||||||
|
.max(10.0);
|
||||||
|
// Keep enough room for severe queueing events while still adapting to configured delay budgets.
|
||||||
|
let max_rtt_ms = (expected_path_delay_ms * 20.0).clamp(1000.0, 10000.0);
|
||||||
|
|
||||||
|
let burst_reflector_count = if reselection_enabled {
|
||||||
|
active_reflector_count + RESELECTION_CANDIDATE_BURST
|
||||||
|
} else {
|
||||||
|
active_reflector_count
|
||||||
|
};
|
||||||
|
let probes_per_reflector = (max_rtt_ms / tick_interval_ms).ceil() as usize;
|
||||||
|
|
||||||
|
(burst_reflector_count
|
||||||
|
.saturating_mul(probes_per_reflector)
|
||||||
|
.saturating_mul(INFLIGHT_CAPACITY_DUPLICATE_FACTOR))
|
||||||
|
.max(INFLIGHT_CAPACITY_MIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_pinger(measurement_type: MeasurementType) -> (PingListenerBox, PingSenderBox) {
|
||||||
|
match measurement_type {
|
||||||
|
MeasurementType::Icmp => (
|
||||||
|
Box::new(PingerICMPEchoListener {}),
|
||||||
|
Box::new(PingerICMPEchoSender {}),
|
||||||
|
),
|
||||||
|
MeasurementType::IcmpTimestamps => (
|
||||||
|
Box::new(PingerICMPTimestampListener {}),
|
||||||
|
Box::new(PingerICMPTimestampSender {}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_shaper<T: TrafficControlBackend>(
|
||||||
|
settings: &Settings,
|
||||||
|
traffic_control: &mut T,
|
||||||
|
) -> anyhow::Result<(T::Handle, T::Handle)> {
|
||||||
|
initialize_shaper_with_settle_time(settings, traffic_control, Duration::from_secs(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_shaper_with_settle_time<T: TrafficControlBackend>(
|
||||||
|
settings: &Settings,
|
||||||
|
traffic_control: &mut T,
|
||||||
|
settle_time: Duration,
|
||||||
|
) -> anyhow::Result<(T::Handle, T::Handle)> {
|
||||||
|
let down = traffic_control.find_shaper(&settings.network.download_interface)?;
|
||||||
|
let up = traffic_control.find_shaper(&settings.network.upload_interface)?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Requesting minimum shaper rates (D/U): {} / {}",
|
||||||
|
settings.network.download_min_kbits(),
|
||||||
|
settings.network.upload_min_kbits(),
|
||||||
|
);
|
||||||
|
|
||||||
|
traffic_control.set_rate(
|
||||||
|
&down,
|
||||||
|
settings.network.download_min_kbits() as u64,
|
||||||
|
settings.advanced_settings.dry_run,
|
||||||
|
)?;
|
||||||
|
traffic_control.set_rate(
|
||||||
|
&up,
|
||||||
|
settings.network.upload_min_kbits() as u64,
|
||||||
|
settings.advanced_settings.dry_run,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Sleeping for {} seconds to give the shaper a chance to control existing bloat",
|
||||||
|
settle_time.as_secs_f64(),
|
||||||
|
);
|
||||||
|
|
||||||
|
sleep(settle_time);
|
||||||
|
|
||||||
|
Ok((down, up))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_signal_handlers() {
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::signal(libc::SIGINT, signal_handler as *const () as libc::sighandler_t);
|
libc::signal(
|
||||||
libc::signal(libc::SIGTERM, signal_handler as *const () as libc::sighandler_t);
|
libc::SIGINT,
|
||||||
|
signal_handler as *const () as libc::sighandler_t,
|
||||||
|
);
|
||||||
|
libc::signal(
|
||||||
|
libc::SIGTERM,
|
||||||
|
signal_handler as *const () as libc::sighandler_t,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = Config::new()?;
|
fn probe_identifier() -> u16 {
|
||||||
log::init(config.log_level)?;
|
// The ICMP identifier is only two bytes.
|
||||||
if config.dry_run {
|
(process::id() & 0xffff) as u16
|
||||||
info!("*** MONITORING MODE ACTIVE — qdisc rates will NOT be changed ***");
|
|
||||||
}
|
}
|
||||||
let mut reflectors = config.load_reflectors()?;
|
|
||||||
let start_t = Instant::now();
|
|
||||||
|
|
||||||
// The identifier field in ICMP is only 2 bytes
|
fn restore_shaper<T: TrafficControlBackend>(
|
||||||
// so take the last 2 bytes of the PID as the ID
|
settings: &Settings,
|
||||||
let id = (process::id() & 0xFFFF) as u16;
|
traffic_control: &mut T,
|
||||||
|
down: &T::Handle,
|
||||||
|
up: &T::Handle,
|
||||||
|
) {
|
||||||
|
info!(
|
||||||
|
"Requesting restoration to base shaper rates (D/U): {} / {}",
|
||||||
|
settings.network.download_base_kbits, settings.network.upload_base_kbits,
|
||||||
|
);
|
||||||
|
|
||||||
// Create data structures shared by different threads
|
if let Err(error) = traffic_control.set_rate(
|
||||||
let owd_baseline = Arc::new(Mutex::new(HashMap::<IpAddr, ReflectorStats>::new()));
|
down,
|
||||||
let owd_recent = Arc::new(Mutex::new(HashMap::<IpAddr, ReflectorStats>::new()));
|
settings.network.download_base_kbits as u64,
|
||||||
let reflector_peers_lock = Arc::new(RwLock::new(Vec::<IpAddr>::new()));
|
settings.advanced_settings.dry_run,
|
||||||
let mut reflector_pool = Vec::<IpAddr>::new();
|
) {
|
||||||
let reflector_pool_size = reflectors.len();
|
warn!("Failed to restore download shaper rate: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = traffic_control.set_rate(
|
||||||
|
up,
|
||||||
|
settings.network.upload_base_kbits as u64,
|
||||||
|
settings.advanced_settings.dry_run,
|
||||||
|
) {
|
||||||
|
warn!("Failed to restore upload shaper rate: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_reflectors(settings: &Settings) -> anyhow::Result<ReflectorSetup> {
|
||||||
|
let configured = settings.load_reflectors()?;
|
||||||
|
let configured_count = configured.len();
|
||||||
|
|
||||||
let default_reflectors = [
|
let default_reflectors = [
|
||||||
IpAddr::from_str("9.9.9.9")?,
|
IpAddr::from_str("9.9.9.9")?,
|
||||||
@@ -79,28 +201,74 @@ fn main() -> anyhow::Result<()> {
|
|||||||
IpAddr::from_str("94.140.14.14")?,
|
IpAddr::from_str("94.140.14.14")?,
|
||||||
];
|
];
|
||||||
|
|
||||||
match reflector_pool_size > config.num_reflectors as usize {
|
let reselection_enabled = configured_count > settings.advanced_settings.num_reflectors as usize;
|
||||||
true => {
|
let pool = if reselection_enabled {
|
||||||
let mut peers = reflector_peers_lock.write().unwrap();
|
configured
|
||||||
peers.extend_from_slice(&default_reflectors);
|
} else {
|
||||||
reflector_pool.append(reflectors.as_mut());
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ReflectorSetup {
|
||||||
|
peers: Arc::new(RwLock::new(default_reflectors.to_vec())),
|
||||||
|
pool,
|
||||||
|
reselection_enabled,
|
||||||
|
active_count: default_reflectors
|
||||||
|
.len()
|
||||||
|
.max(settings.advanced_settings.num_reflectors as usize),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_exit(error_rx: &flume::Receiver<anyhow::Error>) -> anyhow::Result<()> {
|
||||||
|
loop {
|
||||||
|
match error_rx.recv_timeout(Duration::from_secs(1)) {
|
||||||
|
Ok(error) => {
|
||||||
|
return Err(anyhow::anyhow!("thread exited with error: {error}"));
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Timeout) => {
|
||||||
|
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||||
|
info!("Received shutdown signal");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
false => {
|
|
||||||
let mut peers = reflector_peers_lock.write().unwrap();
|
|
||||||
peers.extend_from_slice(&default_reflectors);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (baseliner_stats_tx, baseliner_stats_rx) = channel();
|
fn run(settings: &Settings) -> anyhow::Result<()> {
|
||||||
let (error_tx, error_rx) = channel::<anyhow::Error>();
|
if settings.advanced_settings.dry_run {
|
||||||
let (reselect_tx, reselect_rx) = channel();
|
info!("*** MONITORING MODE ACTIVE — qdisc rates will NOT be changed ***");
|
||||||
|
}
|
||||||
|
|
||||||
let dropped = Arc::new(AtomicU64::new(0));
|
let start_time = Instant::now();
|
||||||
|
let probe_id = probe_identifier();
|
||||||
|
|
||||||
let (metrics_tx, metrics_thread_handle) = if config.observability_enabled {
|
let ReflectorSetup {
|
||||||
let (tx, rx) = sync_channel(1000);
|
peers: reflector_peers,
|
||||||
|
pool: reflector_pool,
|
||||||
|
reselection_enabled,
|
||||||
|
active_count: active_reflector_count,
|
||||||
|
} = setup_reflectors(settings)?;
|
||||||
|
|
||||||
|
let (baseliner_stats_tx, baseliner_stats_rx) = flume::unbounded();
|
||||||
|
let (control_snapshot_tx, control_snapshot_rx) = flume::unbounded();
|
||||||
|
let (selection_snapshot_tx, selection_snapshot_rx) = if reselection_enabled {
|
||||||
|
let (tx, rx) = flume::unbounded();
|
||||||
|
(Some(tx), Some(rx))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
let (error_tx, error_rx) = flume::unbounded::<anyhow::Error>();
|
||||||
|
let (reselect_tx, reselect_rx) = flume::bounded(1);
|
||||||
|
|
||||||
|
let dropped = Arc::new(AtomicU32::new(0));
|
||||||
|
|
||||||
|
let (metrics_tx, metrics_thread_handle) = if settings.observability.enabled {
|
||||||
|
let (tx, rx) = flume::bounded(1000);
|
||||||
let metrics = Metrics {
|
let metrics = Metrics {
|
||||||
config: config.clone(),
|
settings: settings.clone(),
|
||||||
metrics_rx: rx,
|
metrics_rx: rx,
|
||||||
metrics_dropped: Arc::clone(&dropped),
|
metrics_dropped: Arc::clone(&dropped),
|
||||||
};
|
};
|
||||||
@@ -108,8 +276,8 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let handle = thread::Builder::new()
|
let handle = thread::Builder::new()
|
||||||
.name("metrics".to_string())
|
.name("metrics".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
if let Err(e) = metrics.run() {
|
if let Err(error) = metrics.run() {
|
||||||
let _ = err_tx.send(e);
|
let _ = err_tx.send(error);
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
(Some(tx), Some(handle))
|
(Some(tx), Some(handle))
|
||||||
@@ -125,79 +293,68 @@ fn main() -> anyhow::Result<()> {
|
|||||||
.unwrap_or_else(MetricsSender::disabled)
|
.unwrap_or_else(MetricsSender::disabled)
|
||||||
};
|
};
|
||||||
|
|
||||||
let ping_metrics = make_sender(config.observability_export_ping_metrics);
|
let ping_metrics = make_sender(settings.observability.export_ping_metrics);
|
||||||
let baseline_metrics = make_sender(config.observability_export_baseline_metrics);
|
let baseline_metrics = make_sender(settings.observability.export_baseline_metrics);
|
||||||
let event_metrics = make_sender(config.observability_export_events);
|
let event_metrics = make_sender(settings.observability.export_events);
|
||||||
let rate_metrics = make_sender(config.observability_export_rate_metrics);
|
let rate_metrics = make_sender(settings.observability.export_rate_metrics);
|
||||||
|
|
||||||
event_metrics.send(Metric::Event {
|
event_metrics.send(Metric::Event {
|
||||||
name: "starting",
|
name: "starting",
|
||||||
reason: "",
|
reason: "",
|
||||||
reflector: None,
|
reflector: None,
|
||||||
tags: if config.dry_run { &[("dry_run", "true")] } else { &[] },
|
tags: if settings.advanced_settings.dry_run {
|
||||||
|
&[("dry_run", "true")]
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let (mut ping_listener, mut ping_sender) = match config.measurement_type {
|
let (mut ping_listener, mut ping_sender) =
|
||||||
MeasurementType::Icmp => (
|
create_pinger(settings.advanced_settings.measurement_type);
|
||||||
Box::new(PingerICMPEchoListener {}) as Box<dyn PingListener + Send>,
|
|
||||||
Box::new(PingerICMPEchoSender {}) as Box<dyn PingSender + Send>,
|
let inflight_probe_capacity =
|
||||||
),
|
compute_inflight_probe_capacity(settings, active_reflector_count, reselection_enabled);
|
||||||
MeasurementType::IcmpTimestamps => (
|
info!(
|
||||||
Box::new(PingerICMPTimestampListener {}) as Box<dyn PingListener + Send>,
|
"In-flight probe cache capacity: {} (active_reflectors={}, reselection_enabled={}, tick_interval_s={})",
|
||||||
Box::new(PingerICMPTimestampSender {}) as Box<dyn PingSender + Send>,
|
inflight_probe_capacity,
|
||||||
),
|
active_reflector_count,
|
||||||
MeasurementType::Ntp | MeasurementType::TcpTimestamps => {
|
reselection_enabled,
|
||||||
todo!()
|
settings.advanced_settings.tick_interval
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
let inflight: InFlightProbeCache =
|
||||||
|
Arc::new(Mutex::new(HashMap::with_capacity(inflight_probe_capacity)));
|
||||||
|
|
||||||
let baseliner = Baseliner {
|
let baseliner = Baseliner {
|
||||||
config: config.clone(),
|
settings: settings.clone(),
|
||||||
owd_baseline: owd_baseline.clone(),
|
|
||||||
owd_recent: owd_recent.clone(),
|
|
||||||
reselect_trigger: reselect_tx.clone(),
|
reselect_trigger: reselect_tx.clone(),
|
||||||
start_time: start_t,
|
start_time,
|
||||||
stats_rx: baseliner_stats_rx,
|
stats_rx: baseliner_stats_rx,
|
||||||
|
control_tx: control_snapshot_tx,
|
||||||
|
selection_tx: selection_snapshot_tx,
|
||||||
baseline_metrics,
|
baseline_metrics,
|
||||||
event_metrics: event_metrics.clone(),
|
event_metrics: event_metrics.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let down_qdisc = Netlink::qdisc_from_ifname(config.download_interface.as_str())?;
|
let mut main_traffic_control = traffic_control_backend();
|
||||||
let up_qdisc = Netlink::qdisc_from_ifname(config.upload_interface.as_str())?;
|
let (down_shaper, up_shaper) = initialize_shaper(settings, &mut main_traffic_control)?;
|
||||||
|
|
||||||
/* Set initial TC values to minimum
|
|
||||||
* so there should be no initial bufferbloat to
|
|
||||||
* fool the baseliner
|
|
||||||
*/
|
|
||||||
info!(
|
|
||||||
"Setting shaper rates to minimum (D/L): {} / {}",
|
|
||||||
config.download_min_kbits, config.upload_min_kbits
|
|
||||||
);
|
|
||||||
Netlink::set_qdisc_rate(down_qdisc, config.download_min_kbits as u64, config.dry_run)?;
|
|
||||||
Netlink::set_qdisc_rate(up_qdisc, config.upload_min_kbits as u64, config.dry_run)?;
|
|
||||||
|
|
||||||
// Sleep for a few seconds to give the shaper a chance
|
|
||||||
// to control the queue if load is heavy
|
|
||||||
let settle_sleep_time = Duration::new(2, 0);
|
|
||||||
info!(
|
|
||||||
"Sleeping for {} to give the shaper a chance to get in control if there's bloat",
|
|
||||||
settle_sleep_time.as_secs_f64()
|
|
||||||
);
|
|
||||||
sleep(settle_sleep_time);
|
|
||||||
|
|
||||||
let err_tx = error_tx.clone();
|
let err_tx = error_tx.clone();
|
||||||
let reflector_peers_lock_clone = reflector_peers_lock.clone();
|
let listener_peers = Arc::clone(&reflector_peers);
|
||||||
|
let listener_inflight = Arc::clone(&inflight);
|
||||||
|
let measurement_type = settings.advanced_settings.measurement_type;
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("receiver".to_string())
|
.name("receiver".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
if let Err(e) = ping_listener.listen(
|
if let Err(error) = ping_listener.listen(
|
||||||
id,
|
probe_id,
|
||||||
config.measurement_type,
|
measurement_type,
|
||||||
reflector_peers_lock_clone,
|
listener_peers,
|
||||||
|
listener_inflight,
|
||||||
baseliner_stats_tx,
|
baseliner_stats_tx,
|
||||||
ping_metrics,
|
ping_metrics,
|
||||||
) {
|
) {
|
||||||
let _ = err_tx.send(e);
|
let _ = err_tx.send(error);
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -205,33 +362,38 @@ fn main() -> anyhow::Result<()> {
|
|||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("baseliner".to_string())
|
.name("baseliner".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
if let Err(e) = baseliner.run() {
|
if let Err(error) = baseliner.run() {
|
||||||
let _ = err_tx.send(e);
|
let _ = err_tx.send(error);
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let err_tx = error_tx.clone();
|
let err_tx = error_tx.clone();
|
||||||
let reflector_peers_lock_clone = reflector_peers_lock.clone();
|
let sender_peers = Arc::clone(&reflector_peers);
|
||||||
|
let sender_inflight = Arc::clone(&inflight);
|
||||||
|
let measurement_type = settings.advanced_settings.measurement_type;
|
||||||
|
let tick_interval = settings.advanced_settings.tick_interval;
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("sender".to_string())
|
.name("sender".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
if let Err(e) = ping_sender.send(
|
if let Err(error) = ping_sender.send(
|
||||||
id,
|
probe_id,
|
||||||
config.measurement_type,
|
measurement_type,
|
||||||
reflector_peers_lock_clone,
|
sender_peers,
|
||||||
config.tick_interval,
|
sender_inflight,
|
||||||
|
tick_interval,
|
||||||
) {
|
) {
|
||||||
let _ = err_tx.send(e);
|
let _ = err_tx.send(error);
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let main_event_metrics = event_metrics.clone();
|
let main_event_metrics = event_metrics.clone();
|
||||||
|
|
||||||
if reflector_pool_size > config.num_reflectors as usize {
|
if reselection_enabled {
|
||||||
let reflector_selector = ReflectorSelector {
|
let reflector_selector = ReflectorSelector {
|
||||||
config: config.clone(),
|
settings: settings.clone(),
|
||||||
owd_recent: owd_recent.clone(),
|
snapshot_rx: selection_snapshot_rx
|
||||||
reflector_peers_lock: reflector_peers_lock.clone(),
|
.expect("reselection snapshot receiver must exist when reselection is enabled"),
|
||||||
|
reflector_peers_lock: Arc::clone(&reflector_peers),
|
||||||
reflector_pool,
|
reflector_pool,
|
||||||
trigger_channel: reselect_rx,
|
trigger_channel: reselect_rx,
|
||||||
metrics: event_metrics,
|
metrics: event_metrics,
|
||||||
@@ -240,75 +402,39 @@ fn main() -> anyhow::Result<()> {
|
|||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("reselection".to_string())
|
.name("reselection".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
if let Err(e) = reflector_selector.run() {
|
if let Err(error) = reflector_selector.run() {
|
||||||
let _ = err_tx.send(e);
|
let _ = err_tx.send(error);
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sleep 10 seconds before we start adjusting speeds
|
// Give the baseliner time to collect initial samples before adjusting rates.
|
||||||
sleep(Duration::new(10, 0));
|
sleep(Duration::from_secs(10));
|
||||||
|
|
||||||
let dl_direction = if config.download_interface.starts_with("ifb")
|
|
||||||
|| config.download_interface.starts_with("veth")
|
|
||||||
{
|
|
||||||
StatsDirection::TX
|
|
||||||
} else {
|
|
||||||
StatsDirection::RX
|
|
||||||
};
|
|
||||||
let ul_direction = if config.upload_interface.starts_with("ifb")
|
|
||||||
|| config.upload_interface.starts_with("veth")
|
|
||||||
{
|
|
||||||
StatsDirection::RX
|
|
||||||
} else {
|
|
||||||
StatsDirection::TX
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut ratecontroller = Ratecontroller::new(
|
let mut ratecontroller = Ratecontroller::new(
|
||||||
config.clone(),
|
settings.clone(),
|
||||||
owd_baseline,
|
control_snapshot_rx,
|
||||||
owd_recent,
|
reflector_peers,
|
||||||
reflector_peers_lock,
|
|
||||||
reselect_tx,
|
reselect_tx,
|
||||||
dl_direction,
|
|
||||||
ul_direction,
|
|
||||||
rate_metrics,
|
rate_metrics,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Download direction: {}:{:?}",
|
|
||||||
config.download_interface, dl_direction
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Upload direction: {}:{:?}",
|
|
||||||
config.upload_interface, ul_direction
|
|
||||||
);
|
|
||||||
|
|
||||||
let err_tx = error_tx.clone();
|
let err_tx = error_tx.clone();
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("ratecontroller".to_string())
|
.name("ratecontroller".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
if let Err(e) = ratecontroller.run() {
|
if let Err(error) = ratecontroller.run() {
|
||||||
let _ = err_tx.send(e);
|
let _ = err_tx.send(error);
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Drop original sender so the channel disconnects if all threads exit cleanly
|
// Drop the original sender so the channel disconnects if all workers exit cleanly.
|
||||||
drop(error_tx);
|
drop(error_tx);
|
||||||
|
|
||||||
let result = loop {
|
let result = wait_for_exit(&error_rx);
|
||||||
match error_rx.recv_timeout(Duration::from_secs(1)) {
|
|
||||||
Ok(e) => break Err(anyhow::anyhow!("thread exited with error: {e}")),
|
// Make the error path request shutdown too, rather than leaving the other workers running.
|
||||||
Err(RecvTimeoutError::Disconnected) => break Ok(()),
|
SHUTDOWN.store(true, Ordering::Relaxed);
|
||||||
Err(RecvTimeoutError::Timeout) => {
|
|
||||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
|
||||||
info!("Received shutdown signal");
|
|
||||||
break Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let stopping_reason = if result.is_err() { "error" } else { "signal" };
|
let stopping_reason = if result.is_err() { "error" } else { "signal" };
|
||||||
main_event_metrics.send(Metric::Event {
|
main_event_metrics.send(Metric::Event {
|
||||||
@@ -319,20 +445,34 @@ fn main() -> anyhow::Result<()> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Drop all MetricsSender instances and the raw tx so the metrics channel
|
// Drop all MetricsSender instances and the raw tx so the metrics channel
|
||||||
// disconnects once all threads also drop their copies, allowing the
|
// disconnects once all worker threads also drop their copies.
|
||||||
// metrics thread to drain and exit cleanly.
|
|
||||||
drop(main_event_metrics);
|
drop(main_event_metrics);
|
||||||
drop(metrics_tx);
|
drop(metrics_tx);
|
||||||
|
|
||||||
if let Some(handle) = metrics_thread_handle {
|
if let Some(handle) = metrics_thread_handle {
|
||||||
let _ = handle.join();
|
let _ = handle.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
restore_shaper(
|
||||||
"Restoring base shaper rates (D/L): {} / {}",
|
settings,
|
||||||
config.download_base_kbits, config.upload_base_kbits
|
&mut main_traffic_control,
|
||||||
|
&down_shaper,
|
||||||
|
&up_shaper,
|
||||||
);
|
);
|
||||||
let _ = Netlink::set_qdisc_rate(down_qdisc, config.download_base_kbits as u64, config.dry_run);
|
|
||||||
let _ = Netlink::set_qdisc_rate(up_qdisc, config.upload_base_kbits as u64, config.dry_run);
|
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
println!("Starting sqm-autorate-rust version {}", VERSION);
|
||||||
|
|
||||||
|
install_signal_handlers();
|
||||||
|
|
||||||
|
let settings = Settings::load()?;
|
||||||
|
settings.validate()?;
|
||||||
|
|
||||||
|
log::init(settings.output.log_level)?;
|
||||||
|
warn_platform_limitations();
|
||||||
|
|
||||||
|
run(&settings)
|
||||||
|
}
|
||||||
+27
-25
@@ -1,13 +1,17 @@
|
|||||||
use crate::config::{MeasurementType, ObservabilityProtocol};
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use crate::settings::{MeasurementType, ObservabilityProtocol};
|
||||||
use crate::time::Time;
|
use crate::time::Time;
|
||||||
use crate::Config;
|
use flume::{Receiver, RecvTimeoutError, Sender};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use rustix::time::ClockId;
|
use rustix::time::ClockId;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::net::{IpAddr, TcpStream, UdpSocket};
|
use std::net::{IpAddr, TcpStream, UdpSocket};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const MAX_RECONNECT_BACKOFF: u64 = 60;
|
const MAX_RECONNECT_BACKOFF: u64 = 60;
|
||||||
@@ -74,8 +78,10 @@ impl Transport {
|
|||||||
|
|
||||||
let full_data = format!("{}\n", data);
|
let full_data = format!("{}\n", data);
|
||||||
// take the stream out, write, put it back or set None on failure
|
// take the stream out, write, put it back or set None on failure
|
||||||
if let Some(s) = stream {
|
let write_error = stream
|
||||||
if let Err(e) = std::io::Write::write_all(s, full_data.as_bytes()) {
|
.as_mut()
|
||||||
|
.and_then(|s| std::io::Write::write_all(s, full_data.as_bytes()).err());
|
||||||
|
if let Some(e) = write_error {
|
||||||
warn!("TCP send failed: {}", e);
|
warn!("TCP send failed: {}", e);
|
||||||
*stream = None;
|
*stream = None;
|
||||||
*reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
|
*reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
|
||||||
@@ -84,7 +90,6 @@ impl Transport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Metric {
|
pub enum Metric {
|
||||||
Ping {
|
Ping {
|
||||||
@@ -116,18 +121,18 @@ pub enum Metric {
|
|||||||
tags: &'static [(&'static str, &'static str)],
|
tags: &'static [(&'static str, &'static str)],
|
||||||
},
|
},
|
||||||
Dropped {
|
Dropped {
|
||||||
count: u64,
|
count: u32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MetricsSender {
|
pub struct MetricsSender {
|
||||||
tx: Option<SyncSender<(Metric, u64)>>,
|
tx: Option<Sender<(Metric, u64)>>,
|
||||||
dropped: Arc<AtomicU64>,
|
dropped: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetricsSender {
|
impl MetricsSender {
|
||||||
pub fn new(tx: SyncSender<(Metric, u64)>, dropped: Arc<AtomicU64>) -> Self {
|
pub fn new(tx: Sender<(Metric, u64)>, dropped: Arc<AtomicU32>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tx: Some(tx),
|
tx: Some(tx),
|
||||||
dropped,
|
dropped,
|
||||||
@@ -137,7 +142,7 @@ impl MetricsSender {
|
|||||||
pub fn disabled() -> Self {
|
pub fn disabled() -> Self {
|
||||||
Self {
|
Self {
|
||||||
tx: None,
|
tx: None,
|
||||||
dropped: Arc::new(AtomicU64::new(0)),
|
dropped: Arc::new(AtomicU32::new(0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +157,14 @@ impl MetricsSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Metrics {
|
pub struct Metrics {
|
||||||
pub config: Config,
|
pub settings: Settings,
|
||||||
pub metrics_dropped: Arc<AtomicU64>,
|
pub metrics_dropped: Arc<AtomicU32>,
|
||||||
pub metrics_rx: Receiver<(Metric, u64)>,
|
pub metrics_rx: Receiver<(Metric, u64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metrics {
|
impl Metrics {
|
||||||
pub fn run(self) -> anyhow::Result<()> {
|
pub fn run(self) -> anyhow::Result<()> {
|
||||||
let host = match &self.config.observability_host {
|
let host = match &self.settings.observability.host {
|
||||||
Some(h) => h.clone(),
|
Some(h) => h.clone(),
|
||||||
None => {
|
None => {
|
||||||
error!("Observability host not configured - metrics exporter disabled");
|
error!("Observability host not configured - metrics exporter disabled");
|
||||||
@@ -167,12 +172,12 @@ impl Metrics {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let port = self.config.observability_port;
|
let port = self.settings.observability.port;
|
||||||
let batch_size = self.config.observability_batch_size;
|
let batch_size = self.settings.observability.batch_size;
|
||||||
let timeout = Duration::from_millis(self.config.observability_batch_timeout_ms);
|
let timeout = Duration::from_millis(self.settings.observability.batch_timeout_ms);
|
||||||
let host_tag = &self.config.observability_host_tag.clone();
|
let host_tag = &self.settings.observability.host_tag.clone();
|
||||||
|
|
||||||
let mut transport = match self.config.observability_protocol {
|
let mut transport = match self.settings.observability.protocol {
|
||||||
ObservabilityProtocol::Udp => {
|
ObservabilityProtocol::Udp => {
|
||||||
info!("Metrics exporter configured for UDP to {}:{}", host, port);
|
info!("Metrics exporter configured for UDP to {}:{}", host, port);
|
||||||
Transport::new_udp(&host, port)?
|
Transport::new_udp(&host, port)?
|
||||||
@@ -205,11 +210,8 @@ impl Metrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// greedily drain without blocking
|
// greedily drain without blocking
|
||||||
loop {
|
while let Ok(metric) = self.metrics_rx.try_recv() {
|
||||||
match self.metrics_rx.try_recv() {
|
batch.push(metric);
|
||||||
Ok(metric) => batch.push(metric),
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
if batch.len() >= batch_size {
|
if batch.len() >= batch_size {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
-145
@@ -1,145 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
use std::str::Utf8Error;
|
|
||||||
|
|
||||||
use log::info;
|
|
||||||
use netlink_bindings::{rt_link, tc};
|
|
||||||
use netlink_socket2::NetlinkSocket;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum NetlinkError {
|
|
||||||
#[error("Couldn't find interface `{0}`")]
|
|
||||||
InterfaceNotFound(String),
|
|
||||||
|
|
||||||
#[error("Netlink error: {0}")]
|
|
||||||
Netlink(#[from] io::Error),
|
|
||||||
|
|
||||||
#[error("Netlink reply error: {0}")]
|
|
||||||
Reply(#[from] netlink_socket2::ReplyError),
|
|
||||||
|
|
||||||
#[error("Something went wrong while finding qdisc")]
|
|
||||||
NlQdiscError(String),
|
|
||||||
|
|
||||||
#[error("Couldn't find CAKE qdisc on interface `{0}`")]
|
|
||||||
NoQdiscFound(String),
|
|
||||||
|
|
||||||
#[error("Couldn't find interface statistics: `{0}`")]
|
|
||||||
NoInterfaceStatsFound(String),
|
|
||||||
|
|
||||||
#[error("Error happened while parsing UTF-8 string")]
|
|
||||||
Utf8Error(#[from] Utf8Error),
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Qdisc {
|
|
||||||
pub ifindex: i32,
|
|
||||||
pub parent: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Netlink {}
|
|
||||||
|
|
||||||
impl Netlink {
|
|
||||||
pub fn find_interface(ifname: &str) -> Result<i32, NetlinkError> {
|
|
||||||
let mut socket = NetlinkSocket::new();
|
|
||||||
|
|
||||||
let mut request = rt_link::Request::new()
|
|
||||||
.op_getlink_do_request(&Default::default());
|
|
||||||
request.encode().push_ifname_bytes(ifname.as_bytes());
|
|
||||||
|
|
||||||
let mut iter = socket.request(&request)?;
|
|
||||||
let (header, _) = iter.recv_one()
|
|
||||||
.map_err(|_| NetlinkError::InterfaceNotFound(ifname.to_string()))?;
|
|
||||||
Ok(header.ifi_index())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_interface_stats(ifname: &str) -> Result<(u64, u64), NetlinkError> {
|
|
||||||
let mut socket = NetlinkSocket::new();
|
|
||||||
|
|
||||||
let mut request = rt_link::Request::new()
|
|
||||||
.op_getlink_do_request(&Default::default());
|
|
||||||
request.encode()
|
|
||||||
.push_ifname_bytes(ifname.as_bytes())
|
|
||||||
.push_ext_mask(1 /* RTEXT_FILTER_VF */);
|
|
||||||
|
|
||||||
let mut iter = socket.request(&request)?;
|
|
||||||
while let Some(reply) = iter.recv() {
|
|
||||||
let (_, attrs) = reply?;
|
|
||||||
for attr in attrs {
|
|
||||||
if let Ok(rt_link::OpGetlinkDoReply::Stats64(stats)) = attr {
|
|
||||||
return Ok((stats.rx_bytes(), stats.tx_bytes()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(NetlinkError::NoInterfaceStatsFound(ifname.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn qdisc_from_ifindex(ifindex: i32, ifname: &str) -> Result<Qdisc, NetlinkError> {
|
|
||||||
let mut socket = NetlinkSocket::new();
|
|
||||||
let header = tc::PushTcmsg::new();
|
|
||||||
let request = tc::Request::new().op_getqdisc_dump_request(&header);
|
|
||||||
|
|
||||||
let mut iter = socket.request(&request)?;
|
|
||||||
while let Some(reply) = iter.recv() {
|
|
||||||
let (header, attrs) = reply?;
|
|
||||||
|
|
||||||
if header.ifindex() == ifindex {
|
|
||||||
let mut is_cake = false;
|
|
||||||
for attr in attrs {
|
|
||||||
if let Ok(tc::OpGetqdiscDumpReply::Kind(kind)) = attr {
|
|
||||||
if kind.to_str().map_err(|_| NetlinkError::NlQdiscError("Invalid UTF-8".to_string()))? == "cake" {
|
|
||||||
is_cake = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_cake {
|
|
||||||
return Ok(Qdisc {
|
|
||||||
ifindex,
|
|
||||||
parent: header.parent(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(NetlinkError::NoQdiscFound(ifname.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn qdisc_from_ifname(ifname: &str) -> Result<Qdisc, NetlinkError> {
|
|
||||||
let ifindex = Netlink::find_interface(ifname)?;
|
|
||||||
Netlink::qdisc_from_ifindex(ifindex, ifname)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_qdisc_rate(qdisc: Qdisc, bandwidth_kbit: u64, dry_run: bool) -> Result<(), NetlinkError> {
|
|
||||||
if dry_run {
|
|
||||||
info!(
|
|
||||||
"dry-run: would set qdisc ifindex={} to {} kbit/s",
|
|
||||||
qdisc.ifindex, bandwidth_kbit
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut socket = NetlinkSocket::new();
|
|
||||||
let bandwidth = bandwidth_kbit * 1000 / 8;
|
|
||||||
|
|
||||||
let mut header = tc::PushTcmsg::new();
|
|
||||||
header.set_ifindex(qdisc.ifindex);
|
|
||||||
header.set_parent(qdisc.parent);
|
|
||||||
|
|
||||||
let mut request = tc::Request::new()
|
|
||||||
.set_change()
|
|
||||||
.op_newqdisc_do_request(&header);
|
|
||||||
request.encode()
|
|
||||||
.push_kind(c"cake")
|
|
||||||
.nested_options_cake()
|
|
||||||
.push_base_rate64(bandwidth)
|
|
||||||
.end_nested();
|
|
||||||
|
|
||||||
let mut iter = socket.request(&request)?;
|
|
||||||
iter.recv_ack()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+52
-14
@@ -1,14 +1,22 @@
|
|||||||
use crate::SHUTDOWN;
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::MeasurementType;
|
use crate::MeasurementType;
|
||||||
|
use crate::SHUTDOWN;
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::util::RwLockExt;
|
use crate::util::{MutexExt, RwLockExt};
|
||||||
|
use flume::Sender;
|
||||||
use icmp_socket2::socket::IcmpSocket;
|
use icmp_socket2::socket::IcmpSocket;
|
||||||
use icmp_socket2::{IcmpSocket4, Icmpv4Packet};
|
use icmp_socket2::{IcmpSocket4, Icmpv4Packet};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{io, thread};
|
use std::{io, thread};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -39,12 +47,19 @@ pub struct PingReply {
|
|||||||
pub last_receive_time_s: Instant,
|
pub last_receive_time_s: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct InFlightProbe {
|
||||||
|
pub sent_at: Instant,
|
||||||
|
pub originate_timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type InFlightProbeKey = (IpAddr, MeasurementType, u16);
|
||||||
|
pub type InFlightProbeCache = Arc<Mutex<HashMap<InFlightProbeKey, InFlightProbe>>>;
|
||||||
|
const INFLIGHT_PROBE_TTL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
fn open_socket(type_: MeasurementType) -> io::Result<IcmpSocket4> {
|
fn open_socket(type_: MeasurementType) -> io::Result<IcmpSocket4> {
|
||||||
match type_ {
|
match type_ {
|
||||||
MeasurementType::Icmp | MeasurementType::IcmpTimestamps => IcmpSocket4::new(),
|
MeasurementType::Icmp | MeasurementType::IcmpTimestamps => IcmpSocket4::new(),
|
||||||
_ => {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +69,7 @@ pub trait PingListener {
|
|||||||
id: u16,
|
id: u16,
|
||||||
type_: MeasurementType,
|
type_: MeasurementType,
|
||||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
||||||
|
inflight: InFlightProbeCache,
|
||||||
stats_tx: Sender<PingReply>,
|
stats_tx: Sender<PingReply>,
|
||||||
ping_metrics: MetricsSender,
|
ping_metrics: MetricsSender,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@@ -85,6 +101,14 @@ pub trait PingListener {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let key = (addr, type_, reply.seq);
|
||||||
|
let Some(probe) = inflight.lock_anyhow()?.remove(&key) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if reply.originate_timestamp != probe.originate_timestamp {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
ping_metrics.send(Metric::Ping {
|
ping_metrics.send(Metric::Ping {
|
||||||
reflector: reply.reflector,
|
reflector: reply.reflector,
|
||||||
measurement_type: reply.measurement_type,
|
measurement_type: reply.measurement_type,
|
||||||
@@ -128,6 +152,7 @@ pub trait PingSender {
|
|||||||
id: u16,
|
id: u16,
|
||||||
type_: MeasurementType,
|
type_: MeasurementType,
|
||||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
||||||
|
inflight: InFlightProbeCache,
|
||||||
tick_interval: f64,
|
tick_interval: f64,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut socket = open_socket(type_)?;
|
let mut socket = open_socket(type_)?;
|
||||||
@@ -151,6 +176,10 @@ pub trait PingSender {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inflight
|
||||||
|
.lock_anyhow()?
|
||||||
|
.retain(|_, probe| probe.sent_at.elapsed() < INFLIGHT_PROBE_TTL);
|
||||||
|
|
||||||
let sleep_duration =
|
let sleep_duration =
|
||||||
Duration::from_millis((tick_duration_ms / reflectors.len() as u16) as u64);
|
Duration::from_millis((tick_duration_ms / reflectors.len() as u16) as u64);
|
||||||
|
|
||||||
@@ -160,17 +189,26 @@ pub trait PingSender {
|
|||||||
IpAddr::V6(_) => unimplemented!(),
|
IpAddr::V6(_) => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.send_to(addr, self.craft_packet(id, seq))?;
|
let (packet, originate_timestamp) = self.craft_packet(id, seq);
|
||||||
|
let sent_at = Instant::now();
|
||||||
|
let key = (*reflector, type_, seq);
|
||||||
|
inflight.lock_anyhow()?.insert(
|
||||||
|
key,
|
||||||
|
InFlightProbe {
|
||||||
|
sent_at,
|
||||||
|
originate_timestamp,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if let Err(e) = socket.send_to(addr, packet) {
|
||||||
|
inflight.lock_anyhow()?.remove(&key);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
thread::sleep(sleep_duration);
|
thread::sleep(sleep_duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
if seq == u16::MAX {
|
seq = seq.wrapping_add(1);
|
||||||
seq = 0;
|
|
||||||
} else {
|
|
||||||
seq += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn craft_packet(&self, id: u16, seq: u16) -> icmp_socket2::packet::Icmpv4Packet;
|
fn craft_packet(&self, id: u16, seq: u16) -> (icmp_socket2::packet::Icmpv4Packet, i64);
|
||||||
}
|
}
|
||||||
+14
-4
@@ -1,13 +1,20 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::config::MeasurementType;
|
|
||||||
use crate::pinger::{PingError, PingListener, PingReply, PingSender};
|
use crate::pinger::{PingError, PingListener, PingReply, PingSender};
|
||||||
|
use crate::settings::MeasurementType;
|
||||||
use crate::time::Time;
|
use crate::time::Time;
|
||||||
use icmp_socket2::Icmpv4Message;
|
use icmp_socket2::Icmpv4Message;
|
||||||
use icmp_socket2::Icmpv4Packet;
|
use icmp_socket2::Icmpv4Packet;
|
||||||
use icmp_socket2::packet::WithEchoRequest;
|
use icmp_socket2::packet::WithEchoRequest;
|
||||||
use rustix::thread::ClockId;
|
use rustix::time::ClockId;
|
||||||
|
|
||||||
pub struct PingerICMPEchoListener {}
|
pub struct PingerICMPEchoListener {}
|
||||||
|
|
||||||
@@ -77,11 +84,14 @@ impl PingListener for PingerICMPEchoListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PingSender for PingerICMPEchoSender {
|
impl PingSender for PingerICMPEchoSender {
|
||||||
fn craft_packet(&self, id: u16, seq: u16) -> Icmpv4Packet {
|
fn craft_packet(&self, id: u16, seq: u16) -> (Icmpv4Packet, i64) {
|
||||||
let clock = Time::new(ClockId::Monotonic);
|
let clock = Time::new(ClockId::Monotonic);
|
||||||
let time_ms = clock.to_milliseconds();
|
let time_ms = clock.to_milliseconds();
|
||||||
let payload = time_ms.to_be_bytes().to_vec();
|
let payload = time_ms.to_be_bytes().to_vec();
|
||||||
|
|
||||||
Icmpv4Packet::with_echo_request(id, seq, payload).unwrap()
|
(
|
||||||
|
Icmpv4Packet::with_echo_request(id, seq, payload).unwrap(),
|
||||||
|
time_ms as i64,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+15
-4
@@ -1,10 +1,17 @@
|
|||||||
use crate::config::MeasurementType;
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::pinger::{PingError, PingListener, PingReply, PingSender};
|
use crate::pinger::{PingError, PingListener, PingReply, PingSender};
|
||||||
|
use crate::settings::MeasurementType;
|
||||||
use crate::time::Time;
|
use crate::time::Time;
|
||||||
use icmp_socket2::Icmpv4Message;
|
use icmp_socket2::Icmpv4Message;
|
||||||
use icmp_socket2::Icmpv4Packet;
|
use icmp_socket2::Icmpv4Packet;
|
||||||
use icmp_socket2::packet::WithTimestampRequest;
|
use icmp_socket2::packet::WithTimestampRequest;
|
||||||
use rustix::thread::ClockId;
|
use rustix::time::ClockId;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -72,8 +79,12 @@ impl PingListener for PingerICMPTimestampListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PingSender for PingerICMPTimestampSender {
|
impl PingSender for PingerICMPTimestampSender {
|
||||||
fn craft_packet(&self, id: u16, seq: u16) -> Icmpv4Packet {
|
fn craft_packet(&self, id: u16, seq: u16) -> (Icmpv4Packet, i64) {
|
||||||
let time_since_midnight = Time::new(ClockId::Realtime).get_time_since_midnight();
|
let time_since_midnight = Time::new(ClockId::Realtime).get_time_since_midnight();
|
||||||
Icmpv4Packet::with_timestamp_request(id, seq, time_since_midnight as u32, 0, 0).unwrap()
|
(
|
||||||
|
Icmpv4Packet::with_timestamp_request(id, seq, time_since_midnight as u32, 0, 0)
|
||||||
|
.unwrap(),
|
||||||
|
time_since_midnight as i64,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::io;
|
||||||
|
use std::mem::{MaybeUninit, size_of};
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use super::noop::NoopTrafficControl;
|
||||||
|
use crate::platform::{InterfaceStats, InterfaceStatsProvider};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub(crate) enum FreeBsdInterfaceStatsError {
|
||||||
|
#[error("interface name contains a NUL byte")]
|
||||||
|
InterfaceNameContainsNul(#[source] std::ffi::NulError),
|
||||||
|
|
||||||
|
#[error("interface `{interface}` was not found")]
|
||||||
|
InterfaceNotFound {
|
||||||
|
interface: String,
|
||||||
|
#[source]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("failed to query statistics for interface `{interface}`")]
|
||||||
|
Sysctl {
|
||||||
|
interface: String,
|
||||||
|
#[source]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"unexpected interface-statistics response size for `{interface}`: \
|
||||||
|
expected {expected} bytes, received {actual}"
|
||||||
|
)]
|
||||||
|
UnexpectedResponseSize {
|
||||||
|
interface: String,
|
||||||
|
expected: usize,
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct FreeBsdInterfaceStats;
|
||||||
|
|
||||||
|
impl InterfaceStatsProvider for FreeBsdInterfaceStats {
|
||||||
|
type Error = FreeBsdInterfaceStatsError;
|
||||||
|
|
||||||
|
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error> {
|
||||||
|
let interface_name = CString::new(interface)
|
||||||
|
.map_err(FreeBsdInterfaceStatsError::InterfaceNameContainsNul)?;
|
||||||
|
let interface_index = unsafe { libc::if_nametoindex(interface_name.as_ptr()) };
|
||||||
|
if interface_index == 0 {
|
||||||
|
return Err(FreeBsdInterfaceStatsError::InterfaceNotFound {
|
||||||
|
interface: interface.to_string(),
|
||||||
|
source: io::Error::last_os_error(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mib = [
|
||||||
|
libc::CTL_NET,
|
||||||
|
libc::PF_LINK,
|
||||||
|
libc::NETLINK_GENERIC,
|
||||||
|
libc::IFMIB_IFDATA,
|
||||||
|
interface_index as libc::c_int,
|
||||||
|
libc::IFDATA_GENERAL,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut ifmd = MaybeUninit::<libc::ifmibdata>::uninit();
|
||||||
|
let mut returned_len = size_of::<libc::ifmibdata>();
|
||||||
|
|
||||||
|
let result = unsafe {
|
||||||
|
libc::sysctl(
|
||||||
|
mib.as_ptr(),
|
||||||
|
mib.len() as libc::c_uint,
|
||||||
|
ifmd.as_mut_ptr().cast::<libc::c_void>(),
|
||||||
|
&mut returned_len,
|
||||||
|
ptr::null(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result == -1 {
|
||||||
|
return Err(FreeBsdInterfaceStatsError::Sysctl {
|
||||||
|
interface: interface.to_string(),
|
||||||
|
source: io::Error::last_os_error(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected_len = size_of::<libc::ifmibdata>();
|
||||||
|
if returned_len != expected_len {
|
||||||
|
return Err(FreeBsdInterfaceStatsError::UnexpectedResponseSize {
|
||||||
|
interface: interface.to_string(),
|
||||||
|
expected: expected_len,
|
||||||
|
actual: returned_len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let ifmd = unsafe { ifmd.assume_init() };
|
||||||
|
|
||||||
|
Ok(InterfaceStats {
|
||||||
|
rx_bytes: ifmd.ifmd_data.ifi_ibytes,
|
||||||
|
tx_bytes: ifmd.ifmd_data.ifi_obytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type PlatformInterfaceStats = FreeBsdInterfaceStats;
|
||||||
|
pub(crate) type PlatformTrafficControl = NoopTrafficControl;
|
||||||
|
|
||||||
|
pub(crate) fn interface_stats_provider() -> PlatformInterfaceStats {
|
||||||
|
FreeBsdInterfaceStats
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn traffic_control_backend() -> PlatformTrafficControl {
|
||||||
|
NoopTrafficControl
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn warn_platform_limitations() {
|
||||||
|
warn!(
|
||||||
|
"Traffic control is unavailable on FreeBSD; requested rate changes will be calculated but not applied."
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
mod netlink;
|
||||||
|
|
||||||
|
pub(crate) type PlatformInterfaceStats = netlink::Netlink;
|
||||||
|
pub(crate) type PlatformTrafficControl = netlink::Netlink;
|
||||||
|
|
||||||
|
pub(crate) fn interface_stats_provider() -> PlatformInterfaceStats {
|
||||||
|
netlink::Netlink::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn traffic_control_backend() -> PlatformTrafficControl {
|
||||||
|
netlink::Netlink::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn warn_platform_limitations() {}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::str::Utf8Error;
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use netlink_bindings::{rt_link, tc};
|
||||||
|
use netlink_socket2::NetlinkSocket;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::platform::{InterfaceStats, InterfaceStatsProvider, TrafficControlBackend};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub(crate) enum NetlinkError {
|
||||||
|
#[error("Couldn't find interface `{0}`")]
|
||||||
|
InterfaceNotFound(String),
|
||||||
|
|
||||||
|
#[error("Netlink error: {0}")]
|
||||||
|
Netlink(#[from] io::Error),
|
||||||
|
|
||||||
|
#[error("Netlink reply error: {0}")]
|
||||||
|
Reply(#[from] netlink_socket2::ReplyError),
|
||||||
|
|
||||||
|
#[error("Something went wrong while finding qdisc")]
|
||||||
|
NlQdiscError(String),
|
||||||
|
|
||||||
|
#[error("Couldn't find CAKE qdisc on interface `{0}`")]
|
||||||
|
NoQdiscFound(String),
|
||||||
|
|
||||||
|
#[error("Couldn't find interface statistics: `{0}`")]
|
||||||
|
NoInterfaceStatsFound(String),
|
||||||
|
|
||||||
|
#[error("Error happened while parsing UTF-8 string")]
|
||||||
|
Utf8Error(#[from] Utf8Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct Qdisc {
|
||||||
|
ifindex: i32,
|
||||||
|
parent: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct Netlink {}
|
||||||
|
|
||||||
|
impl Netlink {
|
||||||
|
fn find_interface(ifname: &str) -> Result<i32, NetlinkError> {
|
||||||
|
let mut socket = NetlinkSocket::new();
|
||||||
|
|
||||||
|
let mut request = rt_link::Request::new().op_getlink_do(&Default::default());
|
||||||
|
request.encode().push_ifname_bytes(ifname.as_bytes());
|
||||||
|
|
||||||
|
let mut iter = socket.request(&request)?;
|
||||||
|
let (header, _) = iter
|
||||||
|
.recv_one()
|
||||||
|
.map_err(|_| NetlinkError::InterfaceNotFound(ifname.to_string()))?;
|
||||||
|
Ok(header.ifi_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_interface_stats(ifname: &str) -> Result<(u64, u64), NetlinkError> {
|
||||||
|
let mut socket = NetlinkSocket::new();
|
||||||
|
|
||||||
|
let mut request = rt_link::Request::new().op_getlink_do(&Default::default());
|
||||||
|
request
|
||||||
|
.encode()
|
||||||
|
.push_ifname_bytes(ifname.as_bytes())
|
||||||
|
.push_ext_mask(1 /* RTEXT_FILTER_VF */);
|
||||||
|
|
||||||
|
let mut iter = socket.request(&request)?;
|
||||||
|
while let Some(reply) = iter.recv() {
|
||||||
|
let (_, attrs) = reply?;
|
||||||
|
if let Ok(stats) = attrs.get_stats64() {
|
||||||
|
return Ok((stats.rx_bytes, stats.tx_bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(NetlinkError::NoInterfaceStatsFound(ifname.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn qdisc_from_ifindex(ifindex: i32, ifname: &str) -> Result<Qdisc, NetlinkError> {
|
||||||
|
let mut socket = NetlinkSocket::new();
|
||||||
|
let header = tc::Tcmsg::new();
|
||||||
|
let request = tc::Request::new().op_getqdisc_dump(&header);
|
||||||
|
|
||||||
|
let mut iter = socket.request(&request)?;
|
||||||
|
while let Some(reply) = iter.recv() {
|
||||||
|
let (header, attrs) = reply?;
|
||||||
|
|
||||||
|
if header.ifindex == ifindex
|
||||||
|
&& let Ok(kind) = attrs.get_kind()
|
||||||
|
&& kind
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| NetlinkError::NlQdiscError("Invalid UTF-8".to_string()))?
|
||||||
|
== "cake"
|
||||||
|
{
|
||||||
|
return Ok(Qdisc {
|
||||||
|
ifindex,
|
||||||
|
parent: header.parent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(NetlinkError::NoQdiscFound(ifname.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn qdisc_from_ifname(ifname: &str) -> Result<Qdisc, NetlinkError> {
|
||||||
|
let ifindex = Netlink::find_interface(ifname)?;
|
||||||
|
Netlink::qdisc_from_ifindex(ifindex, ifname)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_qdisc_rate(
|
||||||
|
qdisc: Qdisc,
|
||||||
|
bandwidth_kbit: u64,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<(), NetlinkError> {
|
||||||
|
if dry_run {
|
||||||
|
info!(
|
||||||
|
"dry-run: would set qdisc ifindex={} to {} kbit/s",
|
||||||
|
qdisc.ifindex, bandwidth_kbit
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut socket = NetlinkSocket::new();
|
||||||
|
let bandwidth = bandwidth_kbit * 1000 / 8;
|
||||||
|
|
||||||
|
let mut header = tc::Tcmsg::new();
|
||||||
|
header.ifindex = qdisc.ifindex;
|
||||||
|
header.parent = qdisc.parent;
|
||||||
|
let mut request = tc::Request::new().set_change().op_newqdisc_do(&header);
|
||||||
|
request
|
||||||
|
.encode()
|
||||||
|
.push_kind(c"cake")
|
||||||
|
.nested_options_cake()
|
||||||
|
.push_base_rate64(bandwidth)
|
||||||
|
.end_nested();
|
||||||
|
|
||||||
|
let mut iter = socket.request(&request)?;
|
||||||
|
iter.recv_ack()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterfaceStatsProvider for Netlink {
|
||||||
|
type Error = NetlinkError;
|
||||||
|
|
||||||
|
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error> {
|
||||||
|
let (rx_bytes, tx_bytes) = Self::get_interface_stats(interface)?;
|
||||||
|
Ok(InterfaceStats { rx_bytes, tx_bytes })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrafficControlBackend for Netlink {
|
||||||
|
type Error = NetlinkError;
|
||||||
|
type Handle = Qdisc;
|
||||||
|
|
||||||
|
fn find_shaper(&mut self, interface: &str) -> Result<Self::Handle, Self::Error> {
|
||||||
|
Self::qdisc_from_ifname(interface)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_rate(
|
||||||
|
&mut self,
|
||||||
|
shaper: &Self::Handle,
|
||||||
|
bandwidth_kbit: u64,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
Self::set_qdisc_rate(*shaper, bandwidth_kbit, dry_run)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use std::ffi::{CString, NulError};
|
||||||
|
use std::io;
|
||||||
|
use std::mem::size_of;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::noop::NoopTrafficControl;
|
||||||
|
use crate::platform::{InterfaceStats, InterfaceStatsProvider};
|
||||||
|
|
||||||
|
const ROUTE_MESSAGE_PREFIX_LEN: usize = size_of::<libc::c_ushort>() + 2;
|
||||||
|
const SYSCTL_FETCH_ATTEMPTS: usize = 3;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub(crate) enum MacOsInterfaceStatsError {
|
||||||
|
#[error("interface name contains a NUL byte")]
|
||||||
|
InterfaceNameContainsNul(#[source] NulError),
|
||||||
|
|
||||||
|
#[error("interface `{interface}` was not found")]
|
||||||
|
InterfaceNotFound {
|
||||||
|
interface: String,
|
||||||
|
#[source]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("sysctl failed while {operation}")]
|
||||||
|
Sysctl {
|
||||||
|
operation: &'static str,
|
||||||
|
#[source]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("malformed route-message response: {0}")]
|
||||||
|
MalformedResponse(String),
|
||||||
|
|
||||||
|
#[error("no RTM_IFINFO2 statistics returned for interface index {interface_index}")]
|
||||||
|
NoInterfaceStats { interface_index: libc::c_uint },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct MacOsInterfaceStats;
|
||||||
|
|
||||||
|
impl InterfaceStatsProvider for MacOsInterfaceStats {
|
||||||
|
type Error = MacOsInterfaceStatsError;
|
||||||
|
|
||||||
|
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error> {
|
||||||
|
let interface_name =
|
||||||
|
CString::new(interface).map_err(MacOsInterfaceStatsError::InterfaceNameContainsNul)?;
|
||||||
|
// SAFETY: `interface_name` is a live, NUL-terminated C string for the duration of the call.
|
||||||
|
let interface_index = unsafe { libc::if_nametoindex(interface_name.as_ptr()) };
|
||||||
|
if interface_index == 0 {
|
||||||
|
return Err(MacOsInterfaceStatsError::InterfaceNotFound {
|
||||||
|
interface: interface.to_string(),
|
||||||
|
source: io::Error::last_os_error(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = route_interface_list(interface_index)?;
|
||||||
|
parse_route_messages(&response, interface_index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn route_interface_list(
|
||||||
|
interface_index: libc::c_uint,
|
||||||
|
) -> Result<Vec<u8>, MacOsInterfaceStatsError> {
|
||||||
|
let mut mib = [
|
||||||
|
libc::CTL_NET,
|
||||||
|
libc::PF_ROUTE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
libc::NET_RT_IFLIST2,
|
||||||
|
interface_index as libc::c_int,
|
||||||
|
];
|
||||||
|
|
||||||
|
for attempt in 0..SYSCTL_FETCH_ATTEMPTS {
|
||||||
|
let mut required_len = 0;
|
||||||
|
// SAFETY: `mib` has the declared six elements, `required_len` is writable, and the
|
||||||
|
// null data pointers request only the response size without reading or writing data.
|
||||||
|
let size_result = unsafe {
|
||||||
|
libc::sysctl(
|
||||||
|
mib.as_mut_ptr(),
|
||||||
|
mib.len() as libc::c_uint,
|
||||||
|
ptr::null_mut(),
|
||||||
|
&mut required_len,
|
||||||
|
ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if size_result != 0 {
|
||||||
|
return Err(MacOsInterfaceStatsError::Sysctl {
|
||||||
|
operation: "querying the interface-list buffer size",
|
||||||
|
source: io::Error::last_os_error(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response = vec![0_u8; required_len];
|
||||||
|
let mut returned_len = required_len;
|
||||||
|
// SAFETY: `response` owns `required_len` initialized bytes, `returned_len` describes
|
||||||
|
// that allocation, and all other pointers remain valid for the duration of the call.
|
||||||
|
let fetch_result = unsafe {
|
||||||
|
libc::sysctl(
|
||||||
|
mib.as_mut_ptr(),
|
||||||
|
mib.len() as libc::c_uint,
|
||||||
|
response.as_mut_ptr().cast(),
|
||||||
|
&mut returned_len,
|
||||||
|
ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if fetch_result == 0 {
|
||||||
|
if returned_len > response.len() {
|
||||||
|
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
|
||||||
|
"sysctl returned length {returned_len} for a {}-byte buffer",
|
||||||
|
response.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
response.truncate(returned_len);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = io::Error::last_os_error();
|
||||||
|
let buffer_too_small = matches!(source.raw_os_error(), Some(libc::ENOMEM | libc::ENOBUFS));
|
||||||
|
if !buffer_too_small || attempt + 1 == SYSCTL_FETCH_ATTEMPTS {
|
||||||
|
return Err(MacOsInterfaceStatsError::Sysctl {
|
||||||
|
operation: "fetching the interface list",
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!("the bounded sysctl retry loop always returns")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_route_messages(
|
||||||
|
response: &[u8],
|
||||||
|
requested_interface_index: libc::c_uint,
|
||||||
|
) -> Result<InterfaceStats, MacOsInterfaceStatsError> {
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
while offset < response.len() {
|
||||||
|
let remaining = response.len() - offset;
|
||||||
|
if remaining < ROUTE_MESSAGE_PREFIX_LEN {
|
||||||
|
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
|
||||||
|
"truncated route-message prefix at offset {offset}: {remaining} bytes remain"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let message_len = usize::from(u16::from_ne_bytes([response[offset], response[offset + 1]]));
|
||||||
|
if message_len < ROUTE_MESSAGE_PREFIX_LEN {
|
||||||
|
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
|
||||||
|
"route message at offset {offset} has implausible length {message_len}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let message_end = offset.checked_add(message_len).ok_or_else(|| {
|
||||||
|
MacOsInterfaceStatsError::MalformedResponse(format!(
|
||||||
|
"route message length overflows at offset {offset}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if message_end > response.len() {
|
||||||
|
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
|
||||||
|
"route message at offset {offset} declares length {message_len}, exceeding the {}-byte response",
|
||||||
|
response.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let message_type = response[offset + 3];
|
||||||
|
if message_type == libc::RTM_IFINFO2 as u8 {
|
||||||
|
if message_len < size_of::<libc::if_msghdr2>() {
|
||||||
|
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
|
||||||
|
"RTM_IFINFO2 message at offset {offset} has length {message_len}, smaller than {}",
|
||||||
|
size_of::<libc::if_msghdr2>()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: the bounds checks above prove that a complete `if_msghdr2` lies within
|
||||||
|
// `response`; route messages are not guaranteed to have Rust alignment, so the value
|
||||||
|
// is copied with `read_unaligned` instead of creating a reference into the byte slice.
|
||||||
|
let header = unsafe {
|
||||||
|
ptr::read_unaligned(response.as_ptr().add(offset).cast::<libc::if_msghdr2>())
|
||||||
|
};
|
||||||
|
if libc::c_uint::from(header.ifm_index) == requested_interface_index {
|
||||||
|
return Ok(InterfaceStats {
|
||||||
|
rx_bytes: header.ifm_data.ifi_ibytes,
|
||||||
|
tx_bytes: header.ifm_data.ifi_obytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = message_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(MacOsInterfaceStatsError::NoInterfaceStats {
|
||||||
|
interface_index: requested_interface_index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type PlatformInterfaceStats = MacOsInterfaceStats;
|
||||||
|
pub(crate) type PlatformTrafficControl = NoopTrafficControl;
|
||||||
|
|
||||||
|
pub(crate) fn interface_stats_provider() -> PlatformInterfaceStats {
|
||||||
|
MacOsInterfaceStats
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn traffic_control_backend() -> PlatformTrafficControl {
|
||||||
|
NoopTrafficControl
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn warn_platform_limitations() {
|
||||||
|
warn!(
|
||||||
|
"Traffic control is unavailable on macOS; requested rate changes will be calculated but not applied."
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct InterfaceStats {
|
||||||
|
pub(crate) rx_bytes: u64,
|
||||||
|
pub(crate) tx_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait InterfaceStatsProvider: Send {
|
||||||
|
type Error: Error + Send + Sync + 'static;
|
||||||
|
|
||||||
|
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait TrafficControlBackend: Send {
|
||||||
|
type Error: Error + Send + Sync + 'static;
|
||||||
|
type Handle: Send + 'static;
|
||||||
|
|
||||||
|
fn find_shaper(&mut self, interface: &str) -> Result<Self::Handle, Self::Error>;
|
||||||
|
|
||||||
|
fn set_rate(
|
||||||
|
&mut self,
|
||||||
|
shaper: &Self::Handle,
|
||||||
|
bandwidth_kbit: u64,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<(), Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
mod noop;
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
mod freebsd;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub mod unix;
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "macos")))]
|
||||||
|
mod unsupported;
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
use freebsd as imp;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use linux as imp;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use macos as imp;
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "macos")))]
|
||||||
|
use unsupported as imp;
|
||||||
|
|
||||||
|
pub(crate) use imp::{
|
||||||
|
PlatformInterfaceStats, PlatformTrafficControl, interface_stats_provider,
|
||||||
|
traffic_control_backend, warn_platform_limitations,
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use crate::platform::TrafficControlBackend;
|
||||||
|
use log::debug;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct NoopShaper {
|
||||||
|
interface: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct NoopTrafficControl;
|
||||||
|
|
||||||
|
impl TrafficControlBackend for NoopTrafficControl {
|
||||||
|
type Error = Infallible;
|
||||||
|
type Handle = NoopShaper;
|
||||||
|
|
||||||
|
fn find_shaper(&mut self, interface: &str) -> Result<Self::Handle, Self::Error> {
|
||||||
|
Ok(NoopShaper {
|
||||||
|
interface: interface.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_rate(
|
||||||
|
&mut self,
|
||||||
|
shaper: &Self::Handle,
|
||||||
|
bandwidth_kbit: u64,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
debug!(
|
||||||
|
"Ignoring traffic-control rate request of {} kbit/s for interface {} (dry_run={})",
|
||||||
|
bandwidth_kbit, shaper.interface, dry_run
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#[cfg(feature = "uci")]
|
||||||
|
pub mod uci;
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::settings::uci_schema::{UciConfigSchema, UciSectionMapping};
|
||||||
|
use config::{ConfigError as SourceError, Map, Source, Value, ValueKind};
|
||||||
|
|
||||||
|
use rust_uci::config::{
|
||||||
|
Config as UciConfig, Package as UciPackage, SectionSelector, Value as UciValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum PackageSelection {
|
||||||
|
Selected(Vec<String>),
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct UciSource {
|
||||||
|
packages: PackageSelection,
|
||||||
|
required: bool,
|
||||||
|
schema: &'static [UciSectionMapping],
|
||||||
|
config_dir: Option<PathBuf>,
|
||||||
|
save_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UciSource {
|
||||||
|
fn with_selection(packages: PackageSelection) -> Self {
|
||||||
|
Self {
|
||||||
|
packages,
|
||||||
|
required: true,
|
||||||
|
schema: &[],
|
||||||
|
config_dir: None,
|
||||||
|
save_dir: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new<I, S>(packages: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = S>,
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
Self::with_selection(PackageSelection::Selected(
|
||||||
|
packages.into_iter().map(Into::into).collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn all() -> Self {
|
||||||
|
Self::with_selection(PackageSelection::All)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn required(mut self, required: bool) -> Self {
|
||||||
|
self.required = required;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_schema<T>() -> Self
|
||||||
|
where
|
||||||
|
T: UciConfigSchema,
|
||||||
|
{
|
||||||
|
Self::new([T::UCI_PACKAGE]).with_schema(T::UCI_SECTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_schema(mut self, schema: &'static [UciSectionMapping]) -> Self {
|
||||||
|
self.schema = schema;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn with_directories(
|
||||||
|
mut self,
|
||||||
|
config_dir: impl AsRef<Path>,
|
||||||
|
save_dir: impl AsRef<Path>,
|
||||||
|
) -> Self {
|
||||||
|
self.config_dir = Some(config_dir.as_ref().to_owned());
|
||||||
|
self.save_dir = Some(save_dir.as_ref().to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&self) -> Result<UciConfig, SourceError> {
|
||||||
|
let mut uci = rust_uci::Uci::new()
|
||||||
|
.map_err(|e| SourceError::Message(format!("failed to open UCI: {e}")))?;
|
||||||
|
|
||||||
|
if let Some(config_dir) = &self.config_dir {
|
||||||
|
uci.set_config_dir(config_dir).map_err(|e| {
|
||||||
|
SourceError::Message(format!(
|
||||||
|
"failed to set UCI config directory {config_dir:?}: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(save_dir) = &self.save_dir {
|
||||||
|
uci.set_save_dir(save_dir).map_err(|e| {
|
||||||
|
SourceError::Message(format!(
|
||||||
|
"failed to set UCI save directory {save_dir:?}: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(uci.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Converts a [`SectionSelector`] into a string representation.
|
||||||
|
/// Anonymous sections are converted to `section_type[index]` format,
|
||||||
|
/// because config-rs does not allow @-prefixed section names.
|
||||||
|
fn config_selector(selector: &SectionSelector<'_>) -> String {
|
||||||
|
match selector {
|
||||||
|
SectionSelector::Named(name) => (*name).to_owned(),
|
||||||
|
|
||||||
|
SectionSelector::Anonymous {
|
||||||
|
section_type,
|
||||||
|
index,
|
||||||
|
} => format!("{section_type}[{index}]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mapped_config_key(
|
||||||
|
&self,
|
||||||
|
selector: &SectionSelector<'_>,
|
||||||
|
uci_option: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
let section_type = match selector {
|
||||||
|
SectionSelector::Anonymous {
|
||||||
|
section_type,
|
||||||
|
index,
|
||||||
|
} if *index == 0 => *section_type,
|
||||||
|
|
||||||
|
SectionSelector::Anonymous { .. } | SectionSelector::Named(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.schema
|
||||||
|
.iter()
|
||||||
|
.filter(|section| section.uci_section == section_type)
|
||||||
|
.find_map(|section| {
|
||||||
|
section
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.find(|option| option.uci_option == uci_option)
|
||||||
|
.map(|option| format!("{}.{}", section.config_section, option.config_field,))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_package(
|
||||||
|
&self,
|
||||||
|
package_name: &str,
|
||||||
|
package: &UciPackage,
|
||||||
|
values: &mut Map<String, Value>,
|
||||||
|
) -> Result<(), SourceError> {
|
||||||
|
let sections = package.sections().map_err(|e| {
|
||||||
|
SourceError::Message(format!(
|
||||||
|
"failed to retrieve sections from UCI package \
|
||||||
|
{package_name:?}: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for section in sections {
|
||||||
|
let selector = section.selector().ok_or_else(|| {
|
||||||
|
SourceError::Message(format!(
|
||||||
|
"loaded UCI section of type {:?} in package \
|
||||||
|
{package_name:?} has no selector",
|
||||||
|
section.type_()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let options = section.options().map_err(|e| {
|
||||||
|
SourceError::Message(format!(
|
||||||
|
"failed to retrieve options from UCI section \
|
||||||
|
{package_name}.{selector}: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for option in options {
|
||||||
|
let option_name = option.name();
|
||||||
|
|
||||||
|
let uci_relative_key = format!("{selector}.{option_name}");
|
||||||
|
|
||||||
|
let config_relative_key =
|
||||||
|
format!("{}.{option_name}", Self::config_selector(&selector),);
|
||||||
|
|
||||||
|
let uci_key = format!("{package_name}.{uci_relative_key}");
|
||||||
|
|
||||||
|
let source_key = format!("{package_name}.{config_relative_key}");
|
||||||
|
|
||||||
|
let destination_key = self
|
||||||
|
.mapped_config_key(&selector, option_name)
|
||||||
|
.unwrap_or(source_key);
|
||||||
|
|
||||||
|
let Some(value) = option.get().map_err(|e| {
|
||||||
|
SourceError::Message(format!(
|
||||||
|
"failed to retrieve UCI option \
|
||||||
|
{uci_key}: {e}"
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let origin = format!("UCI option {uci_key}");
|
||||||
|
|
||||||
|
let value = match value {
|
||||||
|
UciValue::String(value) => Value::new(Some(&origin), ValueKind::String(value)),
|
||||||
|
|
||||||
|
UciValue::List(items) => {
|
||||||
|
let items = items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| Value::new(Some(&origin), ValueKind::String(item)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Value::new(Some(&origin), ValueKind::Array(items))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if values.insert(destination_key.clone(), value).is_some() {
|
||||||
|
return Err(SourceError::Message(format!(
|
||||||
|
"duplicate UCI destination key \
|
||||||
|
{destination_key:?} while mapping {uci_key:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for UciSource {
|
||||||
|
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect(&self) -> Result<Map<String, Value>, SourceError> {
|
||||||
|
let resolved = (|| {
|
||||||
|
let uci = self.open()?;
|
||||||
|
|
||||||
|
let packages: Vec<UciPackage> = match &self.packages {
|
||||||
|
PackageSelection::Selected(package_names) => package_names
|
||||||
|
.iter()
|
||||||
|
.map(|package_name| {
|
||||||
|
uci.package(package_name)
|
||||||
|
.map_err(|e| {
|
||||||
|
SourceError::Message(format!(
|
||||||
|
"failed to open UCI package {package_name:?}: {e}"
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
SourceError::NotFound(format!("UCI package {package_name:?}"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_, SourceError>>()?,
|
||||||
|
|
||||||
|
PackageSelection::All => uci
|
||||||
|
.packages()
|
||||||
|
.map_err(|e| {
|
||||||
|
SourceError::Message(format!("failed to enumerate UCI packages: {e}"))
|
||||||
|
})?
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok::<_, SourceError>((uci, packages))
|
||||||
|
})();
|
||||||
|
|
||||||
|
let (_uci, packages) = match resolved {
|
||||||
|
Ok(resolved) => resolved,
|
||||||
|
|
||||||
|
Err(_) if !self.required => {
|
||||||
|
return Ok(Map::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(error) => {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut values = Map::new();
|
||||||
|
|
||||||
|
for package in packages {
|
||||||
|
let package_name = package
|
||||||
|
.name()
|
||||||
|
.map_err(|e| SourceError::Message(format!("failed to read UCI package name: {e}")))?
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
self.collect_package(&package_name, &package, &mut values)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use crate::platform::noop::NoopTrafficControl;
|
||||||
|
use crate::platform::{InterfaceStats, InterfaceStatsProvider};
|
||||||
|
|
||||||
|
// Synthetic rates are fixed per interface and intentionally conservative. Counter increments are
|
||||||
|
// derived from elapsed monotonic time, so changing the polling frequency does not change the
|
||||||
|
// apparent traffic rate.
|
||||||
|
const SYNTHETIC_RX_KBIT_RANGE: std::ops::RangeInclusive<u64> = 500..=25_000;
|
||||||
|
const SYNTHETIC_TX_KBIT_RANGE: std::ops::RangeInclusive<u64> = 250..=10_000;
|
||||||
|
const INITIAL_COUNTER_BYTES: u64 = 1;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SyntheticInterfaceState {
|
||||||
|
rx_bytes: u64,
|
||||||
|
tx_bytes: u64,
|
||||||
|
rx_kbit: u64,
|
||||||
|
tx_kbit: u64,
|
||||||
|
last_update: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyntheticInterfaceState {
|
||||||
|
fn new(now: Instant) -> Self {
|
||||||
|
Self {
|
||||||
|
rx_bytes: INITIAL_COUNTER_BYTES,
|
||||||
|
tx_bytes: INITIAL_COUNTER_BYTES,
|
||||||
|
rx_kbit: fastrand::u64(SYNTHETIC_RX_KBIT_RANGE),
|
||||||
|
tx_kbit: fastrand::u64(SYNTHETIC_TX_KBIT_RANGE),
|
||||||
|
last_update: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, now: Instant) {
|
||||||
|
let elapsed = now.saturating_duration_since(self.last_update);
|
||||||
|
self.rx_bytes = self
|
||||||
|
.rx_bytes
|
||||||
|
.saturating_add(bytes_for_elapsed(elapsed, self.rx_kbit));
|
||||||
|
self.tx_bytes = self
|
||||||
|
.tx_bytes
|
||||||
|
.saturating_add(bytes_for_elapsed(elapsed, self.tx_kbit));
|
||||||
|
self.last_update = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_for_elapsed(elapsed: Duration, bitrate_kbit: u64) -> u64 {
|
||||||
|
let bytes = elapsed
|
||||||
|
.as_nanos()
|
||||||
|
.saturating_mul(u128::from(bitrate_kbit))
|
||||||
|
.saturating_mul(1_000)
|
||||||
|
/ 8_000_000_000;
|
||||||
|
u64::try_from(bytes).unwrap_or(u64::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct SyntheticInterfaceStats {
|
||||||
|
interfaces: HashMap<String, SyntheticInterfaceState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterfaceStatsProvider for SyntheticInterfaceStats {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error> {
|
||||||
|
let now = Instant::now();
|
||||||
|
let state = self
|
||||||
|
.interfaces
|
||||||
|
.entry(interface.to_string())
|
||||||
|
.or_insert_with(|| SyntheticInterfaceState::new(now));
|
||||||
|
state.update(now);
|
||||||
|
|
||||||
|
Ok(InterfaceStats {
|
||||||
|
rx_bytes: state.rx_bytes,
|
||||||
|
tx_bytes: state.tx_bytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type PlatformInterfaceStats = SyntheticInterfaceStats;
|
||||||
|
pub(crate) type PlatformTrafficControl = NoopTrafficControl;
|
||||||
|
|
||||||
|
pub(crate) fn interface_stats_provider() -> PlatformInterfaceStats {
|
||||||
|
SyntheticInterfaceStats::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn traffic_control_backend() -> PlatformTrafficControl {
|
||||||
|
NoopTrafficControl
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn warn_platform_limitations() {
|
||||||
|
warn!(
|
||||||
|
"This operating system has no native interface-statistics or traffic-control backend; synthetic interface load will be used and requested rate changes will not be applied."
|
||||||
|
);
|
||||||
|
}
|
||||||
+344
-204
@@ -1,21 +1,29 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
|
use crate::baseliner::ControlSnapshot;
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::netlink::{Netlink, NetlinkError, Qdisc};
|
use crate::platform::{
|
||||||
|
InterfaceStatsProvider, PlatformInterfaceStats, PlatformTrafficControl, TrafficControlBackend,
|
||||||
|
interface_stats_provider, traffic_control_backend,
|
||||||
|
};
|
||||||
|
use crate::settings::Settings;
|
||||||
use crate::time::Time;
|
use crate::time::Time;
|
||||||
use crate::util::{MutexExt, RwLockExt};
|
use crate::util::{ArcRwLock, RwLockExt};
|
||||||
use crate::{Config, ReflectorStats};
|
use flume::{Receiver, Sender};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use rustix::thread::ClockId;
|
use rustix::time::ClockId;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
enum Direction {
|
enum Direction {
|
||||||
@@ -23,56 +31,32 @@ enum Direction {
|
|||||||
Up,
|
Up,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
fn generate_initial_speeds(rng: &mut fastrand::Rng, base_speed: f64, size: u32) -> Vec<f64> {
|
||||||
pub enum RatecontrolError {
|
|
||||||
#[error("Netlink error")]
|
|
||||||
Netlink(#[from] NetlinkError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum StatsDirection {
|
|
||||||
RX,
|
|
||||||
TX,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_initial_speeds(base_speed: f64, size: u32) -> Vec<f64> {
|
|
||||||
let mut rates = Vec::new();
|
let mut rates = Vec::new();
|
||||||
|
|
||||||
for _ in 0..size {
|
for _ in 0..size {
|
||||||
rates.push((fastrand::f64() * 0.2 + 0.75) * base_speed);
|
rates.push((rng.f64() * 0.2 + 0.75) * base_speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
rates
|
rates
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_interface_stats(
|
fn get_interface_stats<S: InterfaceStatsProvider>(
|
||||||
config: &Config,
|
stats_provider: &mut S,
|
||||||
down_direction: StatsDirection,
|
upload_interface: &str,
|
||||||
up_direction: StatsDirection,
|
) -> Result<(i128, i128), S::Error> {
|
||||||
) -> Result<(i128, i128), RatecontrolError> {
|
let stats = stats_provider.read_stats(upload_interface)?;
|
||||||
let (down_rx, down_tx) = Netlink::get_interface_stats(config.download_interface.as_str())?;
|
|
||||||
let (up_rx, up_tx) = Netlink::get_interface_stats(config.upload_interface.as_str())?;
|
|
||||||
|
|
||||||
let rx_bytes = match down_direction {
|
Ok((stats.rx_bytes.into(), stats.tx_bytes.into()))
|
||||||
StatsDirection::RX => down_rx,
|
|
||||||
StatsDirection::TX => down_tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
let tx_bytes = match up_direction {
|
|
||||||
StatsDirection::RX => up_rx,
|
|
||||||
StatsDirection::TX => up_tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((rx_bytes.into(), tx_bytes.into()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct State {
|
struct State<H> {
|
||||||
current_bytes: i128,
|
current_bytes: i128,
|
||||||
current_rate: f64,
|
current_rate: f64,
|
||||||
delta_stat: f64,
|
delta_stat: f64,
|
||||||
deltas: Vec<f64>,
|
deltas: Vec<f64>,
|
||||||
qdisc: Qdisc,
|
shaper: H,
|
||||||
load: f64,
|
load: f64,
|
||||||
next_rate: f64,
|
next_rate: f64,
|
||||||
nrate: usize,
|
nrate: usize,
|
||||||
@@ -82,8 +66,8 @@ struct State {
|
|||||||
utilisation: f64,
|
utilisation: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl<H> State<H> {
|
||||||
fn new(qdisc: Qdisc, previous_bytes: i128, safe_rates: Vec<f64>) -> Self {
|
fn new(shaper: H, previous_bytes: i128, safe_rates: Vec<f64>) -> Self {
|
||||||
State {
|
State {
|
||||||
current_bytes: 0,
|
current_bytes: 0,
|
||||||
current_rate: 0.0,
|
current_rate: 0.0,
|
||||||
@@ -92,7 +76,7 @@ impl State {
|
|||||||
load: 0.0,
|
load: 0.0,
|
||||||
next_rate: 0.0,
|
next_rate: 0.0,
|
||||||
nrate: 0,
|
nrate: 0,
|
||||||
qdisc,
|
shaper,
|
||||||
previous_bytes,
|
previous_bytes,
|
||||||
prev_t: Instant::now(),
|
prev_t: Instant::now(),
|
||||||
safe_rates,
|
safe_rates,
|
||||||
@@ -101,33 +85,146 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Ratecontroller {
|
fn drain_latest_snapshot(
|
||||||
config: Config,
|
snapshot_rx: &Receiver<ControlSnapshot>,
|
||||||
down_direction: StatsDirection,
|
latest: &mut Option<ControlSnapshot>,
|
||||||
owd_baseline: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
) {
|
||||||
owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
while let Ok(snapshot) = snapshot_rx.try_recv() {
|
||||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
if latest
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|current| snapshot.generated_at >= current.generated_at)
|
||||||
|
{
|
||||||
|
*latest = Some(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deltas_from_snapshot(
|
||||||
|
snapshot: Option<&ControlSnapshot>,
|
||||||
|
reflectors: &[IpAddr],
|
||||||
|
now: Instant,
|
||||||
|
tick_interval: f64,
|
||||||
|
) -> (Vec<f64>, Vec<f64>) {
|
||||||
|
let mut down = Vec::new();
|
||||||
|
let mut up = Vec::new();
|
||||||
|
|
||||||
|
let Some(snapshot) = snapshot else {
|
||||||
|
return (down, up);
|
||||||
|
};
|
||||||
|
|
||||||
|
for reflector in reflectors {
|
||||||
|
let Some(state) = snapshot.reflectors.get(reflector) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if now.duration_since(state.last_receive_at).as_secs_f64() < tick_interval * 2.0 {
|
||||||
|
let down_delta = state.recent.down - state.baseline.down;
|
||||||
|
let up_delta = state.recent.up - state.baseline.up;
|
||||||
|
down.push(down_delta);
|
||||||
|
up.push(up_delta);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Reflector: {} down_delay: {} up_delay: {}",
|
||||||
|
reflector, down_delta, up_delta
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
down.sort_by(|a, b| a.total_cmp(b));
|
||||||
|
up.sort_by(|a, b| a.total_cmp(b));
|
||||||
|
|
||||||
|
(down, up)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Ratecontroller<S: InterfaceStatsProvider, T: TrafficControlBackend> {
|
||||||
|
settings: Settings,
|
||||||
|
control_snapshot: Option<ControlSnapshot>,
|
||||||
|
control_snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
|
reflectors_lock: ArcRwLock<Vec<IpAddr>>,
|
||||||
reselect_trigger: Sender<bool>,
|
reselect_trigger: Sender<bool>,
|
||||||
state_dl: State,
|
state_dl: State<T::Handle>,
|
||||||
state_ul: State,
|
state_ul: State<T::Handle>,
|
||||||
up_direction: StatsDirection,
|
rng: fastrand::Rng,
|
||||||
|
stats_provider: S,
|
||||||
|
traffic_control: T,
|
||||||
metrics: MetricsSender,
|
metrics: MetricsSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ratecontroller {
|
impl<S: InterfaceStatsProvider, T: TrafficControlBackend> Ratecontroller<S, T> {
|
||||||
|
fn new_with_backends(
|
||||||
|
settings: Settings,
|
||||||
|
control_snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
|
reflectors_lock: ArcRwLock<Vec<IpAddr>>,
|
||||||
|
reselect_trigger: Sender<bool>,
|
||||||
|
metrics: MetricsSender,
|
||||||
|
mut stats_provider: S,
|
||||||
|
mut traffic_control: T,
|
||||||
|
mut rng: fastrand::Rng,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let dl_shaper =
|
||||||
|
traffic_control.find_shaper(settings.network.download_interface.as_str())?;
|
||||||
|
let dl_safe_rates = generate_initial_speeds(
|
||||||
|
&mut rng,
|
||||||
|
settings.network.download_base_kbits,
|
||||||
|
settings.advanced_settings.speed_hist_size,
|
||||||
|
);
|
||||||
|
let ul_shaper = traffic_control.find_shaper(settings.network.upload_interface.as_str())?;
|
||||||
|
let ul_safe_rates = generate_initial_speeds(
|
||||||
|
&mut rng,
|
||||||
|
settings.network.upload_base_kbits,
|
||||||
|
settings.advanced_settings.speed_hist_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (cur_rx, cur_tx) =
|
||||||
|
get_interface_stats(&mut stats_provider, &settings.network.upload_interface)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
settings,
|
||||||
|
control_snapshot: None,
|
||||||
|
control_snapshot_rx,
|
||||||
|
reflectors_lock,
|
||||||
|
reselect_trigger,
|
||||||
|
state_dl: State::new(dl_shaper, cur_rx, dl_safe_rates),
|
||||||
|
state_ul: State::new(ul_shaper, cur_tx, ul_safe_rates),
|
||||||
|
rng,
|
||||||
|
stats_provider,
|
||||||
|
traffic_control,
|
||||||
|
metrics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_initial_rates(&mut self) -> anyhow::Result<()> {
|
||||||
|
// Set rates to 60% of base rate to make sure we start with sane baselines.
|
||||||
|
self.state_dl.current_rate = self.settings.network.download_base_kbits * 0.6;
|
||||||
|
self.state_ul.current_rate = self.settings.network.upload_base_kbits * 0.6;
|
||||||
|
|
||||||
|
self.traffic_control.set_rate(
|
||||||
|
&self.state_dl.shaper,
|
||||||
|
self.state_dl.current_rate.round() as u64,
|
||||||
|
self.settings.advanced_settings.dry_run,
|
||||||
|
)?;
|
||||||
|
self.traffic_control.set_rate(
|
||||||
|
&self.state_ul.shaper,
|
||||||
|
self.state_ul.current_rate.round() as u64,
|
||||||
|
self.settings.advanced_settings.dry_run,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn calculate_rate(&mut self, direction: Direction) -> anyhow::Result<()> {
|
fn calculate_rate(&mut self, direction: Direction) -> anyhow::Result<()> {
|
||||||
let (base_rate, delay_ms, min_rate, state) = if direction == Direction::Down {
|
let (base_rate, delay_ms, min_rate, state) = if direction == Direction::Down {
|
||||||
(
|
(
|
||||||
self.config.download_base_kbits,
|
self.settings.network.download_base_kbits,
|
||||||
self.config.download_delay_ms,
|
self.settings.advanced_settings.download_delay_ms,
|
||||||
self.config.download_min_kbits,
|
self.settings.network.download_min_kbits(),
|
||||||
&mut self.state_dl,
|
&mut self.state_dl,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
self.config.upload_base_kbits,
|
self.settings.network.upload_base_kbits,
|
||||||
self.config.upload_delay_ms,
|
self.settings.advanced_settings.upload_delay_ms,
|
||||||
self.config.upload_min_kbits,
|
self.settings.network.upload_min_kbits(),
|
||||||
&mut self.state_ul,
|
&mut self.state_ul,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -155,7 +252,9 @@ impl Ratecontroller {
|
|||||||
/ dur.as_secs_f64();
|
/ dur.as_secs_f64();
|
||||||
state.load = state.utilisation / state.current_rate;
|
state.load = state.utilisation / state.current_rate;
|
||||||
|
|
||||||
if state.delta_stat < delay_ms && state.load > self.config.high_load_level {
|
if state.delta_stat < delay_ms
|
||||||
|
&& state.load > self.settings.advanced_settings.high_load_level
|
||||||
|
{
|
||||||
state.safe_rates[state.nrate] = (state.current_rate * state.load).floor();
|
state.safe_rates[state.nrate] = (state.current_rate * state.load).floor();
|
||||||
let max_rate = state
|
let max_rate = state
|
||||||
.safe_rates
|
.safe_rates
|
||||||
@@ -166,13 +265,13 @@ impl Ratecontroller {
|
|||||||
* (1.0 + 0.1 * (1.0_f64 - state.current_rate / max_rate).max(0.0))
|
* (1.0 + 0.1 * (1.0_f64 - state.current_rate / max_rate).max(0.0))
|
||||||
+ (base_rate * 0.03);
|
+ (base_rate * 0.03);
|
||||||
state.nrate += 1;
|
state.nrate += 1;
|
||||||
state.nrate %= self.config.speed_hist_size as usize;
|
state.nrate %= self.settings.advanced_settings.speed_hist_size as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.delta_stat > delay_ms {
|
if state.delta_stat > delay_ms {
|
||||||
match state
|
match state
|
||||||
.safe_rates
|
.safe_rates
|
||||||
.get(fastrand::usize(..state.safe_rates.len()))
|
.get(self.rng.usize(..state.safe_rates.len()))
|
||||||
{
|
{
|
||||||
Some(rnd_rate) => {
|
Some(rnd_rate) => {
|
||||||
state.next_rate = rnd_rate.min(0.9 * state.current_rate * state.load);
|
state.next_rate = rnd_rate.min(0.9 * state.current_rate * state.load);
|
||||||
@@ -192,121 +291,26 @@ impl Ratecontroller {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_deltas(&mut self) -> anyhow::Result<()> {
|
|
||||||
let state_dl = &mut self.state_dl;
|
|
||||||
let state_ul = &mut self.state_ul;
|
|
||||||
|
|
||||||
state_dl.deltas.clear();
|
|
||||||
state_ul.deltas.clear();
|
|
||||||
|
|
||||||
let now_t = Instant::now();
|
|
||||||
let owd_baseline = self.owd_baseline.lock_anyhow()?;
|
|
||||||
let owd_recent = self.owd_recent.lock_anyhow()?;
|
|
||||||
let reflectors = self.reflectors_lock.read_anyhow()?;
|
|
||||||
|
|
||||||
for reflector in reflectors.iter() {
|
|
||||||
// only consider this data if it's less than 2 * tick_duration seconds old
|
|
||||||
if owd_baseline.contains_key(reflector)
|
|
||||||
&& owd_recent.contains_key(reflector)
|
|
||||||
&& now_t
|
|
||||||
.duration_since(owd_recent[reflector].last_receive_time_s)
|
|
||||||
.as_secs_f64()
|
|
||||||
< self.config.tick_interval * 2.0
|
|
||||||
{
|
|
||||||
state_dl
|
|
||||||
.deltas
|
|
||||||
.push(owd_recent[reflector].down_ewma - owd_baseline[reflector].down_ewma);
|
|
||||||
state_ul
|
|
||||||
.deltas
|
|
||||||
.push(owd_recent[reflector].up_ewma - owd_baseline[reflector].up_ewma);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Reflector: {} down_delay: {} up_delay: {}",
|
|
||||||
reflector,
|
|
||||||
state_dl.deltas.last().unwrap(),
|
|
||||||
state_ul.deltas.last().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort owd's lowest to highest
|
|
||||||
state_dl.deltas.sort_by(|a, b| a.total_cmp(b));
|
|
||||||
state_ul.deltas.sort_by(|a, b| a.total_cmp(b));
|
|
||||||
|
|
||||||
if state_dl.deltas.len() < 5 || state_ul.deltas.len() < 5 {
|
|
||||||
// trigger reselection
|
|
||||||
warn!("Not enough delta values, triggering reselection");
|
|
||||||
let _ = self.reselect_trigger.send(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
config: Config,
|
|
||||||
owd_baseline: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
|
||||||
owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
|
||||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
|
||||||
reselect_trigger: Sender<bool>,
|
|
||||||
down_direction: StatsDirection,
|
|
||||||
up_direction: StatsDirection,
|
|
||||||
metrics: MetricsSender,
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
let dl_qdisc = Netlink::qdisc_from_ifname(config.download_interface.as_str())?;
|
|
||||||
let dl_safe_rates =
|
|
||||||
generate_initial_speeds(config.download_base_kbits, config.speed_hist_size);
|
|
||||||
let ul_qdisc = Netlink::qdisc_from_ifname(config.upload_interface.as_str())?;
|
|
||||||
let ul_safe_rates =
|
|
||||||
generate_initial_speeds(config.upload_base_kbits, config.speed_hist_size);
|
|
||||||
|
|
||||||
let (cur_rx, cur_tx) = get_interface_stats(&config, down_direction, up_direction)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
down_direction,
|
|
||||||
owd_baseline,
|
|
||||||
owd_recent,
|
|
||||||
reflectors_lock,
|
|
||||||
reselect_trigger,
|
|
||||||
state_dl: State::new(dl_qdisc, cur_rx, dl_safe_rates),
|
|
||||||
state_ul: State::new(ul_qdisc, cur_tx, ul_safe_rates),
|
|
||||||
up_direction,
|
|
||||||
metrics,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(&mut self) -> anyhow::Result<()> {
|
pub fn run(&mut self) -> anyhow::Result<()> {
|
||||||
let sleep_time = Duration::from_secs_f64(self.config.min_change_interval);
|
let sleep_time =
|
||||||
|
Duration::from_secs_f64(self.settings.advanced_settings.min_change_interval);
|
||||||
|
|
||||||
let mut lastchg_t = Instant::now();
|
let mut lastchg_t = Instant::now();
|
||||||
let mut lastdump_t = Instant::now();
|
let mut lastdump_t = Instant::now();
|
||||||
|
|
||||||
// set qdisc rates to 60% of base rate to make sure we start with sane baselines
|
self.request_initial_rates()?;
|
||||||
self.state_dl.current_rate = self.config.download_base_kbits * 0.6;
|
|
||||||
self.state_ul.current_rate = self.config.upload_base_kbits * 0.6;
|
|
||||||
|
|
||||||
Netlink::set_qdisc_rate(
|
|
||||||
self.state_dl.qdisc,
|
|
||||||
self.state_dl.current_rate.round() as u64,
|
|
||||||
self.config.dry_run,
|
|
||||||
)?;
|
|
||||||
Netlink::set_qdisc_rate(
|
|
||||||
self.state_ul.qdisc,
|
|
||||||
self.state_ul.current_rate.round() as u64,
|
|
||||||
self.config.dry_run,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut speed_hist_fd: Option<File> = None;
|
let mut speed_hist_fd: Option<File> = None;
|
||||||
let mut speed_hist_fd_inner: File;
|
let mut speed_hist_fd_inner: File;
|
||||||
let mut stats_fd: Option<File> = None;
|
let mut stats_fd: Option<File> = None;
|
||||||
let mut stats_fd_inner: File;
|
let mut stats_fd_inner: File;
|
||||||
|
|
||||||
if !self.config.suppress_statistics {
|
if !self.settings.output.suppress_statistics {
|
||||||
speed_hist_fd_inner = File::options()
|
speed_hist_fd_inner = File::options()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.open(self.config.speed_hist_file.as_str())?;
|
.open(self.settings.output.speed_hist_file.as_str())?;
|
||||||
|
|
||||||
speed_hist_fd_inner.write_all("time,counter,upspeed,downspeed\n".as_bytes())?;
|
speed_hist_fd_inner.write_all("time,counter,upspeed,downspeed\n".as_bytes())?;
|
||||||
speed_hist_fd_inner.flush()?;
|
speed_hist_fd_inner.flush()?;
|
||||||
@@ -317,7 +321,7 @@ impl Ratecontroller {
|
|||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.open(self.config.stats_file.as_str())?;
|
.open(self.settings.output.stats_file.as_str())?;
|
||||||
|
|
||||||
stats_fd_inner.write_all(
|
stats_fd_inner.write_all(
|
||||||
"times,timens,rxload,txload,deltadelaydown,deltadelayup,dlrate,uprate\n".as_bytes(),
|
"times,timens,rxload,txload,deltadelaydown,deltadelayup,dlrate,uprate\n".as_bytes(),
|
||||||
@@ -335,25 +339,16 @@ impl Ratecontroller {
|
|||||||
sleep(sleep_time);
|
sleep(sleep_time);
|
||||||
let now_t = Instant::now();
|
let now_t = Instant::now();
|
||||||
|
|
||||||
if now_t.duration_since(lastchg_t).as_secs_f64() > self.config.min_change_interval {
|
if now_t.duration_since(lastchg_t).as_secs_f64()
|
||||||
|
> self.settings.advanced_settings.min_change_interval
|
||||||
|
{
|
||||||
// if it's been long enough, and the stats indicate needing to change speeds
|
// if it's been long enough, and the stats indicate needing to change speeds
|
||||||
// change speeds here
|
// change speeds here
|
||||||
|
|
||||||
(self.state_dl.current_bytes, self.state_ul.current_bytes) =
|
(self.state_dl.current_bytes, self.state_ul.current_bytes) = get_interface_stats(
|
||||||
get_interface_stats(&self.config, self.down_direction, self.up_direction)?;
|
&mut self.stats_provider,
|
||||||
if self.state_dl.current_bytes == -1 || self.state_ul.current_bytes == -1 {
|
&self.settings.network.upload_interface,
|
||||||
warn!(
|
)?;
|
||||||
"One or both Netlink stats could not be read. Skipping rate control algorithm"
|
|
||||||
);
|
|
||||||
self.metrics.send(Metric::Event {
|
|
||||||
name: "netlink_read_failure",
|
|
||||||
reason: "",
|
|
||||||
reflector: None,
|
|
||||||
tags: &[],
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_deltas()?;
|
self.update_deltas()?;
|
||||||
|
|
||||||
if self.state_dl.deltas.is_empty() || self.state_ul.deltas.is_empty() {
|
if self.state_dl.deltas.is_empty() || self.state_ul.deltas.is_empty() {
|
||||||
@@ -364,18 +359,18 @@ impl Ratecontroller {
|
|||||||
reflector: None,
|
reflector: None,
|
||||||
tags: &[],
|
tags: &[],
|
||||||
});
|
});
|
||||||
self.state_dl.next_rate = self.config.download_min_kbits;
|
self.state_dl.next_rate = self.settings.network.download_min_kbits();
|
||||||
self.state_ul.next_rate = self.config.upload_min_kbits;
|
self.state_ul.next_rate = self.settings.network.upload_min_kbits();
|
||||||
|
|
||||||
Netlink::set_qdisc_rate(
|
self.traffic_control.set_rate(
|
||||||
self.state_dl.qdisc,
|
&self.state_dl.shaper,
|
||||||
self.state_dl.next_rate as u64,
|
self.state_dl.next_rate as u64,
|
||||||
self.config.dry_run,
|
self.settings.advanced_settings.dry_run,
|
||||||
)?;
|
)?;
|
||||||
Netlink::set_qdisc_rate(
|
self.traffic_control.set_rate(
|
||||||
self.state_ul.qdisc,
|
&self.state_ul.shaper,
|
||||||
self.state_ul.next_rate as u64,
|
self.state_ul.next_rate as u64,
|
||||||
self.config.dry_run,
|
self.settings.advanced_settings.dry_run,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.state_dl.current_rate = self.state_dl.next_rate;
|
self.state_dl.current_rate = self.state_dl.next_rate;
|
||||||
@@ -390,24 +385,24 @@ impl Ratecontroller {
|
|||||||
|| self.state_ul.next_rate != self.state_ul.current_rate
|
|| self.state_ul.next_rate != self.state_ul.current_rate
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"self.state_ul.next_rate {} self.state_dl.next_rate {}",
|
"Requesting rates (D/U): {} / {} kbit/s",
|
||||||
self.state_ul.next_rate, self.state_dl.next_rate
|
self.state_dl.next_rate as u64, self.state_ul.next_rate as u64
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.state_dl.next_rate != self.state_dl.current_rate {
|
if self.state_dl.next_rate != self.state_dl.current_rate {
|
||||||
Netlink::set_qdisc_rate(
|
self.traffic_control.set_rate(
|
||||||
self.state_dl.qdisc,
|
&self.state_dl.shaper,
|
||||||
self.state_dl.next_rate as u64,
|
self.state_dl.next_rate as u64,
|
||||||
self.config.dry_run,
|
self.settings.advanced_settings.dry_run,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.state_ul.next_rate != self.state_ul.current_rate {
|
if self.state_ul.next_rate != self.state_ul.current_rate {
|
||||||
Netlink::set_qdisc_rate(
|
self.traffic_control.set_rate(
|
||||||
self.state_ul.qdisc,
|
&self.state_ul.shaper,
|
||||||
self.state_ul.next_rate as u64,
|
self.state_ul.next_rate as u64,
|
||||||
self.config.dry_run,
|
self.settings.advanced_settings.dry_run,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,8 +431,8 @@ impl Ratecontroller {
|
|||||||
delta_delay_up: self.state_ul.delta_stat,
|
delta_delay_up: self.state_ul.delta_stat,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(ref mut fd) = stats_fd {
|
let stats_write_error = stats_fd.as_mut().and_then(|fd| {
|
||||||
if let Err(e) = fd.write_all(
|
fd.write_all(
|
||||||
format!(
|
format!(
|
||||||
"{},{},{},{},{},{},{},{}\n",
|
"{},{},{},{},{},{},{},{}\n",
|
||||||
stats_time.secs(),
|
stats_time.secs(),
|
||||||
@@ -450,17 +445,20 @@ impl Ratecontroller {
|
|||||||
self.state_ul.current_rate as u64
|
self.state_ul.current_rate as u64
|
||||||
)
|
)
|
||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
) {
|
)
|
||||||
|
.err()
|
||||||
|
});
|
||||||
|
if let Some(e) = stats_write_error {
|
||||||
warn!("Failed to write statistics: {}", e);
|
warn!("Failed to write statistics: {}", e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
lastchg_t = now_t;
|
lastchg_t = now_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mut fd) = speed_hist_fd {
|
if let Some(ref mut fd) = speed_hist_fd
|
||||||
if now_t.duration_since(lastdump_t).as_secs_f64() > 300.0 {
|
&& now_t.duration_since(lastdump_t).as_secs_f64() > 300.0
|
||||||
for i in 0..self.config.speed_hist_size as usize {
|
{
|
||||||
|
for i in 0..self.settings.advanced_settings.speed_hist_size as usize {
|
||||||
let hist_time = Time::new(ClockId::Realtime);
|
let hist_time = Time::new(ClockId::Realtime);
|
||||||
if let Err(e) = fd.write_all(
|
if let Err(e) = fd.write_all(
|
||||||
format!(
|
format!(
|
||||||
@@ -480,5 +478,147 @@ impl Ratecontroller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_deltas(&mut self) -> anyhow::Result<()> {
|
||||||
|
drain_latest_snapshot(&self.control_snapshot_rx, &mut self.control_snapshot);
|
||||||
|
let now_t = Instant::now();
|
||||||
|
let reflectors = self.reflectors_lock.read_anyhow()?;
|
||||||
|
let (down_deltas, up_deltas) = deltas_from_snapshot(
|
||||||
|
self.control_snapshot.as_ref(),
|
||||||
|
&reflectors,
|
||||||
|
now_t,
|
||||||
|
self.settings.advanced_settings.tick_interval,
|
||||||
|
);
|
||||||
|
self.state_dl.deltas = down_deltas;
|
||||||
|
self.state_ul.deltas = up_deltas;
|
||||||
|
|
||||||
|
if self.state_dl.deltas.len() < 5 || self.state_ul.deltas.len() < 5 {
|
||||||
|
// trigger reselection
|
||||||
|
warn!(
|
||||||
|
"Not enough delta values (D: {}, U: {}, need 5), triggering reselection",
|
||||||
|
self.state_dl.deltas.len(),
|
||||||
|
self.state_ul.deltas.len()
|
||||||
|
);
|
||||||
|
let _ = self.reselect_trigger.try_send(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ratecontroller<PlatformInterfaceStats, PlatformTrafficControl> {
|
||||||
|
pub fn new(
|
||||||
|
settings: Settings,
|
||||||
|
control_snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
|
reflectors_lock: ArcRwLock<Vec<IpAddr>>,
|
||||||
|
reselect_trigger: Sender<bool>,
|
||||||
|
metrics: MetricsSender,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
Self::new_with_backends(
|
||||||
|
settings,
|
||||||
|
control_snapshot_rx,
|
||||||
|
reflectors_lock,
|
||||||
|
reselect_trigger,
|
||||||
|
metrics,
|
||||||
|
interface_stats_provider(),
|
||||||
|
traffic_control_backend(),
|
||||||
|
fastrand::Rng::new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::baseliner::{EwmaStats, ReflectorState};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_seed_reproduces_initial_safe_rates() {
|
||||||
|
let mut first = fastrand::Rng::with_seed(42);
|
||||||
|
let mut second = fastrand::Rng::with_seed(42);
|
||||||
|
|
||||||
|
let first_rates = generate_initial_speeds(&mut first, 100_000.0, 100);
|
||||||
|
let second_rates = generate_initial_speeds(&mut second, 100_000.0, 100);
|
||||||
|
|
||||||
|
assert_eq!(first_rates, second_rates);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reflector_state(
|
||||||
|
baseline_down: f64,
|
||||||
|
baseline_up: f64,
|
||||||
|
recent_down: f64,
|
||||||
|
recent_up: f64,
|
||||||
|
last_receive_at: Instant,
|
||||||
|
) -> ReflectorState {
|
||||||
|
ReflectorState {
|
||||||
|
baseline: EwmaStats {
|
||||||
|
down: baseline_down,
|
||||||
|
up: baseline_up,
|
||||||
|
},
|
||||||
|
recent: EwmaStats {
|
||||||
|
down: recent_down,
|
||||||
|
up: recent_up,
|
||||||
|
},
|
||||||
|
last_receive_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(generated_at: Instant, marker: f64) -> ControlSnapshot {
|
||||||
|
let reflector = "192.0.2.1".parse().unwrap();
|
||||||
|
ControlSnapshot {
|
||||||
|
generated_at,
|
||||||
|
reflectors: HashMap::from([(
|
||||||
|
reflector,
|
||||||
|
reflector_state(marker, marker, marker, marker, generated_at),
|
||||||
|
)]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drains_queued_snapshots_and_keeps_the_newest() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let newest_at = start + Duration::from_secs(2);
|
||||||
|
let (tx, rx) = flume::unbounded();
|
||||||
|
tx.send(snapshot(start + Duration::from_secs(1), 1.0))
|
||||||
|
.unwrap();
|
||||||
|
tx.send(snapshot(newest_at, 2.0)).unwrap();
|
||||||
|
let mut latest = None;
|
||||||
|
|
||||||
|
drain_latest_snapshot(&rx, &mut latest);
|
||||||
|
|
||||||
|
let latest = latest.unwrap();
|
||||||
|
assert!(rx.is_empty());
|
||||||
|
assert_eq!(latest.generated_at, newest_at);
|
||||||
|
assert_eq!(
|
||||||
|
latest.reflectors[&"192.0.2.1".parse().unwrap()].recent.down,
|
||||||
|
2.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn calculates_sorted_deltas_from_active_fresh_snapshot_state() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let first: IpAddr = "192.0.2.1".parse().unwrap();
|
||||||
|
let second: IpAddr = "192.0.2.2".parse().unwrap();
|
||||||
|
let stale: IpAddr = "192.0.2.3".parse().unwrap();
|
||||||
|
let inactive: IpAddr = "192.0.2.4".parse().unwrap();
|
||||||
|
let snapshot = ControlSnapshot {
|
||||||
|
generated_at: now,
|
||||||
|
reflectors: HashMap::from([
|
||||||
|
(first, reflector_state(10.0, 20.0, 14.0, 28.0, now)),
|
||||||
|
(second, reflector_state(10.0, 20.0, 11.0, 22.0, now)),
|
||||||
|
(
|
||||||
|
stale,
|
||||||
|
reflector_state(10.0, 20.0, 110.0, 220.0, now - Duration::from_secs(3)),
|
||||||
|
),
|
||||||
|
(inactive, reflector_state(10.0, 20.0, 60.0, 80.0, now)),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (down, up) = deltas_from_snapshot(Some(&snapshot), &[first, second, stale], now, 1.0);
|
||||||
|
|
||||||
|
assert_eq!(down, vec![1.0, 4.0]);
|
||||||
|
assert_eq!(up, vec![2.0, 8.0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+283
-30
@@ -1,19 +1,112 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
|
use crate::baseliner::ControlSnapshot;
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::util::{MutexExt, RwLockExt};
|
use crate::settings::Settings;
|
||||||
use crate::{Config, ReflectorStats};
|
use crate::util::RwLockExt;
|
||||||
|
use flume::{Receiver, RecvError, RecvTimeoutError, Selector};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::{Arc, RwLock};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
enum SelectorEvent {
|
||||||
|
Trigger(Result<bool, RecvError>),
|
||||||
|
Snapshot(Result<ControlSnapshot, RecvError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
enum WaitOutcome {
|
||||||
|
Triggered,
|
||||||
|
DeadlineReached,
|
||||||
|
ChannelsClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_trigger_or_deadline(
|
||||||
|
trigger_rx: &Receiver<bool>,
|
||||||
|
snapshot_rx: &Receiver<ControlSnapshot>,
|
||||||
|
latest_snapshot: &mut Option<ControlSnapshot>,
|
||||||
|
deadline: Instant,
|
||||||
|
) -> WaitOutcome {
|
||||||
|
let mut trigger_connected = true;
|
||||||
|
let mut snapshot_connected = true;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
return WaitOutcome::DeadlineReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = match (trigger_connected, snapshot_connected) {
|
||||||
|
(true, true) => Selector::new()
|
||||||
|
.recv(trigger_rx, SelectorEvent::Trigger)
|
||||||
|
.recv(snapshot_rx, SelectorEvent::Snapshot)
|
||||||
|
.wait_deadline(deadline)
|
||||||
|
.ok(),
|
||||||
|
(true, false) => match trigger_rx.recv_deadline(deadline) {
|
||||||
|
Ok(trigger) => Some(SelectorEvent::Trigger(Ok(trigger))),
|
||||||
|
Err(RecvTimeoutError::Timeout) => None,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
Some(SelectorEvent::Trigger(Err(RecvError::Disconnected)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(false, true) => match snapshot_rx.recv_deadline(deadline) {
|
||||||
|
Ok(snapshot) => Some(SelectorEvent::Snapshot(Ok(snapshot))),
|
||||||
|
Err(RecvTimeoutError::Timeout) => None,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
Some(SelectorEvent::Snapshot(Err(RecvError::Disconnected)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(false, false) => return WaitOutcome::ChannelsClosed,
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Some(SelectorEvent::Trigger(Ok(_))) => return WaitOutcome::Triggered,
|
||||||
|
Some(SelectorEvent::Trigger(Err(_))) => trigger_connected = false,
|
||||||
|
Some(SelectorEvent::Snapshot(Ok(snapshot))) => *latest_snapshot = Some(snapshot),
|
||||||
|
Some(SelectorEvent::Snapshot(Err(_))) => snapshot_connected = false,
|
||||||
|
None => return WaitOutcome::DeadlineReached,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_snapshots_until(
|
||||||
|
snapshot_rx: &Receiver<ControlSnapshot>,
|
||||||
|
latest_snapshot: &mut Option<ControlSnapshot>,
|
||||||
|
deadline: Instant,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match snapshot_rx.recv_deadline(deadline) {
|
||||||
|
Ok(snapshot) => *latest_snapshot = Some(snapshot),
|
||||||
|
Err(RecvTimeoutError::Timeout) => return,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
sleep(deadline.saturating_duration_since(Instant::now()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recent_rtt_for_peer(snapshot: Option<&ControlSnapshot>, peer: &IpAddr) -> Option<u64> {
|
||||||
|
snapshot
|
||||||
|
.and_then(|snapshot| snapshot.reflectors.get(peer))
|
||||||
|
.map(|state| (state.recent.down + state.recent.up) as u64)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ReflectorSelector {
|
pub struct ReflectorSelector {
|
||||||
pub config: Config,
|
pub settings: Settings,
|
||||||
pub owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
pub snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
pub reflector_peers_lock: Arc<RwLock<Vec<IpAddr>>>,
|
pub reflector_peers_lock: Arc<RwLock<Vec<IpAddr>>>,
|
||||||
pub reflector_pool: Vec<IpAddr>,
|
pub reflector_pool: Vec<IpAddr>,
|
||||||
pub trigger_channel: Receiver<bool>,
|
pub trigger_channel: Receiver<bool>,
|
||||||
@@ -24,25 +117,36 @@ impl ReflectorSelector {
|
|||||||
pub fn run(&self) -> anyhow::Result<()> {
|
pub fn run(&self) -> anyhow::Result<()> {
|
||||||
let mut selector_sleep_time = Duration::new(30, 0);
|
let mut selector_sleep_time = Duration::new(30, 0);
|
||||||
let mut reselection_count = 0;
|
let mut reselection_count = 0;
|
||||||
let baseline_sleep_time =
|
let mut latest_snapshot = None;
|
||||||
Duration::from_secs_f64(self.config.tick_interval * std::f64::consts::PI);
|
let baseline_sleep_time = Duration::from_secs_f64(
|
||||||
|
self.settings.advanced_settings.tick_interval * std::f64::consts::PI,
|
||||||
|
);
|
||||||
// Initial wait of several seconds to allow some OWD data to build up
|
// Initial wait of several seconds to allow some OWD data to build up
|
||||||
sleep(baseline_sleep_time);
|
receive_snapshots_until(
|
||||||
|
&self.snapshot_rx,
|
||||||
|
&mut latest_snapshot,
|
||||||
|
Instant::now() + baseline_sleep_time,
|
||||||
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||||
info!("Reflector selector shutting down");
|
info!("Reflector selector shutting down");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
* Selection is triggered either by some other thread triggering it through the channel,
|
let triggered = match wait_for_trigger_or_deadline(
|
||||||
* or it passes the timeout. In any case we don't care about the result of this function,
|
&self.trigger_channel,
|
||||||
* so we ignore the result of it.
|
&self.snapshot_rx,
|
||||||
*/
|
&mut latest_snapshot,
|
||||||
let triggered = self
|
Instant::now() + selector_sleep_time,
|
||||||
.trigger_channel
|
) {
|
||||||
.recv_timeout(selector_sleep_time)
|
WaitOutcome::Triggered => true,
|
||||||
.is_ok();
|
WaitOutcome::DeadlineReached => false,
|
||||||
|
WaitOutcome::ChannelsClosed => {
|
||||||
|
info!("Reflector selector channels closed, shutting down");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
reselection_count += 1;
|
reselection_count += 1;
|
||||||
info!("Starting reselection [#{}]", reselection_count);
|
info!("Starting reselection [#{}]", reselection_count);
|
||||||
self.metrics.send(Metric::Event {
|
self.metrics.send(Metric::Event {
|
||||||
@@ -54,7 +158,10 @@ impl ReflectorSelector {
|
|||||||
|
|
||||||
// After 40 reselections, slow down to every 15 minutes
|
// After 40 reselections, slow down to every 15 minutes
|
||||||
if reselection_count > 40 {
|
if reselection_count > 40 {
|
||||||
selector_sleep_time = Duration::new(self.config.peer_reselection_time * 60, 0);
|
selector_sleep_time = Duration::new(
|
||||||
|
self.settings.advanced_settings.peer_reselection_time * 60,
|
||||||
|
0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut next_peers: Vec<IpAddr> = Vec::new();
|
let mut next_peers: Vec<IpAddr> = Vec::new();
|
||||||
@@ -62,7 +169,7 @@ impl ReflectorSelector {
|
|||||||
|
|
||||||
// Include all current peers
|
// Include all current peers
|
||||||
for reflector in reflectors_peers.iter() {
|
for reflector in reflectors_peers.iter() {
|
||||||
debug!("Current peer: {}", reflector.to_string());
|
debug!("Current peer: {}", reflector);
|
||||||
next_peers.push(*reflector);
|
next_peers.push(*reflector);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +179,7 @@ impl ReflectorSelector {
|
|||||||
if next_peers.contains(next_candidate) {
|
if next_peers.contains(next_candidate) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
debug!("Next candidate: {}", next_candidate.to_string());
|
debug!("Next candidate: {}", next_candidate);
|
||||||
next_peers.push(*next_candidate);
|
next_peers.push(*next_candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,23 +193,25 @@ impl ReflectorSelector {
|
|||||||
|
|
||||||
debug!("Waiting for candidates to be baselined");
|
debug!("Waiting for candidates to be baselined");
|
||||||
// Wait for several seconds to allow all reflectors to be re-baselined
|
// Wait for several seconds to allow all reflectors to be re-baselined
|
||||||
sleep(baseline_sleep_time);
|
receive_snapshots_until(
|
||||||
|
&self.snapshot_rx,
|
||||||
|
&mut latest_snapshot,
|
||||||
|
Instant::now() + baseline_sleep_time,
|
||||||
|
);
|
||||||
|
|
||||||
// Re-acquire the lock when we wake up again
|
// Re-acquire the lock when we wake up again
|
||||||
reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
||||||
|
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
let owd_recent = self.owd_recent.lock_anyhow()?;
|
|
||||||
|
|
||||||
for peer in next_peers {
|
for peer in next_peers {
|
||||||
if owd_recent.contains_key(&peer) {
|
if let Some(rtt) = recent_rtt_for_peer(latest_snapshot.as_ref(), &peer) {
|
||||||
let rtt = (owd_recent[&peer].down_ewma + owd_recent[&peer].up_ewma) as u64;
|
|
||||||
candidates.push((peer, rtt));
|
candidates.push((peer, rtt));
|
||||||
info!("Candidate reflector: {} RTT: {}", peer.to_string(), rtt);
|
info!("Candidate reflector: {} RTT: {}", peer, rtt);
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
"No data found from candidate reflector: {} - skipping",
|
"No data found from candidate reflector: {} - skipping",
|
||||||
peer.to_string()
|
peer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +220,7 @@ impl ReflectorSelector {
|
|||||||
candidates.sort_by(|a, b| a.1.cmp(&b.1));
|
candidates.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
|
||||||
// Now we will just limit the candidates down to 2 * num_reflectors
|
// Now we will just limit the candidates down to 2 * num_reflectors
|
||||||
let mut num_reflectors = self.config.num_reflectors;
|
let mut num_reflectors = self.settings.advanced_settings.num_reflectors;
|
||||||
let candidate_pool_num = (2 * num_reflectors) as usize;
|
let candidate_pool_num = (2 * num_reflectors) as usize;
|
||||||
candidates.truncate(candidate_pool_num);
|
candidates.truncate(candidate_pool_num);
|
||||||
|
|
||||||
@@ -146,3 +255,147 @@ impl ReflectorSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::baseliner::{EwmaStats, ReflectorState};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
fn snapshot(generated_at: Instant) -> ControlSnapshot {
|
||||||
|
ControlSnapshot {
|
||||||
|
generated_at,
|
||||||
|
reflectors: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_with_rtt(generated_at: Instant, peer: IpAddr, rtt: f64) -> ControlSnapshot {
|
||||||
|
let mut reflectors = HashMap::new();
|
||||||
|
reflectors.insert(
|
||||||
|
peer,
|
||||||
|
ReflectorState {
|
||||||
|
baseline: EwmaStats {
|
||||||
|
down: rtt / 2.0,
|
||||||
|
up: rtt / 2.0,
|
||||||
|
},
|
||||||
|
recent: EwmaStats {
|
||||||
|
down: rtt / 2.0,
|
||||||
|
up: rtt / 2.0,
|
||||||
|
},
|
||||||
|
last_receive_at: generated_at,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ControlSnapshot {
|
||||||
|
generated_at,
|
||||||
|
reflectors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wait_retains_the_newest_snapshot() {
|
||||||
|
let (_trigger_tx, trigger_rx) = flume::unbounded();
|
||||||
|
let (snapshot_tx, snapshot_rx) = flume::unbounded();
|
||||||
|
let generated_at = Instant::now();
|
||||||
|
snapshot_tx.send(snapshot(generated_at)).unwrap();
|
||||||
|
snapshot_tx
|
||||||
|
.send(snapshot(generated_at + Duration::from_millis(1)))
|
||||||
|
.unwrap();
|
||||||
|
snapshot_tx
|
||||||
|
.send(snapshot(generated_at + Duration::from_millis(2)))
|
||||||
|
.unwrap();
|
||||||
|
let mut latest_snapshot = None;
|
||||||
|
|
||||||
|
let result = wait_for_trigger_or_deadline(
|
||||||
|
&trigger_rx,
|
||||||
|
&snapshot_rx,
|
||||||
|
&mut latest_snapshot,
|
||||||
|
Instant::now() + Duration::from_millis(20),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result, WaitOutcome::DeadlineReached);
|
||||||
|
assert_eq!(
|
||||||
|
latest_snapshot.unwrap().generated_at,
|
||||||
|
generated_at + Duration::from_millis(2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshots_do_not_extend_the_deadline() {
|
||||||
|
let (_trigger_tx, trigger_rx) = flume::unbounded();
|
||||||
|
let (snapshot_tx, snapshot_rx) = flume::unbounded();
|
||||||
|
let producer = thread::spawn(move || {
|
||||||
|
let stop_at = Instant::now() + Duration::from_millis(500);
|
||||||
|
while Instant::now() < stop_at {
|
||||||
|
if snapshot_tx.send(snapshot(Instant::now())).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut latest_snapshot = None;
|
||||||
|
let started_at = Instant::now();
|
||||||
|
|
||||||
|
let result = wait_for_trigger_or_deadline(
|
||||||
|
&trigger_rx,
|
||||||
|
&snapshot_rx,
|
||||||
|
&mut latest_snapshot,
|
||||||
|
started_at + Duration::from_millis(30),
|
||||||
|
);
|
||||||
|
let elapsed = started_at.elapsed();
|
||||||
|
drop(snapshot_rx);
|
||||||
|
producer.join().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, WaitOutcome::DeadlineReached);
|
||||||
|
assert!(latest_snapshot.is_some());
|
||||||
|
assert!(elapsed < Duration::from_millis(250), "elapsed: {elapsed:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trigger_interrupts_the_wait() {
|
||||||
|
let (trigger_tx, trigger_rx) = flume::unbounded();
|
||||||
|
let (_snapshot_tx, snapshot_rx) = flume::unbounded();
|
||||||
|
trigger_tx.send(true).unwrap();
|
||||||
|
let mut latest_snapshot = None;
|
||||||
|
let started_at = Instant::now();
|
||||||
|
|
||||||
|
let result = wait_for_trigger_or_deadline(
|
||||||
|
&trigger_rx,
|
||||||
|
&snapshot_rx,
|
||||||
|
&mut latest_snapshot,
|
||||||
|
started_at + Duration::from_secs(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result, WaitOutcome::Triggered);
|
||||||
|
assert!(started_at.elapsed() < Duration::from_millis(250));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn baseline_wait_retains_snapshot_for_evaluation() {
|
||||||
|
let (snapshot_tx, snapshot_rx) = flume::unbounded();
|
||||||
|
let peer = "192.0.2.1".parse().unwrap();
|
||||||
|
let generated_at = Instant::now();
|
||||||
|
snapshot_tx
|
||||||
|
.send(snapshot_with_rtt(generated_at, peer, 20.0))
|
||||||
|
.unwrap();
|
||||||
|
snapshot_tx
|
||||||
|
.send(snapshot_with_rtt(
|
||||||
|
generated_at + Duration::from_millis(1),
|
||||||
|
peer,
|
||||||
|
42.0,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let mut latest_snapshot = None;
|
||||||
|
|
||||||
|
receive_snapshots_until(
|
||||||
|
&snapshot_rx,
|
||||||
|
&mut latest_snapshot,
|
||||||
|
Instant::now() + Duration::from_millis(20),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
recent_rtt_for_peer(latest_snapshot.as_ref(), &peer),
|
||||||
|
Some(42)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use anyhow::{Context, bail};
|
||||||
|
use config::{Config, ConfigError, Environment};
|
||||||
|
use log::Level;
|
||||||
|
use serde::{Deserialize, Deserializer, de};
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use uci_derive::{UciConfig, UciSection};
|
||||||
|
|
||||||
|
pub(crate) mod uci_schema;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "uci", unix))]
|
||||||
|
use crate::platform::unix::uci::UciSource;
|
||||||
|
|
||||||
|
struct FlexiBool(bool);
|
||||||
|
|
||||||
|
impl FromStr for FlexiBool {
|
||||||
|
type Err = ConfigError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.trim().to_lowercase().as_str() {
|
||||||
|
"1" | "true" | "yes" | "y" | "on" | "enabled" => Ok(FlexiBool(true)),
|
||||||
|
"0" | "false" | "no" | "n" | "off" | "disabled" => Ok(FlexiBool(false)),
|
||||||
|
_ => Err(ConfigError::Message(format!(
|
||||||
|
"invalid boolean string: {}",
|
||||||
|
s
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_flex_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct FlexiBoolVisitor;
|
||||||
|
|
||||||
|
impl<'de> de::Visitor<'de> for FlexiBoolVisitor {
|
||||||
|
type Value = bool;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a boolean or a UCI-compatible boolean string")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
value
|
||||||
|
.parse::<FlexiBool>()
|
||||||
|
.map(|value| value.0)
|
||||||
|
.map_err(E::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
self.visit_str(&value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_any(FlexiBoolVisitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum MeasurementType {
|
||||||
|
Icmp = 1,
|
||||||
|
IcmpTimestamps,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MeasurementType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
MeasurementType::Icmp => write!(f, "icmp"),
|
||||||
|
MeasurementType::IcmpTimestamps => write!(f, "icmp-timestamps"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MeasurementType {
|
||||||
|
type Err = ConfigError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"icmp" => Ok(MeasurementType::Icmp),
|
||||||
|
"icmp-timestamps" | "icmp_timestamps" => Ok(MeasurementType::IcmpTimestamps),
|
||||||
|
_ => Err(ConfigError::Message(format!(
|
||||||
|
"invalid measurement type: {s}"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum ObservabilityProtocol {
|
||||||
|
Udp,
|
||||||
|
Tcp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ObservabilityProtocol {
|
||||||
|
type Err = ConfigError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"udp" => Ok(ObservabilityProtocol::Udp),
|
||||||
|
"tcp" => Ok(ObservabilityProtocol::Tcp),
|
||||||
|
_ => Err(ConfigError::Message(format!(
|
||||||
|
"invalid observability protocol: {s}",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ReflectorRecord {
|
||||||
|
reflector_ip: IpAddr,
|
||||||
|
ip_version: u8,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, UciConfig)]
|
||||||
|
#[uci(package = "sqm-autorate-rust")]
|
||||||
|
pub(crate) struct Settings {
|
||||||
|
#[uci(section)]
|
||||||
|
pub(crate) network: NetworkSettings,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[uci(section)]
|
||||||
|
pub(crate) output: OutputSettings,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[uci(section)]
|
||||||
|
pub(crate) observability: ObservabilitySettings,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[uci(section)]
|
||||||
|
pub(crate) advanced_settings: AdvancedSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
pub(crate) fn load() -> Result<Self, ConfigError> {
|
||||||
|
let builder = Config::builder();
|
||||||
|
|
||||||
|
#[cfg(all(feature = "uci", unix))]
|
||||||
|
let builder = builder.add_source(UciSource::from_schema::<Self>().required(false));
|
||||||
|
|
||||||
|
#[cfg(feature = "toml")]
|
||||||
|
let builder = builder.add_source(
|
||||||
|
config::File::with_name("/etc/sqm-autorate-rust/config.toml").required(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
let builder = builder.add_source(Environment::with_prefix("SQMA").separator("__"));
|
||||||
|
let built_config = builder.build()?;
|
||||||
|
|
||||||
|
built_config.try_deserialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_reflectors(&self) -> anyhow::Result<Vec<IpAddr>> {
|
||||||
|
let mut reader = csv::ReaderBuilder::new()
|
||||||
|
.has_headers(true)
|
||||||
|
.trim(csv::Trim::All)
|
||||||
|
.from_path(&self.advanced_settings.reflector_list_file)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to open reflector list: {}",
|
||||||
|
self.advanced_settings.reflector_list_file
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut reflectors = Vec::with_capacity(50);
|
||||||
|
|
||||||
|
for record in reader.deserialize::<ReflectorRecord>() {
|
||||||
|
let record = record.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to parse reflector list: {}",
|
||||||
|
self.advanced_settings.reflector_list_file
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match (record.ip_version, record.reflector_ip) {
|
||||||
|
(4, IpAddr::V4(_)) | (6, IpAddr::V6(_)) => {}
|
||||||
|
(version, address) => {
|
||||||
|
bail!("reflector {address} has inconsistent IP version {version}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reflectors.push(record.reflector_ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(reflectors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||||
|
pub(crate) struct NetworkSettings {
|
||||||
|
pub(crate) download_interface: String,
|
||||||
|
pub(crate) upload_interface: String,
|
||||||
|
pub(crate) download_base_kbits: f64,
|
||||||
|
#[serde(default = "default_min_percent")]
|
||||||
|
pub(crate) download_min_percent: f64,
|
||||||
|
pub(crate) upload_base_kbits: f64,
|
||||||
|
#[serde(default = "default_min_percent")]
|
||||||
|
pub(crate) upload_min_percent: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkSettings {
|
||||||
|
pub(crate) fn download_min_kbits(&self) -> f64 {
|
||||||
|
(self.download_base_kbits * self.download_min_percent / 100.0).floor()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn upload_min_kbits(&self) -> f64 {
|
||||||
|
(self.upload_base_kbits * self.upload_min_percent / 100.0).floor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) struct OutputSettings {
|
||||||
|
#[serde(deserialize_with = "deserialize_from_str")]
|
||||||
|
pub(crate) log_level: Level,
|
||||||
|
pub(crate) speed_hist_file: String,
|
||||||
|
pub(crate) stats_file: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_flex_bool")]
|
||||||
|
pub(crate) suppress_statistics: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OutputSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
log_level: Level::Error,
|
||||||
|
speed_hist_file: "/tmp/sqm-speedhist.csv".to_owned(),
|
||||||
|
stats_file: "/tmp/sqm-autorate-rust.csv".to_owned(),
|
||||||
|
suppress_statistics: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) struct ObservabilitySettings {
|
||||||
|
#[serde(deserialize_with = "deserialize_flex_bool")]
|
||||||
|
pub(crate) enabled: bool,
|
||||||
|
#[serde(deserialize_with = "deserialize_from_str")]
|
||||||
|
pub(crate) protocol: ObservabilityProtocol,
|
||||||
|
pub(crate) host: Option<String>,
|
||||||
|
pub(crate) port: u16,
|
||||||
|
pub(crate) batch_size: usize,
|
||||||
|
pub(crate) batch_timeout_ms: u64,
|
||||||
|
#[serde(deserialize_with = "deserialize_flex_bool")]
|
||||||
|
pub(crate) export_ping_metrics: bool,
|
||||||
|
#[serde(deserialize_with = "deserialize_flex_bool")]
|
||||||
|
pub(crate) export_rate_metrics: bool,
|
||||||
|
#[serde(deserialize_with = "deserialize_flex_bool")]
|
||||||
|
pub(crate) export_baseline_metrics: bool,
|
||||||
|
#[serde(deserialize_with = "deserialize_flex_bool")]
|
||||||
|
pub(crate) export_events: bool,
|
||||||
|
pub(crate) host_tag: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ObservabilitySettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
protocol: ObservabilityProtocol::Udp,
|
||||||
|
host: None,
|
||||||
|
port: 8089,
|
||||||
|
batch_size: 25,
|
||||||
|
batch_timeout_ms: 100,
|
||||||
|
export_ping_metrics: false,
|
||||||
|
export_rate_metrics: true,
|
||||||
|
export_baseline_metrics: false,
|
||||||
|
export_events: true,
|
||||||
|
host_tag: default_host_tag(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) struct AdvancedSettings {
|
||||||
|
pub(crate) download_delay_ms: f64,
|
||||||
|
pub(crate) high_load_level: f64,
|
||||||
|
pub(crate) min_change_interval: f64,
|
||||||
|
#[serde(deserialize_with = "deserialize_from_str")]
|
||||||
|
pub(crate) measurement_type: MeasurementType,
|
||||||
|
pub(crate) num_reflectors: u8,
|
||||||
|
pub(crate) peer_reselection_time: u64,
|
||||||
|
pub(crate) reflector_list_file: String,
|
||||||
|
pub(crate) speed_hist_size: u32,
|
||||||
|
pub(crate) tick_interval: f64,
|
||||||
|
pub(crate) upload_delay_ms: f64,
|
||||||
|
#[serde(deserialize_with = "deserialize_flex_bool")]
|
||||||
|
pub(crate) dry_run: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AdvancedSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
download_delay_ms: 15.0,
|
||||||
|
high_load_level: 0.8,
|
||||||
|
min_change_interval: 0.5,
|
||||||
|
measurement_type: MeasurementType::IcmpTimestamps,
|
||||||
|
num_reflectors: 5,
|
||||||
|
peer_reselection_time: 15,
|
||||||
|
reflector_list_file: "/etc/sqm-autorate/reflectors-icmp.csv".to_owned(),
|
||||||
|
speed_hist_size: 100,
|
||||||
|
tick_interval: 0.5,
|
||||||
|
upload_delay_ms: 15.0,
|
||||||
|
dry_run: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_min_percent() -> f64 {
|
||||||
|
20.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_host_tag() -> String {
|
||||||
|
rustix::system::uname()
|
||||||
|
.nodename()
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_from_str<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
T: FromStr,
|
||||||
|
T::Err: Display,
|
||||||
|
{
|
||||||
|
let value = String::deserialize(deserializer)?;
|
||||||
|
value.parse().map_err(de::Error::custom)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct UciOptionMapping {
|
||||||
|
pub(crate) uci_option: &'static str,
|
||||||
|
pub(crate) config_field: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) struct UciSectionMapping {
|
||||||
|
/// UCI section type, corresponding to `@section_type[0]`.
|
||||||
|
pub(crate) uci_section: &'static str,
|
||||||
|
|
||||||
|
/// Top-level config-rs / Serde settings key.
|
||||||
|
pub(crate) config_section: &'static str,
|
||||||
|
|
||||||
|
pub(crate) options: &'static [UciOptionMapping],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait UciSectionSchema {
|
||||||
|
const UCI_OPTIONS: &'static [UciOptionMapping];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait UciConfigSchema {
|
||||||
|
const UCI_PACKAGE: &'static str;
|
||||||
|
const UCI_SECTIONS: &'static [UciSectionMapping];
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
//
|
||||||
|
|
||||||
|
use super::{AdvancedSettings, NetworkSettings, ObservabilitySettings, OutputSettings, Settings};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
const WARN_DOWNLOAD_MIN_KBITS: f64 = 1_000.0;
|
||||||
|
const WARN_UPLOAD_MIN_KBITS: f64 = 500.0;
|
||||||
|
const WARN_HIGH_MIN_PERCENT: f64 = 80.0;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum ValidationSeverity {
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ValidationIssue {
|
||||||
|
severity: ValidationSeverity,
|
||||||
|
path: &'static str,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct ValidationReport {
|
||||||
|
issues: Vec<ValidationIssue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationReport {
|
||||||
|
fn warning(&mut self, path: &'static str, message: impl Into<String>) {
|
||||||
|
self.issues.push(ValidationIssue {
|
||||||
|
severity: ValidationSeverity::Warning,
|
||||||
|
path,
|
||||||
|
message: message.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error(&mut self, path: &'static str, message: impl Into<String>) {
|
||||||
|
self.issues.push(ValidationIssue {
|
||||||
|
severity: ValidationSeverity::Error,
|
||||||
|
path,
|
||||||
|
message: message.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Result<(), Self> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
for issue in self.issues {
|
||||||
|
match issue.severity {
|
||||||
|
ValidationSeverity::Warning => {
|
||||||
|
eprintln!("warning: {}: {}", issue.path, issue.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationSeverity::Error => {
|
||||||
|
errors.push(issue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Self { issues: errors })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ValidationReport {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "settings validation failed:")?;
|
||||||
|
|
||||||
|
for issue in &self.issues {
|
||||||
|
write!(f, "\n {}: {}", issue.path, issue.message)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for ValidationReport {}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
pub(crate) fn validate(&self) -> Result<(), ValidationReport> {
|
||||||
|
let mut report = ValidationReport::default();
|
||||||
|
|
||||||
|
self.network.validate(&mut report);
|
||||||
|
self.output.validate(&mut report);
|
||||||
|
self.observability.validate(&mut report);
|
||||||
|
self.advanced_settings.validate(&mut report);
|
||||||
|
|
||||||
|
report.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkSettings {
|
||||||
|
fn validate(&self, report: &mut ValidationReport) {
|
||||||
|
if self.download_interface.trim().is_empty() {
|
||||||
|
report.error("network.download_interface", "must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.upload_interface.trim().is_empty() {
|
||||||
|
report.error("network.upload_interface", "must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let download_base_valid = validate_greater_than_zero_f64(
|
||||||
|
report,
|
||||||
|
"network.download_base_kbits",
|
||||||
|
self.download_base_kbits,
|
||||||
|
);
|
||||||
|
|
||||||
|
let upload_base_valid = validate_greater_than_zero_f64(
|
||||||
|
report,
|
||||||
|
"network.upload_base_kbits",
|
||||||
|
self.upload_base_kbits,
|
||||||
|
);
|
||||||
|
|
||||||
|
let download_percent_valid = validate_percent(
|
||||||
|
report,
|
||||||
|
"network.download_min_percent",
|
||||||
|
self.download_min_percent,
|
||||||
|
);
|
||||||
|
|
||||||
|
let upload_percent_valid = validate_percent(
|
||||||
|
report,
|
||||||
|
"network.upload_min_percent",
|
||||||
|
self.upload_min_percent,
|
||||||
|
);
|
||||||
|
|
||||||
|
if download_percent_valid && self.download_min_percent > WARN_HIGH_MIN_PERCENT {
|
||||||
|
report.warning(
|
||||||
|
"network.download_min_percent",
|
||||||
|
format!(
|
||||||
|
"{}% leaves little room for the shaper to reduce the download rate",
|
||||||
|
self.download_min_percent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if upload_percent_valid && self.upload_min_percent > WARN_HIGH_MIN_PERCENT {
|
||||||
|
report.warning(
|
||||||
|
"network.upload_min_percent",
|
||||||
|
format!(
|
||||||
|
"{}% leaves little room for the shaper to reduce the upload rate",
|
||||||
|
self.upload_min_percent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if download_base_valid && download_percent_valid {
|
||||||
|
let minimum = self.download_min_kbits();
|
||||||
|
|
||||||
|
if minimum < WARN_DOWNLOAD_MIN_KBITS {
|
||||||
|
report.warning(
|
||||||
|
"network.download_min_percent",
|
||||||
|
format!(
|
||||||
|
"results in a minimum download rate of only {minimum:.0} kbit/s \
|
||||||
|
({:.1}% of {:.0} kbit/s); connectivity may become severely constrained",
|
||||||
|
self.download_min_percent, self.download_base_kbits,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if upload_base_valid && upload_percent_valid {
|
||||||
|
let minimum = self.upload_min_kbits();
|
||||||
|
|
||||||
|
if minimum < WARN_UPLOAD_MIN_KBITS {
|
||||||
|
report.warning(
|
||||||
|
"network.upload_min_percent",
|
||||||
|
format!(
|
||||||
|
"results in a minimum upload rate of only {minimum:.0} kbit/s \
|
||||||
|
({:.1}% of {:.0} kbit/s); connectivity may become severely constrained",
|
||||||
|
self.upload_min_percent, self.upload_base_kbits,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputSettings {
|
||||||
|
fn validate(&self, report: &mut ValidationReport) {
|
||||||
|
if self.suppress_statistics {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let speed_hist_file = self.speed_hist_file.trim();
|
||||||
|
let stats_file = self.stats_file.trim();
|
||||||
|
|
||||||
|
if speed_hist_file.is_empty() {
|
||||||
|
report.error(
|
||||||
|
"output.speed_hist_file",
|
||||||
|
"must not be empty when statistics are enabled",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats_file.is_empty() {
|
||||||
|
report.error(
|
||||||
|
"output.stats_file",
|
||||||
|
"must not be empty when statistics are enabled",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !speed_hist_file.is_empty() && !stats_file.is_empty() && speed_hist_file == stats_file {
|
||||||
|
report.error(
|
||||||
|
"output.stats_file",
|
||||||
|
"must not refer to the same file as output.speed_hist_file",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObservabilitySettings {
|
||||||
|
fn validate(&self, report: &mut ValidationReport) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.host
|
||||||
|
.as_deref()
|
||||||
|
.is_none_or(|host| host.trim().is_empty())
|
||||||
|
{
|
||||||
|
report.error(
|
||||||
|
"observability.host",
|
||||||
|
"must be set when observability is enabled",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.port == 0 {
|
||||||
|
report.error("observability.port", "must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.batch_size == 0 {
|
||||||
|
report.error("observability.batch_size", "must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.batch_timeout_ms == 0 {
|
||||||
|
report.error("observability.batch_timeout_ms", "must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.export_ping_metrics
|
||||||
|
&& !self.export_rate_metrics
|
||||||
|
&& !self.export_baseline_metrics
|
||||||
|
&& !self.export_events
|
||||||
|
{
|
||||||
|
report.warning(
|
||||||
|
"observability.enabled",
|
||||||
|
"observability is enabled but all metric and event exports are disabled",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdvancedSettings {
|
||||||
|
fn validate(&self, report: &mut ValidationReport) {
|
||||||
|
validate_zero_or_greater_f64(
|
||||||
|
report,
|
||||||
|
"advanced_settings.download_delay_ms",
|
||||||
|
self.download_delay_ms,
|
||||||
|
);
|
||||||
|
|
||||||
|
validate_zero_or_greater_f64(
|
||||||
|
report,
|
||||||
|
"advanced_settings.upload_delay_ms",
|
||||||
|
self.upload_delay_ms,
|
||||||
|
);
|
||||||
|
|
||||||
|
let high_load_level_valid = validate_zero_or_greater_f64(
|
||||||
|
report,
|
||||||
|
"advanced_settings.high_load_level",
|
||||||
|
self.high_load_level,
|
||||||
|
);
|
||||||
|
|
||||||
|
validate_greater_than_zero_f64(
|
||||||
|
report,
|
||||||
|
"advanced_settings.min_change_interval",
|
||||||
|
self.min_change_interval,
|
||||||
|
);
|
||||||
|
|
||||||
|
validate_greater_than_zero_f64(
|
||||||
|
report,
|
||||||
|
"advanced_settings.tick_interval",
|
||||||
|
self.tick_interval,
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.num_reflectors < 5 {
|
||||||
|
report.error(
|
||||||
|
"advanced_settings.num_reflectors",
|
||||||
|
format!("must be at least 5, got {}", self.num_reflectors,),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.peer_reselection_time == 0 {
|
||||||
|
report.error(
|
||||||
|
"advanced_settings.peer_reselection_time",
|
||||||
|
"must be greater than 0",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.reflector_list_file.trim().is_empty() {
|
||||||
|
report.error("advanced_settings.reflector_list_file", "must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.speed_hist_size == 0 {
|
||||||
|
report.error(
|
||||||
|
"advanced_settings.speed_hist_size",
|
||||||
|
"must be greater than 0",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if high_load_level_valid {
|
||||||
|
if self.high_load_level == 0.0 {
|
||||||
|
report.warning(
|
||||||
|
"advanced_settings.high_load_level",
|
||||||
|
"0 causes any positive load to be treated as high load",
|
||||||
|
);
|
||||||
|
} else if self.high_load_level > 1.0 {
|
||||||
|
report.warning(
|
||||||
|
"advanced_settings.high_load_level",
|
||||||
|
format!(
|
||||||
|
"{} requires utilisation to exceed the current shaper rate \
|
||||||
|
before the high-load threshold is reached",
|
||||||
|
self.high_load_level,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_greater_than_zero_f64(
|
||||||
|
report: &mut ValidationReport,
|
||||||
|
path: &'static str,
|
||||||
|
value: f64,
|
||||||
|
) -> bool {
|
||||||
|
if !value.is_finite() {
|
||||||
|
report.error(path, format!("must be finite, got {value}"));
|
||||||
|
false
|
||||||
|
} else if value <= 0.0 {
|
||||||
|
report.error(path, format!("must be greater than 0, got {value}"));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_zero_or_greater_f64(
|
||||||
|
report: &mut ValidationReport,
|
||||||
|
path: &'static str,
|
||||||
|
value: f64,
|
||||||
|
) -> bool {
|
||||||
|
if !value.is_finite() {
|
||||||
|
report.error(path, format!("must be finite, got {value}"));
|
||||||
|
false
|
||||||
|
} else if value < 0.0 {
|
||||||
|
report.error(path, format!("must be 0 or greater, got {value}"));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_percent(report: &mut ValidationReport, path: &'static str, value: f64) -> bool {
|
||||||
|
if !value.is_finite() {
|
||||||
|
report.error(path, format!("must be finite, got {value}"));
|
||||||
|
false
|
||||||
|
} else if !(1.0..=100.0).contains(&value) {
|
||||||
|
report.error(path, format!("must be between 1 and 100, got {value}"));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -1,6 +1,9 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use rustix::fs::Timespec;
|
use rustix::fs::Timespec;
|
||||||
use rustix::thread::ClockId;
|
use rustix::time::{ClockId, clock_gettime};
|
||||||
use rustix::time::clock_gettime;
|
|
||||||
|
|
||||||
pub struct Time {
|
pub struct Time {
|
||||||
time_s: u64,
|
time_s: u64,
|
||||||
|
|||||||
+7
-1
@@ -1,5 +1,11 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
|
||||||
|
pub type ArcRwLock<T> = Arc<RwLock<T>>;
|
||||||
|
|
||||||
pub trait MutexExt<T> {
|
pub trait MutexExt<T> {
|
||||||
fn lock_anyhow(&self) -> anyhow::Result<MutexGuard<'_, T>>;
|
fn lock_anyhow(&self) -> anyhow::Result<MutexGuard<'_, T>>;
|
||||||
|
|||||||
Reference in new issue
Block a user