Compare commits
108
Commits
v0.4.0
...
321c82ce21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
321c82ce21 | ||
|
|
e27bab1944 | ||
|
|
fbb971b47d | ||
|
|
3fbc96b15f | ||
|
|
c199d63c93 | ||
|
|
2afa1bba35 | ||
|
|
d03effd600 | ||
|
|
6103d2dae4 | ||
|
|
efe9eff036 | ||
|
|
bb94b42d24 | ||
|
|
de8d7e3aeb | ||
|
|
746636b59a | ||
|
|
83e02c6e1c | ||
|
|
6d4849899e | ||
|
|
7254aa5cf9 | ||
|
|
5b0854920d | ||
|
|
4c9fe15201 | ||
|
|
5872f40d3e | ||
|
|
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
@@ -0,0 +1,110 @@
|
||||
name: Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
name: cargo fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
with:
|
||||
components: rustfmt
|
||||
cache: false
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: cargo clippy
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
with:
|
||||
components: clippy
|
||||
cache: false
|
||||
- name: Run clippy with all features
|
||||
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
- name: Run clippy without default features
|
||||
run: cargo clippy --workspace --all-targets --no-default-features --features toml --locked -- -D warnings
|
||||
|
||||
verify:
|
||||
name: cargo ${{ matrix.task }} (${{ matrix.target }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- task: check
|
||||
target: x86_64-unknown-linux-gnu
|
||||
musl: false
|
||||
- task: test
|
||||
target: x86_64-unknown-linux-gnu
|
||||
musl: false
|
||||
- task: check
|
||||
target: x86_64-unknown-linux-musl
|
||||
musl: true
|
||||
- task: test
|
||||
target: x86_64-unknown-linux-musl
|
||||
musl: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install musl toolchain
|
||||
if: matrix.musl
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends musl-tools
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
cache: false
|
||||
- name: Run ${{ matrix.task }} with all features
|
||||
run: cargo ${{ matrix.task }} --workspace --all-targets --all-features --target ${{ matrix.target }} --locked
|
||||
- name: Run ${{ matrix.task }} without default features
|
||||
run: cargo ${{ matrix.task }} --workspace --all-targets --no-default-features --features toml --target ${{ matrix.target }} --locked
|
||||
|
||||
docs:
|
||||
name: cargo doc
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
with:
|
||||
cache: false
|
||||
- name: Check documentation
|
||||
run: cargo doc --workspace --all-features --no-deps --locked
|
||||
- name: Run doctests
|
||||
run: cargo test --workspace --doc --all-features --locked
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Supply chain
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: "41 2 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
ACTIONLINT_VERSION: 1.7.7
|
||||
CARGO_DENY_VERSION: 0.20.2
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
name: cargo-deny ${{ matrix.checks }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- checks: advisories
|
||||
continue-on-error: true
|
||||
- checks: bans licenses sources
|
||||
continue-on-error: false
|
||||
continue-on-error: ${{ matrix.continue-on-error }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install cargo-deny
|
||||
run: cargo install cargo-deny --version "${CARGO_DENY_VERSION}" --locked
|
||||
- name: Run cargo-deny
|
||||
run: cargo deny --manifest-path Cargo.toml --all-features check ${{ matrix.checks }}
|
||||
|
||||
actionlint:
|
||||
name: actionlint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install actionlint
|
||||
run: go install "github.com/rhysd/actionlint/cmd/actionlint@v${ACTIONLINT_VERSION}"
|
||||
- name: Check workflow syntax
|
||||
run: |
|
||||
"$(go env GOPATH)/bin/actionlint" \
|
||||
.gitea/workflows/ci.yml \
|
||||
.gitea/workflows/supply-chain.yml \
|
||||
.github/workflows/actions-security.yml \
|
||||
.github/workflows/ci-arm.yml \
|
||||
.github/workflows/devskim.yml \
|
||||
.github/workflows/osv-scanner.yml \
|
||||
.github/workflows/rust-clippy.yml
|
||||
@@ -0,0 +1,35 @@
|
||||
name: GitHub Actions Security
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: "11 3 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: zizmor
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||
with:
|
||||
persona: regular
|
||||
inputs: |
|
||||
.github/workflows
|
||||
.gitea/workflows
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install Mold
|
||||
run: sudo apt install mold
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Rust ARM CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: cargo ${{ matrix.task }} (${{ matrix.target }})
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- task: check
|
||||
target: aarch64-unknown-linux-gnu
|
||||
musl: false
|
||||
- task: test
|
||||
target: aarch64-unknown-linux-gnu
|
||||
musl: false
|
||||
- task: check
|
||||
target: aarch64-unknown-linux-musl
|
||||
musl: true
|
||||
- task: test
|
||||
target: aarch64-unknown-linux-musl
|
||||
musl: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install musl toolchain
|
||||
if: matrix.musl
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends musl-tools
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
cache: false
|
||||
- name: Run ${{ matrix.task }} with all features
|
||||
run: cargo ${{ matrix.task }} --workspace --all-targets --all-features --target ${{ matrix.target }} --locked
|
||||
- name: Run ${{ matrix.task }} without default features
|
||||
run: cargo ${{ matrix.task }} --workspace --all-targets --no-default-features --features toml --target ${{ matrix.target }} --locked
|
||||
@@ -1,33 +1,29 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# This workflow uploads SARIF results to GitHub Code Scanning.
|
||||
name: DevSkim
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '18 0 * * 4'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: DevSkim
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
uses: actions/checkout@v7
|
||||
- name: Run DevSkim scanner
|
||||
uses: microsoft/DevSkim-Action@v1
|
||||
|
||||
- name: Upload DevSkim scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
|
||||
@@ -1,55 +1,34 @@
|
||||
# Copyright 2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
name: OSV-Scanner Scheduled Scan
|
||||
name: OSV-Scanner
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["master", "v1"]
|
||||
branches: [master, v1]
|
||||
schedule:
|
||||
- cron: "12 12 * * 1"
|
||||
push:
|
||||
branches: ["master", "v1"]
|
||||
branches: [master, v1]
|
||||
|
||||
# Restrict jobs in this workflow to have no permissions by default; permissions
|
||||
# should be granted per job as needed using a dedicated `permissions` block
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
scan-scheduled:
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
security-events: write # for uploading SARIF files
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
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
|
||||
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.6.0
|
||||
with:
|
||||
# Just scan the root directory and docs, since everything else is fixtures
|
||||
scan-args: |-
|
||||
./
|
||||
./docs/
|
||||
scan-args: ./
|
||||
scan-pr:
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
security-events: write # for uploading SARIF files
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
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
|
||||
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.6.0
|
||||
with:
|
||||
# Just scan the root directory and docs, since everything else is fixtures
|
||||
scan-args: |-
|
||||
./
|
||||
./docs/
|
||||
scan-args: ./
|
||||
@@ -1,55 +1,61 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# rust-clippy is a tool that runs a bunch of lints to catch common
|
||||
# mistakes in your Rust code and help improve your Rust code.
|
||||
# More details at https://github.com/rust-lang/rust-clippy
|
||||
# and https://rust-lang.github.io/rust-clippy/
|
||||
|
||||
name: rust-clippy analyze
|
||||
name: Clippy SARIF
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '28 20 * * 3'
|
||||
- cron: "17 12 * * 4"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
rust-clippy-analyze:
|
||||
name: Run rust-clippy analyzing
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||
actions: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
clippy-sarif:
|
||||
name: Clippy SARIF
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
|
||||
- name: Install required cargo
|
||||
run: cargo install clippy-sarif sarif-fmt
|
||||
|
||||
- name: Run rust-clippy
|
||||
run:
|
||||
cargo clippy
|
||||
--all-features
|
||||
--message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload analysis results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
cache: false
|
||||
- name: Install SARIF tools
|
||||
run: cargo install --locked clippy-sarif sarif-fmt
|
||||
- name: Run Clippy and generate SARIF
|
||||
id: clippy
|
||||
shell: bash
|
||||
run: |
|
||||
set +e
|
||||
cargo clippy \
|
||||
--workspace \
|
||||
--all-targets \
|
||||
--all-features \
|
||||
--locked \
|
||||
--message-format=json \
|
||||
| clippy-sarif \
|
||||
| tee rust-clippy-results.sarif \
|
||||
| sarif-fmt
|
||||
clippy_status=${PIPESTATUS[0]}
|
||||
echo "exit_code=${clippy_status}" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
- name: Upload Clippy SARIF
|
||||
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: rust-clippy-results.sarif
|
||||
category: clippy
|
||||
wait-for-processing: true
|
||||
- name: Fail if Clippy failed
|
||||
if: steps.clippy.outputs.exit_code != '0'
|
||||
run: exit 1
|
||||
Generated
+432
-198
@@ -11,82 +11,59 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.58.1"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"itertools",
|
||||
"log",
|
||||
"peeking_take_while",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"which",
|
||||
"shlex 1.3.0",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
"shlex 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
@@ -108,43 +85,60 @@ dependencies = [
|
||||
"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]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.8.4"
|
||||
name = "config"
|
||||
version = "0.15.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
|
||||
checksum = "790374c727a4cb20877dd2cc99ce7011cbe03b0b8df083acccd37df0d161304d"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
"pathdiff",
|
||||
"serde_core",
|
||||
"toml",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -157,9 +151,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
@@ -167,6 +164,29 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
@@ -174,46 +194,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "icmp-socket2"
|
||||
version = "0.4.2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e57405ef902d5506111426bb8a2498f10979c19273bcdadc852ae03d45d19d8"
|
||||
checksum = "8e7d36a7710b3a23b6e364bc921f266300d2f72b714fa4834d1aa98da7fb2c0d"
|
||||
dependencies = [
|
||||
"socket2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
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]]
|
||||
name = "libc"
|
||||
version = "0.2.182"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -227,11 +262,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libuci-sys"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "544c17966d9131e80b3616783974a31b526197ca5b56948bb130eeb6c04fbaf1"
|
||||
version = "1.1.0"
|
||||
source = "git+https://github.com/Lochnair/rust-uci.git?rev=fcfedd204fb76928ef617bf9631b041c816e03d4#fcfedd204fb76928ef617bf9631b041c816e03d4"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"cmake",
|
||||
]
|
||||
|
||||
@@ -242,84 +277,110 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-async"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "netlink-bindings"
|
||||
version = "0.2.4"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e639b3a966932d8ab55cfc1e606e5421aef3d30a2610284535b854f11f6e4b44"
|
||||
checksum = "15c68f271f059945bc6fb55d210e78c1ceb2d3eac00d18fe9b0806ade81e8a21"
|
||||
|
||||
[[package]]
|
||||
name = "netlink-socket2"
|
||||
version = "0.2.4"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffef37f26cf649b9f473ccc00fa52d419511225a7b3a2093bf16dc8673ab9cf1"
|
||||
checksum = "0951d4d6a5131a59af90a26fcecd4e366e8fcee3a38880ba58705e479f5bd8c3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"maybe-async",
|
||||
"netlink-bindings",
|
||||
"strip-async",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.1.3"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"version_check",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
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]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
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 = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -329,9 +390,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -340,40 +401,110 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.9"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rust-uci"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c664edb3ec081c1f2746960b04861354bbeaafdbb962fb8789d6ded5153ac3bb"
|
||||
version = "0.1.6"
|
||||
source = "git+https://github.com/Lochnair/rust-uci.git?rev=fcfedd204fb76928ef617bf9631b041c816e03d4#fcfedd204fb76928ef617bf9631b041c816e03d4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libuci-sys",
|
||||
"log",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-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.6",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -381,21 +512,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
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 = [
|
||||
"libc",
|
||||
"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]]
|
||||
name = "sqm-autorate-rust"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"config",
|
||||
"csv",
|
||||
"fastrand",
|
||||
"flume",
|
||||
"icmp-socket2",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -403,26 +552,45 @@ dependencies = [
|
||||
"netlink-socket2",
|
||||
"rust-uci",
|
||||
"rustix",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"uci-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
name = "strip-async"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
checksum = "85ec932792e62b2d3936163eacbd2e09984453bcd114f665c1e1694e36ff59c9"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-tuple"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "876fef147edbcbddc8ac5cbbba92c7b86519e314e86638596c09673b2ed01e7f"
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
@@ -432,33 +600,88 @@ dependencies = [
|
||||
"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]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[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.121"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0cabaa10be1917331a313866bd94526343e03c77bcf69144b62b072ad35d47c"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"target-tuple",
|
||||
"termcolor",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uci-derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"trybuild",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -468,47 +691,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[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"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
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]]
|
||||
name = "winapi-util"
|
||||
@@ -519,12 +744,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
@@ -539,3 +758,18 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"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]
|
||||
name = "sqm-autorate-rust"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -20,18 +20,34 @@ strip = "symbols"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
icmp-socket2 = "0.4.2"
|
||||
netlink-bindings = { version = "0.2", features = ["rt-link", "tc"] }
|
||||
netlink-socket2 = "0.2"
|
||||
icmp-socket2 = "0.6.2"
|
||||
fastrand = "2"
|
||||
rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] }
|
||||
rust-uci = { version = "0.1", optional = true }
|
||||
thiserror = "2.0"
|
||||
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]
|
||||
features = ["std"]
|
||||
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]
|
||||
default = ["toml", "uci-vendored"]
|
||||
toml = ["config/toml"]
|
||||
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 {
|
||||
docker {
|
||||
image "ghcr.io/rust-cross/rust-musl-cross:${TARGET}"
|
||||
alwaysPull true
|
||||
dockerfile {
|
||||
filename 'Dockerfile.ci'
|
||||
additionalBuildArgs "--pull --build-arg TARGET=${TARGET}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ pipeline {
|
||||
echo "----------------------------------"
|
||||
|
||||
# 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...")
|
||||
if rustc -V | grep -q "nightly"; then
|
||||
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 = \"...\")]`",
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# cargo-deny policy for sqm-autorate-rust.
|
||||
# Keep published dependency sources and license terms explicit while allowing
|
||||
# advisory reports to be reviewed without blocking unrelated changes.
|
||||
|
||||
[graph]
|
||||
all-features = true
|
||||
no-default-features = false
|
||||
targets = []
|
||||
|
||||
[output]
|
||||
feature-depth = 1
|
||||
|
||||
[advisories]
|
||||
yanked = "deny"
|
||||
unmaintained = "workspace"
|
||||
unsound = "workspace"
|
||||
ignore = []
|
||||
|
||||
[licenses]
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn"
|
||||
wildcards = "allow"
|
||||
highlight = "all"
|
||||
workspace-default-features = "allow"
|
||||
external-default-features = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
required-git-spec = "rev"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = ["https://github.com/Lochnair/rust-uci"]
|
||||
|
||||
[sources.allow-org]
|
||||
github = []
|
||||
gitlab = []
|
||||
bitbucket = []
|
||||
@@ -2,5 +2,18 @@
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"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::Config;
|
||||
use crate::metrics::{Metric, MetricsSender};
|
||||
use crate::pinger::PingReply;
|
||||
use crate::util::MutexExt;
|
||||
use log::info;
|
||||
use crate::settings::Settings;
|
||||
use flume::{Receiver, Sender};
|
||||
use log::{debug, info};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ReflectorStats {
|
||||
pub down_ewma: f64,
|
||||
pub up_ewma: f64,
|
||||
pub last_receive_time_s: Instant,
|
||||
const STALE_AFTER: Duration = Duration::from_secs(30);
|
||||
const ANOMALY_THRESHOLD_MS: f64 = 5_000.0;
|
||||
const BASELINE_HALF_LIFE_SECS: f64 = 135.0;
|
||||
const RECENT_HALF_LIFE_SECS: f64 = 0.4;
|
||||
|
||||
#[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 config: Config,
|
||||
pub owd_baseline: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
||||
pub owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
||||
pub settings: Settings,
|
||||
pub reselect_trigger: Sender<bool>,
|
||||
pub start_time: Instant,
|
||||
pub stats_rx: Receiver<PingReply>,
|
||||
pub control_tx: Sender<ControlSnapshot>,
|
||||
pub selection_tx: Option<Sender<ControlSnapshot>>,
|
||||
pub baseline_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()
|
||||
}
|
||||
|
||||
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 {
|
||||
pub fn run(&self) -> anyhow::Result<()> {
|
||||
/*
|
||||
* 135 seconds to decay to 50% for the slow factor and
|
||||
* 0.4 seconds to decay to 50% for the fast factor.
|
||||
* The fast one can be adjusted to tune, try anything from 0.01 to 3.0 to get more or less sensitivity
|
||||
* with more sensitivity we respond faster to bloat, but are at risk from triggering due to lag spikes that
|
||||
* aren't bloat related, with less sensitivity (bigger numbers) we smooth through quick spikes
|
||||
* but take longer to respond to real bufferbloat
|
||||
*/
|
||||
let slow_factor = ewma_factor(self.config.tick_interval, 135.0);
|
||||
let fast_factor = ewma_factor(self.config.tick_interval, 0.4);
|
||||
let mut reflectors = HashMap::<IpAddr, ReflectorState>::new();
|
||||
|
||||
let slow_factor = ewma_factor(
|
||||
self.settings.advanced_settings.tick_interval,
|
||||
BASELINE_HALF_LIFE_SECS,
|
||||
);
|
||||
let fast_factor = ewma_factor(
|
||||
self.settings.advanced_settings.tick_interval,
|
||||
RECENT_HALF_LIFE_SECS,
|
||||
);
|
||||
|
||||
loop {
|
||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||
info!("Baseliner shutting down");
|
||||
return Ok(());
|
||||
}
|
||||
let time_data = self.stats_rx.recv()?;
|
||||
|
||||
let mut owd_baseline_map = self.owd_baseline.lock_anyhow()?;
|
||||
let mut owd_recent_map = self.owd_recent.lock_anyhow()?;
|
||||
let reply = self.stats_rx.recv()?;
|
||||
let reflector = reply.reflector;
|
||||
let (state, anomaly) = process_reply(
|
||||
&mut reflectors,
|
||||
&reply,
|
||||
self.start_time,
|
||||
slow_factor,
|
||||
fast_factor,
|
||||
);
|
||||
|
||||
let owd_baseline_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_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;
|
||||
if anomaly {
|
||||
info!(
|
||||
"Reflector {} has OWD > 5 seconds more than baseline, triggering reselection",
|
||||
time_data.reflector
|
||||
reflector
|
||||
);
|
||||
self.event_metrics.send(Metric::Event {
|
||||
name: "reselection",
|
||||
reason: "anomaly",
|
||||
reflector: Some(time_data.reflector),
|
||||
reflector: Some(reflector),
|
||||
tags: &[],
|
||||
});
|
||||
// If reselection is disabled this would trigger an error
|
||||
// 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;
|
||||
}
|
||||
let _ = self.reselect_trigger.try_send(true);
|
||||
}
|
||||
|
||||
self.baseline_metrics.send(Metric::Baseline {
|
||||
reflector: time_data.reflector,
|
||||
baseline_up_ewma: owd_baseline.up_ewma,
|
||||
baseline_down_ewma: owd_baseline.down_ewma,
|
||||
recent_up_ewma: owd_recent.up_ewma,
|
||||
recent_down_ewma: owd_recent.down_ewma,
|
||||
reflector,
|
||||
baseline_up_ewma: state.baseline.up,
|
||||
baseline_down_ewma: state.baseline.down,
|
||||
recent_up_ewma: state.recent.up,
|
||||
recent_down_ewma: state.recent.down,
|
||||
});
|
||||
|
||||
info!(
|
||||
debug!(
|
||||
"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 = {}",
|
||||
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)
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -1,5 +1,9 @@
|
||||
use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
|
||||
use rustix::thread::ClockId;
|
||||
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use log::{Level, Metadata, Record, SetLoggerError};
|
||||
use rustix::time::ClockId;
|
||||
|
||||
use crate::time::Time;
|
||||
|
||||
@@ -32,5 +36,5 @@ impl log::Log for SimpleLogger {
|
||||
|
||||
pub fn init(level: Level) -> Result<(), SetLoggerError> {
|
||||
log::set_boxed_logger(Box::new(SimpleLogger { level }))
|
||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
||||
.map(|()| log::set_max_level(level.to_level_filter()))
|
||||
}
|
||||
+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;
|
||||
|
||||
mod baseliner;
|
||||
mod config;
|
||||
mod log;
|
||||
mod metrics;
|
||||
mod netlink;
|
||||
mod pinger;
|
||||
mod pinger_icmp;
|
||||
mod pinger_icmp_ts;
|
||||
mod platform;
|
||||
mod ratecontroller;
|
||||
mod reflector_selector;
|
||||
mod settings;
|
||||
mod time;
|
||||
mod util;
|
||||
|
||||
use crate::baseliner::{Baseliner, ReflectorStats};
|
||||
use crate::baseliner::Baseliner;
|
||||
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::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::mpsc::{channel, sync_channel, RecvTimeoutError};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
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);
|
||||
|
||||
extern "C" fn signal_handler(_: libc::c_int) {
|
||||
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 RESELECTION_CANDIDATE_BURST: usize = 20;
|
||||
const INFLIGHT_CAPACITY_DUPLICATE_FACTOR: usize = 2;
|
||||
const INFLIGHT_CAPACITY_MIN: usize = 256;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
println!("Starting sqm-autorate version {}", VERSION);
|
||||
fn compute_inflight_probe_capacity(
|
||||
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 {
|
||||
libc::signal(libc::SIGINT, signal_handler as *const () as libc::sighandler_t);
|
||||
libc::signal(libc::SIGTERM, signal_handler as *const () as libc::sighandler_t);
|
||||
libc::signal(
|
||||
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()?;
|
||||
log::init(config.log_level)?;
|
||||
if config.dry_run {
|
||||
info!("*** MONITORING MODE ACTIVE — qdisc rates will NOT be changed ***");
|
||||
fn probe_identifier() -> u16 {
|
||||
// The ICMP identifier is only two bytes.
|
||||
(process::id() & 0xffff) as u16
|
||||
}
|
||||
let mut reflectors = config.load_reflectors()?;
|
||||
let start_t = Instant::now();
|
||||
|
||||
// The identifier field in ICMP is only 2 bytes
|
||||
// so take the last 2 bytes of the PID as the ID
|
||||
let id = (process::id() & 0xFFFF) as u16;
|
||||
fn restore_shaper<T: TrafficControlBackend>(
|
||||
settings: &Settings,
|
||||
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
|
||||
let owd_baseline = Arc::new(Mutex::new(HashMap::<IpAddr, ReflectorStats>::new()));
|
||||
let owd_recent = Arc::new(Mutex::new(HashMap::<IpAddr, ReflectorStats>::new()));
|
||||
let reflector_peers_lock = Arc::new(RwLock::new(Vec::<IpAddr>::new()));
|
||||
let mut reflector_pool = Vec::<IpAddr>::new();
|
||||
let reflector_pool_size = reflectors.len();
|
||||
if let Err(error) = traffic_control.set_rate(
|
||||
down,
|
||||
settings.network.download_base_kbits as u64,
|
||||
settings.advanced_settings.dry_run,
|
||||
) {
|
||||
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 = [
|
||||
IpAddr::from_str("9.9.9.9")?,
|
||||
@@ -79,28 +201,74 @@ fn main() -> anyhow::Result<()> {
|
||||
IpAddr::from_str("94.140.14.14")?,
|
||||
];
|
||||
|
||||
match reflector_pool_size > config.num_reflectors as usize {
|
||||
true => {
|
||||
let mut peers = reflector_peers_lock.write().unwrap();
|
||||
peers.extend_from_slice(&default_reflectors);
|
||||
reflector_pool.append(reflectors.as_mut());
|
||||
let reselection_enabled = configured_count > settings.advanced_settings.num_reflectors as usize;
|
||||
let pool = if reselection_enabled {
|
||||
configured
|
||||
} else {
|
||||
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();
|
||||
let (error_tx, error_rx) = channel::<anyhow::Error>();
|
||||
let (reselect_tx, reselect_rx) = channel();
|
||||
fn run(settings: &Settings) -> anyhow::Result<()> {
|
||||
if settings.advanced_settings.dry_run {
|
||||
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 (tx, rx) = sync_channel(1000);
|
||||
let ReflectorSetup {
|
||||
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 {
|
||||
config: config.clone(),
|
||||
settings: settings.clone(),
|
||||
metrics_rx: rx,
|
||||
metrics_dropped: Arc::clone(&dropped),
|
||||
};
|
||||
@@ -108,8 +276,8 @@ fn main() -> anyhow::Result<()> {
|
||||
let handle = thread::Builder::new()
|
||||
.name("metrics".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(e) = metrics.run() {
|
||||
let _ = err_tx.send(e);
|
||||
if let Err(error) = metrics.run() {
|
||||
let _ = err_tx.send(error);
|
||||
}
|
||||
})?;
|
||||
(Some(tx), Some(handle))
|
||||
@@ -125,79 +293,68 @@ fn main() -> anyhow::Result<()> {
|
||||
.unwrap_or_else(MetricsSender::disabled)
|
||||
};
|
||||
|
||||
let ping_metrics = make_sender(config.observability_export_ping_metrics);
|
||||
let baseline_metrics = make_sender(config.observability_export_baseline_metrics);
|
||||
let event_metrics = make_sender(config.observability_export_events);
|
||||
let rate_metrics = make_sender(config.observability_export_rate_metrics);
|
||||
let ping_metrics = make_sender(settings.observability.export_ping_metrics);
|
||||
let baseline_metrics = make_sender(settings.observability.export_baseline_metrics);
|
||||
let event_metrics = make_sender(settings.observability.export_events);
|
||||
let rate_metrics = make_sender(settings.observability.export_rate_metrics);
|
||||
|
||||
event_metrics.send(Metric::Event {
|
||||
name: "starting",
|
||||
reason: "",
|
||||
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 {
|
||||
MeasurementType::Icmp => (
|
||||
Box::new(PingerICMPEchoListener {}) as Box<dyn PingListener + Send>,
|
||||
Box::new(PingerICMPEchoSender {}) as Box<dyn PingSender + Send>,
|
||||
),
|
||||
MeasurementType::IcmpTimestamps => (
|
||||
Box::new(PingerICMPTimestampListener {}) as Box<dyn PingListener + Send>,
|
||||
Box::new(PingerICMPTimestampSender {}) as Box<dyn PingSender + Send>,
|
||||
),
|
||||
MeasurementType::Ntp | MeasurementType::TcpTimestamps => {
|
||||
todo!()
|
||||
}
|
||||
};
|
||||
let (mut ping_listener, mut ping_sender) =
|
||||
create_pinger(settings.advanced_settings.measurement_type);
|
||||
|
||||
let inflight_probe_capacity =
|
||||
compute_inflight_probe_capacity(settings, active_reflector_count, reselection_enabled);
|
||||
info!(
|
||||
"In-flight probe cache capacity: {} (active_reflectors={}, reselection_enabled={}, tick_interval_s={})",
|
||||
inflight_probe_capacity,
|
||||
active_reflector_count,
|
||||
reselection_enabled,
|
||||
settings.advanced_settings.tick_interval
|
||||
);
|
||||
|
||||
let inflight: InFlightProbeCache =
|
||||
Arc::new(Mutex::new(HashMap::with_capacity(inflight_probe_capacity)));
|
||||
|
||||
let baseliner = Baseliner {
|
||||
config: config.clone(),
|
||||
owd_baseline: owd_baseline.clone(),
|
||||
owd_recent: owd_recent.clone(),
|
||||
settings: settings.clone(),
|
||||
reselect_trigger: reselect_tx.clone(),
|
||||
start_time: start_t,
|
||||
start_time,
|
||||
stats_rx: baseliner_stats_rx,
|
||||
control_tx: control_snapshot_tx,
|
||||
selection_tx: selection_snapshot_tx,
|
||||
baseline_metrics,
|
||||
event_metrics: event_metrics.clone(),
|
||||
};
|
||||
|
||||
let down_qdisc = Netlink::qdisc_from_ifname(config.download_interface.as_str())?;
|
||||
let up_qdisc = Netlink::qdisc_from_ifname(config.upload_interface.as_str())?;
|
||||
|
||||
/* 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 mut main_traffic_control = traffic_control_backend();
|
||||
let (down_shaper, up_shaper) = initialize_shaper(settings, &mut main_traffic_control)?;
|
||||
|
||||
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()
|
||||
.name("receiver".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(e) = ping_listener.listen(
|
||||
id,
|
||||
config.measurement_type,
|
||||
reflector_peers_lock_clone,
|
||||
if let Err(error) = ping_listener.listen(
|
||||
probe_id,
|
||||
measurement_type,
|
||||
listener_peers,
|
||||
listener_inflight,
|
||||
baseliner_stats_tx,
|
||||
ping_metrics,
|
||||
) {
|
||||
let _ = err_tx.send(e);
|
||||
let _ = err_tx.send(error);
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -205,33 +362,38 @@ fn main() -> anyhow::Result<()> {
|
||||
thread::Builder::new()
|
||||
.name("baseliner".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(e) = baseliner.run() {
|
||||
let _ = err_tx.send(e);
|
||||
if let Err(error) = baseliner.run() {
|
||||
let _ = err_tx.send(error);
|
||||
}
|
||||
})?;
|
||||
|
||||
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()
|
||||
.name("sender".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(e) = ping_sender.send(
|
||||
id,
|
||||
config.measurement_type,
|
||||
reflector_peers_lock_clone,
|
||||
config.tick_interval,
|
||||
if let Err(error) = ping_sender.send(
|
||||
probe_id,
|
||||
measurement_type,
|
||||
sender_peers,
|
||||
sender_inflight,
|
||||
tick_interval,
|
||||
) {
|
||||
let _ = err_tx.send(e);
|
||||
let _ = err_tx.send(error);
|
||||
}
|
||||
})?;
|
||||
|
||||
let main_event_metrics = event_metrics.clone();
|
||||
|
||||
if reflector_pool_size > config.num_reflectors as usize {
|
||||
if reselection_enabled {
|
||||
let reflector_selector = ReflectorSelector {
|
||||
config: config.clone(),
|
||||
owd_recent: owd_recent.clone(),
|
||||
reflector_peers_lock: reflector_peers_lock.clone(),
|
||||
settings: settings.clone(),
|
||||
snapshot_rx: selection_snapshot_rx
|
||||
.expect("reselection snapshot receiver must exist when reselection is enabled"),
|
||||
reflector_peers_lock: Arc::clone(&reflector_peers),
|
||||
reflector_pool,
|
||||
trigger_channel: reselect_rx,
|
||||
metrics: event_metrics,
|
||||
@@ -240,75 +402,39 @@ fn main() -> anyhow::Result<()> {
|
||||
thread::Builder::new()
|
||||
.name("reselection".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(e) = reflector_selector.run() {
|
||||
let _ = err_tx.send(e);
|
||||
if let Err(error) = reflector_selector.run() {
|
||||
let _ = err_tx.send(error);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
// Sleep 10 seconds before we start adjusting speeds
|
||||
sleep(Duration::new(10, 0));
|
||||
|
||||
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
|
||||
};
|
||||
// Give the baseliner time to collect initial samples before adjusting rates.
|
||||
sleep(Duration::from_secs(10));
|
||||
|
||||
let mut ratecontroller = Ratecontroller::new(
|
||||
config.clone(),
|
||||
owd_baseline,
|
||||
owd_recent,
|
||||
reflector_peers_lock,
|
||||
settings.clone(),
|
||||
control_snapshot_rx,
|
||||
reflector_peers,
|
||||
reselect_tx,
|
||||
dl_direction,
|
||||
ul_direction,
|
||||
rate_metrics,
|
||||
)?;
|
||||
|
||||
debug!(
|
||||
"Download direction: {}:{:?}",
|
||||
config.download_interface, dl_direction
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Upload direction: {}:{:?}",
|
||||
config.upload_interface, ul_direction
|
||||
);
|
||||
|
||||
let err_tx = error_tx.clone();
|
||||
thread::Builder::new()
|
||||
.name("ratecontroller".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(e) = ratecontroller.run() {
|
||||
let _ = err_tx.send(e);
|
||||
if let Err(error) = ratecontroller.run() {
|
||||
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);
|
||||
|
||||
let result = loop {
|
||||
match error_rx.recv_timeout(Duration::from_secs(1)) {
|
||||
Ok(e) => break Err(anyhow::anyhow!("thread exited with error: {e}")),
|
||||
Err(RecvTimeoutError::Disconnected) => break Ok(()),
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||
info!("Received shutdown signal");
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let result = wait_for_exit(&error_rx);
|
||||
|
||||
// Make the error path request shutdown too, rather than leaving the other workers running.
|
||||
SHUTDOWN.store(true, Ordering::Relaxed);
|
||||
|
||||
let stopping_reason = if result.is_err() { "error" } else { "signal" };
|
||||
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
|
||||
// disconnects once all threads also drop their copies, allowing the
|
||||
// metrics thread to drain and exit cleanly.
|
||||
// disconnects once all worker threads also drop their copies.
|
||||
drop(main_event_metrics);
|
||||
drop(metrics_tx);
|
||||
|
||||
if let Some(handle) = metrics_thread_handle {
|
||||
let _ = handle.join();
|
||||
}
|
||||
|
||||
info!(
|
||||
"Restoring base shaper rates (D/L): {} / {}",
|
||||
config.download_base_kbits, config.upload_base_kbits
|
||||
restore_shaper(
|
||||
settings,
|
||||
&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
|
||||
}
|
||||
|
||||
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::Config;
|
||||
use flume::{Receiver, RecvTimeoutError, Sender};
|
||||
use log::{error, info, warn};
|
||||
use rustix::time::ClockId;
|
||||
use std::fmt::Write;
|
||||
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::atomic::{AtomicU32, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
const MAX_RECONNECT_BACKOFF: u64 = 60;
|
||||
@@ -74,8 +78,10 @@ impl Transport {
|
||||
|
||||
let full_data = format!("{}\n", data);
|
||||
// take the stream out, write, put it back or set None on failure
|
||||
if let Some(s) = stream {
|
||||
if let Err(e) = std::io::Write::write_all(s, full_data.as_bytes()) {
|
||||
let write_error = stream
|
||||
.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);
|
||||
*stream = None;
|
||||
*reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
|
||||
@@ -84,7 +90,6 @@ impl Transport {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Metric {
|
||||
Ping {
|
||||
@@ -116,18 +121,18 @@ pub enum Metric {
|
||||
tags: &'static [(&'static str, &'static str)],
|
||||
},
|
||||
Dropped {
|
||||
count: u64,
|
||||
count: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MetricsSender {
|
||||
tx: Option<SyncSender<(Metric, u64)>>,
|
||||
dropped: Arc<AtomicU64>,
|
||||
tx: Option<Sender<(Metric, u64)>>,
|
||||
dropped: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
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 {
|
||||
tx: Some(tx),
|
||||
dropped,
|
||||
@@ -137,7 +142,7 @@ impl MetricsSender {
|
||||
pub fn disabled() -> Self {
|
||||
Self {
|
||||
tx: None,
|
||||
dropped: Arc::new(AtomicU64::new(0)),
|
||||
dropped: Arc::new(AtomicU32::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,14 +157,14 @@ impl MetricsSender {
|
||||
}
|
||||
|
||||
pub struct Metrics {
|
||||
pub config: Config,
|
||||
pub metrics_dropped: Arc<AtomicU64>,
|
||||
pub settings: Settings,
|
||||
pub metrics_dropped: Arc<AtomicU32>,
|
||||
pub metrics_rx: Receiver<(Metric, u64)>,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
let host = match &self.config.observability_host {
|
||||
let host = match &self.settings.observability.host {
|
||||
Some(h) => h.clone(),
|
||||
None => {
|
||||
error!("Observability host not configured - metrics exporter disabled");
|
||||
@@ -167,12 +172,12 @@ impl Metrics {
|
||||
}
|
||||
};
|
||||
|
||||
let port = self.config.observability_port;
|
||||
let batch_size = self.config.observability_batch_size;
|
||||
let timeout = Duration::from_millis(self.config.observability_batch_timeout_ms);
|
||||
let host_tag = &self.config.observability_host_tag.clone();
|
||||
let port = self.settings.observability.port;
|
||||
let batch_size = self.settings.observability.batch_size;
|
||||
let timeout = Duration::from_millis(self.settings.observability.batch_timeout_ms);
|
||||
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 => {
|
||||
info!("Metrics exporter configured for UDP to {}:{}", host, port);
|
||||
Transport::new_udp(&host, port)?
|
||||
@@ -205,11 +210,8 @@ impl Metrics {
|
||||
}
|
||||
|
||||
// greedily drain without blocking
|
||||
loop {
|
||||
match self.metrics_rx.try_recv() {
|
||||
Ok(metric) => batch.push(metric),
|
||||
Err(_) => break,
|
||||
}
|
||||
while let Ok(metric) = self.metrics_rx.try_recv() {
|
||||
batch.push(metric);
|
||||
if batch.len() >= batch_size {
|
||||
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::SHUTDOWN;
|
||||
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::{IcmpSocket4, Icmpv4Packet};
|
||||
use log::{debug, info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{io, thread};
|
||||
use thiserror::Error;
|
||||
@@ -39,12 +47,19 @@ pub struct PingReply {
|
||||
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> {
|
||||
match type_ {
|
||||
MeasurementType::Icmp | MeasurementType::IcmpTimestamps => IcmpSocket4::new(),
|
||||
_ => {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +69,7 @@ pub trait PingListener {
|
||||
id: u16,
|
||||
type_: MeasurementType,
|
||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
||||
inflight: InFlightProbeCache,
|
||||
stats_tx: Sender<PingReply>,
|
||||
ping_metrics: MetricsSender,
|
||||
) -> 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 {
|
||||
reflector: reply.reflector,
|
||||
measurement_type: reply.measurement_type,
|
||||
@@ -128,6 +152,7 @@ pub trait PingSender {
|
||||
id: u16,
|
||||
type_: MeasurementType,
|
||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
||||
inflight: InFlightProbeCache,
|
||||
tick_interval: f64,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut socket = open_socket(type_)?;
|
||||
@@ -151,6 +176,10 @@ pub trait PingSender {
|
||||
continue;
|
||||
}
|
||||
|
||||
inflight
|
||||
.lock_anyhow()?
|
||||
.retain(|_, probe| probe.sent_at.elapsed() < INFLIGHT_PROBE_TTL);
|
||||
|
||||
let sleep_duration =
|
||||
Duration::from_millis((tick_duration_ms / reflectors.len() as u16) as u64);
|
||||
|
||||
@@ -160,17 +189,26 @@ pub trait PingSender {
|
||||
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);
|
||||
}
|
||||
|
||||
if seq == u16::MAX {
|
||||
seq = 0;
|
||||
} else {
|
||||
seq += 1;
|
||||
}
|
||||
seq = seq.wrapping_add(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::time::Instant;
|
||||
|
||||
use crate::config::MeasurementType;
|
||||
use crate::pinger::{PingError, PingListener, PingReply, PingSender};
|
||||
use crate::settings::MeasurementType;
|
||||
use crate::time::Time;
|
||||
use icmp_socket2::Icmpv4Message;
|
||||
use icmp_socket2::Icmpv4Packet;
|
||||
use icmp_socket2::packet::WithEchoRequest;
|
||||
use rustix::thread::ClockId;
|
||||
use rustix::time::ClockId;
|
||||
|
||||
pub struct PingerICMPEchoListener {}
|
||||
|
||||
@@ -77,11 +84,14 @@ impl PingListener for PingerICMPEchoListener {
|
||||
}
|
||||
|
||||
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 time_ms = clock.to_milliseconds();
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+63
-9
@@ -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::settings::MeasurementType;
|
||||
use crate::time::Time;
|
||||
use icmp_socket2::Icmpv4Message;
|
||||
use icmp_socket2::Icmpv4Packet;
|
||||
use icmp_socket2::packet::WithTimestampRequest;
|
||||
use rustix::thread::ClockId;
|
||||
use rustix::time::ClockId;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -12,6 +19,19 @@ pub struct PingerICMPTimestampListener {}
|
||||
|
||||
pub struct PingerICMPTimestampSender {}
|
||||
|
||||
const MS_PER_DAY: i64 = 86_400_000;
|
||||
|
||||
fn timestamp_delta(later: i64, earlier: i64) -> i64 {
|
||||
let delta = later - earlier;
|
||||
if delta < -MS_PER_DAY / 2 {
|
||||
delta + MS_PER_DAY
|
||||
} else if delta > MS_PER_DAY / 2 {
|
||||
delta - MS_PER_DAY
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
}
|
||||
|
||||
impl PingListener for PingerICMPTimestampListener {
|
||||
// Result: RTT, down time, up time
|
||||
fn parse_packet(
|
||||
@@ -39,12 +59,18 @@ impl PingListener for PingerICMPTimestampListener {
|
||||
});
|
||||
}
|
||||
|
||||
let time_now = Time::new(ClockId::Realtime);
|
||||
let time_since_midnight = time_now.get_time_since_midnight();
|
||||
if receive & 0x8000_0000 != 0 || transmit & 0x8000_0000 != 0 {
|
||||
return Err(PingError::InvalidPacket(
|
||||
"non-standard RFC 792 timestamp".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let rtt = (time_since_midnight - originate as i64) as f64;
|
||||
let dl_time = (time_since_midnight - transmit as i64) as f64;
|
||||
let ul_time = (receive as i64 - originate as i64) as f64;
|
||||
let time_since_midnight =
|
||||
Time::new(ClockId::Realtime).get_time_since_midnight();
|
||||
|
||||
let rtt = timestamp_delta(time_since_midnight, originate as i64) as f64;
|
||||
let dl_time = timestamp_delta(time_since_midnight, transmit as i64) as f64;
|
||||
let ul_time = timestamp_delta(receive as i64, originate as i64) as f64;
|
||||
|
||||
Ok(PingReply {
|
||||
reflector,
|
||||
@@ -72,8 +98,36 @@ impl PingListener for PingerICMPTimestampListener {
|
||||
}
|
||||
|
||||
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();
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use icmp_socket2::packet::WithTimestampReply;
|
||||
|
||||
#[test]
|
||||
fn timestamps_wrap_at_midnight_in_both_directions() {
|
||||
assert_eq!(timestamp_delta(20, 86_399_900), 120);
|
||||
assert_eq!(timestamp_delta(86_399_900, 20), -120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_standard_rfc_792_timestamps_are_rejected() {
|
||||
let packet = Icmpv4Packet::with_timestamp_reply(7, 1, 10, 0x8000_0001, 20).unwrap();
|
||||
let result = PingerICMPTimestampListener {}.parse_packet(
|
||||
7,
|
||||
"192.0.2.1".parse().unwrap(),
|
||||
MeasurementType::IcmpTimestamps,
|
||||
packet,
|
||||
);
|
||||
assert!(matches!(result, Err(PingError::InvalidPacket(_))));
|
||||
}
|
||||
}
|
||||
@@ -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,313 @@
|
||||
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(SourceError::NotFound(_)) 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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::settings::Settings;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn malformed_optional_package_is_an_error() {
|
||||
let root = std::env::temp_dir().join(format!("sqma-uci-{}", std::process::id()));
|
||||
let config_dir = root.join("config");
|
||||
let save_dir = root.join("save");
|
||||
fs::create_dir_all(&config_dir).unwrap();
|
||||
fs::create_dir_all(&save_dir).unwrap();
|
||||
let config_file = config_dir.join("sqm-autorate-rust");
|
||||
fs::write(&config_file, "config network 'unterminated\n").unwrap();
|
||||
|
||||
let source = UciSource::from_schema::<Settings>()
|
||||
.with_directories(&config_dir, &save_dir)
|
||||
.required(false);
|
||||
let result = source.collect();
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -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::baseliner::ControlSnapshot;
|
||||
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::util::{MutexExt, RwLockExt};
|
||||
use crate::{Config, ReflectorStats};
|
||||
use crate::util::{ArcRwLock, RwLockExt};
|
||||
use flume::{Receiver, Sender};
|
||||
use log::{debug, info, warn};
|
||||
use rustix::thread::ClockId;
|
||||
use std::collections::HashMap;
|
||||
use rustix::time::ClockId;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
enum Direction {
|
||||
@@ -23,56 +31,32 @@ enum Direction {
|
||||
Up,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
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> {
|
||||
fn generate_initial_speeds(rng: &mut fastrand::Rng, base_speed: f64, size: u32) -> Vec<f64> {
|
||||
let mut rates = Vec::new();
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn get_interface_stats(
|
||||
config: &Config,
|
||||
down_direction: StatsDirection,
|
||||
up_direction: StatsDirection,
|
||||
) -> Result<(i128, i128), RatecontrolError> {
|
||||
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())?;
|
||||
fn get_interface_stats<S: InterfaceStatsProvider>(
|
||||
stats_provider: &mut S,
|
||||
upload_interface: &str,
|
||||
) -> Result<(i128, i128), S::Error> {
|
||||
let stats = stats_provider.read_stats(upload_interface)?;
|
||||
|
||||
let rx_bytes = match down_direction {
|
||||
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()))
|
||||
Ok((stats.rx_bytes.into(), stats.tx_bytes.into()))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct State {
|
||||
struct State<H> {
|
||||
current_bytes: i128,
|
||||
current_rate: f64,
|
||||
delta_stat: f64,
|
||||
deltas: Vec<f64>,
|
||||
qdisc: Qdisc,
|
||||
shaper: H,
|
||||
load: f64,
|
||||
next_rate: f64,
|
||||
nrate: usize,
|
||||
@@ -82,8 +66,8 @@ struct State {
|
||||
utilisation: f64,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new(qdisc: Qdisc, previous_bytes: i128, safe_rates: Vec<f64>) -> Self {
|
||||
impl<H> State<H> {
|
||||
fn new(shaper: H, previous_bytes: i128, safe_rates: Vec<f64>) -> Self {
|
||||
State {
|
||||
current_bytes: 0,
|
||||
current_rate: 0.0,
|
||||
@@ -92,7 +76,7 @@ impl State {
|
||||
load: 0.0,
|
||||
next_rate: 0.0,
|
||||
nrate: 0,
|
||||
qdisc,
|
||||
shaper,
|
||||
previous_bytes,
|
||||
prev_t: Instant::now(),
|
||||
safe_rates,
|
||||
@@ -101,33 +85,146 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ratecontroller {
|
||||
config: Config,
|
||||
down_direction: StatsDirection,
|
||||
owd_baseline: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
||||
owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
||||
fn drain_latest_snapshot(
|
||||
snapshot_rx: &Receiver<ControlSnapshot>,
|
||||
latest: &mut Option<ControlSnapshot>,
|
||||
) {
|
||||
while let Ok(snapshot) = snapshot_rx.try_recv() {
|
||||
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>,
|
||||
state_dl: State,
|
||||
state_ul: State,
|
||||
up_direction: StatsDirection,
|
||||
state_dl: State<T::Handle>,
|
||||
state_ul: State<T::Handle>,
|
||||
rng: fastrand::Rng,
|
||||
stats_provider: S,
|
||||
traffic_control: T,
|
||||
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<()> {
|
||||
let (base_rate, delay_ms, min_rate, state) = if direction == Direction::Down {
|
||||
(
|
||||
self.config.download_base_kbits,
|
||||
self.config.download_delay_ms,
|
||||
self.config.download_min_kbits,
|
||||
self.settings.network.download_base_kbits,
|
||||
self.settings.advanced_settings.download_delay_ms,
|
||||
self.settings.network.download_min_kbits(),
|
||||
&mut self.state_dl,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
self.config.upload_base_kbits,
|
||||
self.config.upload_delay_ms,
|
||||
self.config.upload_min_kbits,
|
||||
self.settings.network.upload_base_kbits,
|
||||
self.settings.advanced_settings.upload_delay_ms,
|
||||
self.settings.network.upload_min_kbits(),
|
||||
&mut self.state_ul,
|
||||
)
|
||||
};
|
||||
@@ -155,7 +252,9 @@ impl Ratecontroller {
|
||||
/ dur.as_secs_f64();
|
||||
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();
|
||||
let max_rate = state
|
||||
.safe_rates
|
||||
@@ -166,13 +265,13 @@ impl Ratecontroller {
|
||||
* (1.0 + 0.1 * (1.0_f64 - state.current_rate / max_rate).max(0.0))
|
||||
+ (base_rate * 0.03);
|
||||
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 {
|
||||
match state
|
||||
.safe_rates
|
||||
.get(fastrand::usize(..state.safe_rates.len()))
|
||||
.get(self.rng.usize(..state.safe_rates.len()))
|
||||
{
|
||||
Some(rnd_rate) => {
|
||||
state.next_rate = rnd_rate.min(0.9 * state.current_rate * state.load);
|
||||
@@ -192,121 +291,26 @@ impl Ratecontroller {
|
||||
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<()> {
|
||||
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 lastdump_t = Instant::now();
|
||||
|
||||
// set qdisc rates to 60% of base rate to make sure we start with sane baselines
|
||||
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,
|
||||
)?;
|
||||
self.request_initial_rates()?;
|
||||
|
||||
let mut speed_hist_fd: Option<File> = None;
|
||||
let mut speed_hist_fd_inner: File;
|
||||
let mut stats_fd: Option<File> = None;
|
||||
let mut stats_fd_inner: File;
|
||||
|
||||
if !self.config.suppress_statistics {
|
||||
if !self.settings.output.suppress_statistics {
|
||||
speed_hist_fd_inner = File::options()
|
||||
.create(true)
|
||||
.write(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.flush()?;
|
||||
@@ -317,7 +321,7 @@ impl Ratecontroller {
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(self.config.stats_file.as_str())?;
|
||||
.open(self.settings.output.stats_file.as_str())?;
|
||||
|
||||
stats_fd_inner.write_all(
|
||||
"times,timens,rxload,txload,deltadelaydown,deltadelayup,dlrate,uprate\n".as_bytes(),
|
||||
@@ -335,25 +339,16 @@ impl Ratecontroller {
|
||||
sleep(sleep_time);
|
||||
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
|
||||
// change speeds here
|
||||
|
||||
(self.state_dl.current_bytes, self.state_ul.current_bytes) =
|
||||
get_interface_stats(&self.config, self.down_direction, self.up_direction)?;
|
||||
if self.state_dl.current_bytes == -1 || self.state_ul.current_bytes == -1 {
|
||||
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.state_dl.current_bytes, self.state_ul.current_bytes) = get_interface_stats(
|
||||
&mut self.stats_provider,
|
||||
&self.settings.network.upload_interface,
|
||||
)?;
|
||||
self.update_deltas()?;
|
||||
|
||||
if self.state_dl.deltas.is_empty() || self.state_ul.deltas.is_empty() {
|
||||
@@ -364,18 +359,18 @@ impl Ratecontroller {
|
||||
reflector: None,
|
||||
tags: &[],
|
||||
});
|
||||
self.state_dl.next_rate = self.config.download_min_kbits;
|
||||
self.state_ul.next_rate = self.config.upload_min_kbits;
|
||||
self.state_dl.next_rate = self.settings.network.download_min_kbits();
|
||||
self.state_ul.next_rate = self.settings.network.upload_min_kbits();
|
||||
|
||||
Netlink::set_qdisc_rate(
|
||||
self.state_dl.qdisc,
|
||||
self.traffic_control.set_rate(
|
||||
&self.state_dl.shaper,
|
||||
self.state_dl.next_rate as u64,
|
||||
self.config.dry_run,
|
||||
self.settings.advanced_settings.dry_run,
|
||||
)?;
|
||||
Netlink::set_qdisc_rate(
|
||||
self.state_ul.qdisc,
|
||||
self.traffic_control.set_rate(
|
||||
&self.state_ul.shaper,
|
||||
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;
|
||||
@@ -390,24 +385,24 @@ impl Ratecontroller {
|
||||
|| self.state_ul.next_rate != self.state_ul.current_rate
|
||||
{
|
||||
info!(
|
||||
"self.state_ul.next_rate {} self.state_dl.next_rate {}",
|
||||
self.state_ul.next_rate, self.state_dl.next_rate
|
||||
"Requesting rates (D/U): {} / {} kbit/s",
|
||||
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 {
|
||||
Netlink::set_qdisc_rate(
|
||||
self.state_dl.qdisc,
|
||||
self.traffic_control.set_rate(
|
||||
&self.state_dl.shaper,
|
||||
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 {
|
||||
Netlink::set_qdisc_rate(
|
||||
self.state_ul.qdisc,
|
||||
self.traffic_control.set_rate(
|
||||
&self.state_ul.shaper,
|
||||
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,
|
||||
});
|
||||
|
||||
if let Some(ref mut fd) = stats_fd {
|
||||
if let Err(e) = fd.write_all(
|
||||
let stats_write_error = stats_fd.as_mut().and_then(|fd| {
|
||||
fd.write_all(
|
||||
format!(
|
||||
"{},{},{},{},{},{},{},{}\n",
|
||||
stats_time.secs(),
|
||||
@@ -450,17 +445,20 @@ impl Ratecontroller {
|
||||
self.state_ul.current_rate as u64
|
||||
)
|
||||
.as_bytes(),
|
||||
) {
|
||||
)
|
||||
.err()
|
||||
});
|
||||
if let Some(e) = stats_write_error {
|
||||
warn!("Failed to write statistics: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
lastchg_t = now_t;
|
||||
}
|
||||
|
||||
if let Some(ref mut fd) = speed_hist_fd {
|
||||
if now_t.duration_since(lastdump_t).as_secs_f64() > 300.0 {
|
||||
for i in 0..self.config.speed_hist_size as usize {
|
||||
if let Some(ref mut fd) = speed_hist_fd
|
||||
&& now_t.duration_since(lastdump_t).as_secs_f64() > 300.0
|
||||
{
|
||||
for i in 0..self.settings.advanced_settings.speed_hist_size as usize {
|
||||
let hist_time = Time::new(ClockId::Realtime);
|
||||
if let Err(e) = fd.write_all(
|
||||
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::baseliner::ControlSnapshot;
|
||||
use crate::metrics::{Metric, MetricsSender};
|
||||
use crate::util::{MutexExt, RwLockExt};
|
||||
use crate::{Config, ReflectorStats};
|
||||
use crate::settings::Settings;
|
||||
use crate::util::RwLockExt;
|
||||
use flume::{Receiver, RecvError, RecvTimeoutError, Selector};
|
||||
use log::{debug, info};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::{Arc, RwLock};
|
||||
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 config: Config,
|
||||
pub owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
||||
pub settings: Settings,
|
||||
pub snapshot_rx: Receiver<ControlSnapshot>,
|
||||
pub reflector_peers_lock: Arc<RwLock<Vec<IpAddr>>>,
|
||||
pub reflector_pool: Vec<IpAddr>,
|
||||
pub trigger_channel: Receiver<bool>,
|
||||
@@ -24,25 +117,36 @@ impl ReflectorSelector {
|
||||
pub fn run(&self) -> anyhow::Result<()> {
|
||||
let mut selector_sleep_time = Duration::new(30, 0);
|
||||
let mut reselection_count = 0;
|
||||
let baseline_sleep_time =
|
||||
Duration::from_secs_f64(self.config.tick_interval * std::f64::consts::PI);
|
||||
let mut latest_snapshot = None;
|
||||
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
|
||||
sleep(baseline_sleep_time);
|
||||
receive_snapshots_until(
|
||||
&self.snapshot_rx,
|
||||
&mut latest_snapshot,
|
||||
Instant::now() + baseline_sleep_time,
|
||||
);
|
||||
|
||||
loop {
|
||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||
info!("Reflector selector shutting down");
|
||||
return Ok(());
|
||||
}
|
||||
/*
|
||||
* Selection is triggered either by some other thread triggering it through the channel,
|
||||
* or it passes the timeout. In any case we don't care about the result of this function,
|
||||
* so we ignore the result of it.
|
||||
*/
|
||||
let triggered = self
|
||||
.trigger_channel
|
||||
.recv_timeout(selector_sleep_time)
|
||||
.is_ok();
|
||||
|
||||
let triggered = match wait_for_trigger_or_deadline(
|
||||
&self.trigger_channel,
|
||||
&self.snapshot_rx,
|
||||
&mut latest_snapshot,
|
||||
Instant::now() + selector_sleep_time,
|
||||
) {
|
||||
WaitOutcome::Triggered => true,
|
||||
WaitOutcome::DeadlineReached => false,
|
||||
WaitOutcome::ChannelsClosed => {
|
||||
info!("Reflector selector channels closed, shutting down");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
reselection_count += 1;
|
||||
info!("Starting reselection [#{}]", reselection_count);
|
||||
self.metrics.send(Metric::Event {
|
||||
@@ -54,7 +158,10 @@ impl ReflectorSelector {
|
||||
|
||||
// After 40 reselections, slow down to every 15 minutes
|
||||
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();
|
||||
@@ -62,7 +169,7 @@ impl ReflectorSelector {
|
||||
|
||||
// Include all current peers
|
||||
for reflector in reflectors_peers.iter() {
|
||||
debug!("Current peer: {}", reflector.to_string());
|
||||
debug!("Current peer: {}", reflector);
|
||||
next_peers.push(*reflector);
|
||||
}
|
||||
|
||||
@@ -72,7 +179,7 @@ impl ReflectorSelector {
|
||||
if next_peers.contains(next_candidate) {
|
||||
continue;
|
||||
}
|
||||
debug!("Next candidate: {}", next_candidate.to_string());
|
||||
debug!("Next candidate: {}", next_candidate);
|
||||
next_peers.push(*next_candidate);
|
||||
}
|
||||
|
||||
@@ -86,23 +193,25 @@ impl ReflectorSelector {
|
||||
|
||||
debug!("Waiting for candidates to be 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
|
||||
reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
let owd_recent = self.owd_recent.lock_anyhow()?;
|
||||
|
||||
for peer in next_peers {
|
||||
if owd_recent.contains_key(&peer) {
|
||||
let rtt = (owd_recent[&peer].down_ewma + owd_recent[&peer].up_ewma) as u64;
|
||||
if let Some(rtt) = recent_rtt_for_peer(latest_snapshot.as_ref(), &peer) {
|
||||
candidates.push((peer, rtt));
|
||||
info!("Candidate reflector: {} RTT: {}", peer.to_string(), rtt);
|
||||
info!("Candidate reflector: {} RTT: {}", peer, rtt);
|
||||
} else {
|
||||
info!(
|
||||
"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));
|
||||
|
||||
// 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;
|
||||
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,350 @@
|
||||
// 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, address @ IpAddr::V6(_)) => {
|
||||
bail!("reflector {address} is IPv6; only IPv4 reflectors are supported");
|
||||
}
|
||||
(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,423 @@
|
||||
// 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 < 1.0 {
|
||||
report.error(
|
||||
"network.download_min_percent",
|
||||
"minimum download rate floors to zero",
|
||||
);
|
||||
} else 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 < 1.0 {
|
||||
report.error(
|
||||
"network.upload_min_percent",
|
||||
"minimum upload rate floors to zero",
|
||||
);
|
||||
} else 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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn network(download_base_kbits: f64, upload_base_kbits: f64) -> NetworkSettings {
|
||||
NetworkSettings {
|
||||
download_interface: "ifb0".into(),
|
||||
upload_interface: "eth0".into(),
|
||||
download_base_kbits,
|
||||
download_min_percent: 1.0,
|
||||
upload_base_kbits,
|
||||
upload_min_percent: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimum_rates_that_floor_to_zero_are_errors_for_both_directions() {
|
||||
let mut report = ValidationReport::default();
|
||||
network(50.0, 50.0).validate(&mut report);
|
||||
let error = report.finish().unwrap_err().to_string();
|
||||
|
||||
assert!(error.contains("network.download_min_percent"));
|
||||
assert!(error.contains("network.upload_min_percent"));
|
||||
assert!(error.contains("floors to zero"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn representable_low_minimum_rates_remain_warnings() {
|
||||
let mut report = ValidationReport::default();
|
||||
network(10_000.0, 10_000.0).validate(&mut report);
|
||||
|
||||
assert!(report.finish().is_ok());
|
||||
}
|
||||
}
|
||||
+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::thread::ClockId;
|
||||
use rustix::time::clock_gettime;
|
||||
use rustix::time::{ClockId, clock_gettime};
|
||||
|
||||
pub struct Time {
|
||||
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 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> {
|
||||
fn lock_anyhow(&self) -> anyhow::Result<MutexGuard<'_, T>>;
|
||||
|
||||
Reference in new issue
Block a user