Compare commits
142
Commits
v0.4.0
..
f851972062
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f851972062 | ||
|
|
a58e6d1ce5 | ||
|
|
4770b4fb5d | ||
|
|
eaa20e79d2 | ||
|
|
41929875f7 | ||
|
|
8aa971920c | ||
|
|
2efa0d30c2 | ||
|
|
f1a50af7d3 | ||
|
|
5624746914 | ||
|
|
2e24da4bf8 | ||
|
|
8c923f373d | ||
|
|
8267089733 | ||
|
|
6ad41931a1 | ||
|
|
b94742c62a | ||
|
|
7e8213556f | ||
|
|
df7e9ffbab | ||
|
|
68b34a587f | ||
|
|
b7bbed6e5e | ||
|
|
0e2e2f9d38 | ||
|
|
e80940ba6d | ||
|
|
480976b666 | ||
|
|
ac349a1897 | ||
|
|
a925e60f36 | ||
|
|
c7b283d761 | ||
|
|
89c7d75947 | ||
|
|
afedfbeb65 | ||
|
|
f51ff19f04 | ||
|
|
60fdea4dff | ||
|
|
3016f4b887 | ||
|
|
52a5ab2dfe | ||
|
|
fca0967563 | ||
|
|
b7b66fc1f4 | ||
|
|
305a182001 | ||
|
|
64e002c0d0 | ||
|
|
817cc304e6 | ||
|
|
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,114 @@
|
|||||||
|
name: Build binaries
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
# Measure before stripping; the packaged copy is stripped below.
|
||||||
|
CARGO_PROFILE_RELEASE_STRIP: none
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (${{ matrix.target }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
TARGET: ${{ matrix.target }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
# Supported by Gitea 28; bound concurrent cross-build memory use.
|
||||||
|
max-parallel: 2
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- x86_64-musl
|
||||||
|
- i686-musl
|
||||||
|
- arm-musleabi
|
||||||
|
- arm-musleabihf
|
||||||
|
- armv7-musleabi
|
||||||
|
- armv7-musleabihf
|
||||||
|
- aarch64-musl
|
||||||
|
- mips-musl
|
||||||
|
- mipsel-musl
|
||||||
|
- mips64-muslabi64
|
||||||
|
- mips64el-muslabi64
|
||||||
|
- riscv64gc-musl
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: builtin:checkout
|
||||||
|
|
||||||
|
- name: Configure Rust directories
|
||||||
|
# Cross remote's symlink traversal skips hidden directories under /root.
|
||||||
|
run: |
|
||||||
|
echo "CARGO_HOME=$RUNNER_TEMP/cargo" >> "$GITHUB_ENV"
|
||||||
|
echo "RUSTUP_HOME=$RUNNER_TEMP/rustup" >> "$GITHUB_ENV"
|
||||||
|
echo "$RUNNER_TEMP/cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||||
|
with:
|
||||||
|
toolchain: ${{ startsWith(matrix.target, 'mips') && 'nightly' || 'stable' }}
|
||||||
|
cache: false
|
||||||
|
build-warnings: warn
|
||||||
|
|
||||||
|
- name: Install cross and binary tools
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends binutils llvm
|
||||||
|
cargo install --git https://github.com/cross-rs/cross --locked cross
|
||||||
|
echo "RUST_TARGET=${TARGET%-*}-unknown-linux-${TARGET#*-}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
env:
|
||||||
|
# MIPS build-std lacks Rust's bundled CRT objects; use the cross GCC copies.
|
||||||
|
RUSTFLAGS: ${{ startsWith(matrix.target, 'mips') && '-C target-feature=+crt-static -C link-self-contained=no' || '-C target-feature=+crt-static' }}
|
||||||
|
# Copy toolchains and sources instead of translating nested bind mounts.
|
||||||
|
CROSS_REMOTE: "true"
|
||||||
|
# Cross's per-artifact JSON capture hides compiler diagnostics.
|
||||||
|
CROSS_REMOTE_COPY_FULL_TARGET_DIR: ${{ startsWith(matrix.target, 'mips') && 'true' || 'false' }}
|
||||||
|
run: |
|
||||||
|
cross build --release --locked --target "$RUST_TARGET"
|
||||||
|
|
||||||
|
- name: Verify, package, and summarize binary
|
||||||
|
run: |
|
||||||
|
binary="target/${RUST_TARGET}/release/sqm-autorate-rust"
|
||||||
|
readelf --program-headers "$binary" > /tmp/program-headers.txt
|
||||||
|
readelf --dynamic "$binary" > /tmp/dynamic-section.txt
|
||||||
|
if grep -q INTERP /tmp/program-headers.txt || grep -q NEEDED /tmp/dynamic-section.txt; then
|
||||||
|
echo "Binary is not statically linked" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p dist
|
||||||
|
stripped="dist/sqm-autorate-rust-${TARGET}"
|
||||||
|
artifact="${stripped}.tar.gz"
|
||||||
|
cp "$binary" "$stripped"
|
||||||
|
llvm-strip --strip-all "$stripped"
|
||||||
|
# Tar preserves executable permissions when downloading the artifact.
|
||||||
|
tar -czf "$artifact" -C dist "sqm-autorate-rust-${TARGET}"
|
||||||
|
|
||||||
|
size() {
|
||||||
|
stat -c %s "$1" | numfmt --to=iec-i --suffix=B --format='%.1f'
|
||||||
|
}
|
||||||
|
{
|
||||||
|
echo '| Target | Unstripped | Stripped | Compressed |'
|
||||||
|
echo '|:--|--:|--:|--:|'
|
||||||
|
printf '| %s | %s | %s | %s |\n' \
|
||||||
|
"$TARGET" "$(size "$binary")" "$(size "$stripped")" "$(size "$artifact")"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
# Use the v3 artifact protocol supported by Gitea.
|
||||||
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
|
with:
|
||||||
|
name: sqm-autorate-rust-${{ matrix.target }}
|
||||||
|
path: dist/sqm-autorate-rust-${{ matrix.target }}.tar.gz
|
||||||
|
if-no-files-found: error
|
||||||
@@ -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,66 @@
|
|||||||
|
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
|
||||||
|
# actionlint does not recognize Gitea's built-in checkout action.
|
||||||
|
run: |
|
||||||
|
"$(go env GOPATH)/bin/actionlint" \
|
||||||
|
-ignore 'specifying action "builtin:checkout" in invalid format' \
|
||||||
|
.gitea/workflows/build.yml \
|
||||||
|
.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@85b2b9fb8e3325e7a23d7d0a9d7ba5ae0f999e3e # v0.7.1
|
||||||
|
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.
|
# This workflow uploads SARIF results to GitHub Code Scanning.
|
||||||
# They are provided by a third-party and are governed by
|
|
||||||
# separate terms of service, privacy policy, and support
|
|
||||||
# documentation.
|
|
||||||
|
|
||||||
name: DevSkim
|
name: DevSkim
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [master]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "master" ]
|
branches: [master]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '18 0 * * 4'
|
- cron: '18 0 * * 4'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: DevSkim
|
name: DevSkim
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
|
||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Run DevSkim scanner
|
- name: Run DevSkim scanner
|
||||||
uses: microsoft/DevSkim-Action@v1
|
uses: microsoft/DevSkim-Action@v1
|
||||||
|
|
||||||
- name: Upload DevSkim scan results to GitHub Security tab
|
- name: Upload DevSkim scan results to GitHub Security tab
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,55 +1,34 @@
|
|||||||
# Copyright 2023 Google LLC
|
# Copyright 2023 Google LLC
|
||||||
#
|
# Licensed under the Apache License, Version 2.0.
|
||||||
# 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.
|
|
||||||
|
|
||||||
name: OSV-Scanner Scheduled Scan
|
name: OSV-Scanner
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["master", "v1"]
|
branches: [master, v1]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "12 12 * * 1"
|
- cron: "12 12 * * 1"
|
||||||
push:
|
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: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
scan-scheduled:
|
scan-scheduled:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read
|
||||||
security-events: write # for uploading SARIF files
|
security-events: write
|
||||||
actions: read
|
actions: read
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
|
||||||
# If you want to copy this config, highly suggest pinning this version to a release rather than tracking the main branch
|
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.7.0
|
||||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.3"
|
|
||||||
with:
|
with:
|
||||||
# Just scan the root directory and docs, since everything else is fixtures
|
scan-args: ./
|
||||||
scan-args: |-
|
|
||||||
./
|
|
||||||
./docs/
|
|
||||||
scan-pr:
|
scan-pr:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read
|
||||||
security-events: write # for uploading SARIF files
|
security-events: write
|
||||||
actions: read
|
actions: read
|
||||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||||
# If you want to copy this config, highly suggest pinning this version to a release rather than tracking the main branch
|
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.7.0
|
||||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.3"
|
|
||||||
with:
|
with:
|
||||||
# Just scan the root directory and docs, since everything else is fixtures
|
scan-args: ./
|
||||||
scan-args: |-
|
|
||||||
./
|
|
||||||
./docs/
|
|
||||||
@@ -1,55 +1,61 @@
|
|||||||
# This workflow uses actions that are not certified by GitHub.
|
name: Clippy SARIF
|
||||||
# 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
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [master]
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ "master" ]
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '28 20 * * 3'
|
- cron: "17 12 * * 4"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
permissions:
|
||||||
rust-clippy-analyze:
|
|
||||||
name: Run rust-clippy analyzing
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
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:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1
|
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
components: clippy
|
components: clippy
|
||||||
override: true
|
cache: false
|
||||||
|
- name: Install SARIF tools
|
||||||
- name: Install required cargo
|
run: cargo install --locked clippy-sarif sarif-fmt
|
||||||
run: cargo install clippy-sarif sarif-fmt
|
- name: Run Clippy and generate SARIF
|
||||||
|
id: clippy
|
||||||
- name: Run rust-clippy
|
shell: bash
|
||||||
run:
|
run: |
|
||||||
cargo clippy
|
set +e
|
||||||
--all-features
|
cargo clippy \
|
||||||
--message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt
|
--workspace \
|
||||||
continue-on-error: true
|
--all-targets \
|
||||||
|
--all-features \
|
||||||
- name: Upload analysis results to GitHub
|
--locked \
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
--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@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
|
||||||
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: rust-clippy-results.sarif
|
sarif_file: rust-clippy-results.sarif
|
||||||
|
category: clippy
|
||||||
wait-for-processing: true
|
wait-for-processing: true
|
||||||
|
- name: Fail if Clippy failed
|
||||||
|
if: steps.clippy.outputs.exit_code != '0'
|
||||||
|
run: exit 1
|
||||||
Generated
+712
-193
File diff suppressed because it is too large.
Load diff
+26
-7
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sqm-autorate-rust"
|
name = "sqm-autorate-rust"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@@ -20,18 +20,37 @@ strip = "symbols"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
icmp-socket2 = "0.4.2"
|
config = { version = "0.15.27", default-features = false }
|
||||||
netlink-bindings = { version = "0.2", features = ["rt-link", "tc"] }
|
csv = "1.4.0"
|
||||||
netlink-socket2 = "0.2"
|
|
||||||
fastrand = "2"
|
fastrand = "2"
|
||||||
rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] }
|
flume = { version = "0.12.0", default-features = false, features = ["async", "eventual-fairness", "select"] }
|
||||||
rust-uci = { version = "0.1", optional = true }
|
icmp-socket2 = { version = "0.6.4", features = ["tokio"] }
|
||||||
thiserror = "2.0"
|
irtt-client = { version = "0.6.0", optional = true, features = ["ancillary", "tokio"] }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
thiserror = "2.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
uci-derive = { path = "crates/uci-derive" }
|
||||||
|
|
||||||
[dependencies.log]
|
[dependencies.log]
|
||||||
features = ["std"]
|
features = ["std"]
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
netlink-bindings = { version = "0.3.5", features = ["rt-link", "tc"] }
|
||||||
|
netlink-socket2 = { version = "0.3.5" }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
rust-uci = { git = "https://github.com/Lochnair/rust-uci.git", rev = "6835397f4f4ba8b69e75e727105186865c357417", optional = true, default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["irtt", "toml", "uci-vendored"]
|
||||||
|
irtt = ["dep:irtt-client"]
|
||||||
|
toml = ["config/toml"]
|
||||||
uci = ["dep:rust-uci"]
|
uci = ["dep:rust-uci"]
|
||||||
|
uci-vendored = ["uci", "rust-uci/vendored"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/uci-derive"]
|
||||||
|
resolver = "3"
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
[target.x86_64-unknown-freebsd]
|
||||||
|
image.name = "ghcr.io/cross-rs/x86_64-unknown-freebsd:local"
|
||||||
|
image.toolchain = ["linux/arm64=aarch64-unknown-linux-gnu"]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
# Vendored UCI searches CMAKE_LIBRARY_PATH for libubox built outside the sysroot.
|
||||||
|
pre-build = [
|
||||||
|
"apt-get update && apt-get install -y --no-install-recommends pkg-config",
|
||||||
|
'echo "set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)" >> /opt/toolchain.cmake',
|
||||||
|
# Rust's musl std links -lunwind; MIPS cross GCC supplies the unwinder as libgcc_eh.a.
|
||||||
|
'''
|
||||||
|
for cc in /usr/local/bin/mips*-gcc; do
|
||||||
|
[ -x "$cc" ] || continue
|
||||||
|
archive=$("$cc" -print-file-name=libgcc_eh.a)
|
||||||
|
test -f "$archive" || exit 1
|
||||||
|
ln -sf libgcc_eh.a "$(dirname "$archive")/libunwind.a"
|
||||||
|
done
|
||||||
|
''',
|
||||||
|
]
|
||||||
|
|
||||||
|
# This target's image was added after the cross 0.2.5 release.
|
||||||
|
[target.riscv64gc-unknown-linux-musl]
|
||||||
|
image = "ghcr.io/cross-rs/riscv64gc-unknown-linux-musl:main"
|
||||||
|
|
||||||
|
# MIPS images exist but are excluded from cross's automatic image selection.
|
||||||
|
[target.mips-unknown-linux-musl]
|
||||||
|
image = "ghcr.io/cross-rs/mips-unknown-linux-musl:main"
|
||||||
|
build-std = ["std", "panic_abort"]
|
||||||
|
|
||||||
|
[target.mipsel-unknown-linux-musl]
|
||||||
|
image = "ghcr.io/cross-rs/mipsel-unknown-linux-musl:main"
|
||||||
|
build-std = ["std", "panic_abort"]
|
||||||
|
|
||||||
|
[target.mips64-unknown-linux-muslabi64]
|
||||||
|
image = "ghcr.io/cross-rs/mips64-unknown-linux-muslabi64:main"
|
||||||
|
build-std = ["std", "panic_abort"]
|
||||||
|
|
||||||
|
[target.mips64el-unknown-linux-muslabi64]
|
||||||
|
image = "ghcr.io/cross-rs/mips64el-unknown-linux-muslabi64:main"
|
||||||
|
build-std = ["std", "panic_abort"]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ARG TARGET
|
||||||
|
|
||||||
|
FROM ghcr.io/rust-cross/rust-musl-cross:${TARGET}
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
Vendored
+6
-3
@@ -12,9 +12,9 @@ pipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
agent {
|
agent {
|
||||||
docker {
|
dockerfile {
|
||||||
image "ghcr.io/rust-cross/rust-musl-cross:${TARGET}"
|
filename 'Dockerfile.ci'
|
||||||
alwaysPull true
|
additionalBuildArgs "--pull --build-arg TARGET=${TARGET}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +44,9 @@ pipeline {
|
|||||||
echo "----------------------------------"
|
echo "----------------------------------"
|
||||||
|
|
||||||
# 5. Check Toolchain & Build
|
# 5. Check Toolchain & Build
|
||||||
|
# Force standalone static artifacts across the entire CI matrix.
|
||||||
|
export RUSTFLAGS="${RUSTFLAGS:+$RUSTFLAGS }-C target-feature=+crt-static"
|
||||||
|
|
||||||
# We grep for "nightly" in the version string (e.g., "rustc 1.95.0-nightly...")
|
# We grep for "nightly" in the version string (e.g., "rustc 1.95.0-nightly...")
|
||||||
if rustc -V | grep -q "nightly"; then
|
if rustc -V | grep -q "nightly"; then
|
||||||
echo "Detected Nightly toolchain. Enabling '-Z build-std' to support panic=abort..."
|
echo "Detected Nightly toolchain. Enabling '-Z build-std' to support panic=abort..."
|
||||||
|
|||||||
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -57,3 +57,23 @@ You can also build an OpenWrt package to install in your images with the SDK:
|
|||||||
# Copy the generated .ipk from bin/packages/... to your router
|
# Copy the generated .ipk from bin/packages/... to your router
|
||||||
opkg install sqm-autorate-rust_*.ipk
|
opkg install sqm-autorate-rust_*.ipk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reflector files and inline UCI reflectors
|
||||||
|
|
||||||
|
Inline `config reflector` sections supplement the default
|
||||||
|
`/etc/sqm-autorate-rust/reflectors.csv`. To use only inline reflectors, explicitly
|
||||||
|
disable file loading:
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config reflectors
|
||||||
|
option files 'none'
|
||||||
|
|
||||||
|
config reflector
|
||||||
|
option host 'global.irtt.lochnair.net'
|
||||||
|
list protocols 'irtt'
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `list files '/path/to/reflectors.csv'` instead of `option files 'none'` to
|
||||||
|
combine CSV files with inline reflectors. Omitting `files` keeps the default CSV;
|
||||||
|
UCI discards empty string options, so `option files ''` does not disable it.
|
||||||
|
Absent or blank `irtt_hmac` values use unauthenticated IRTT sessions.
|
||||||
@@ -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,279 @@
|
|||||||
|
// 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 without `#[uci(skip)]` is mapped one-to-one:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// Rust field name -> UCI option name
|
||||||
|
/// Rust field name -> config-rs field name
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_derive(UciSection, attributes(uci))]
|
||||||
|
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 mut mappings = Vec::with_capacity(fields.named.len());
|
||||||
|
|
||||||
|
for field in &fields.named {
|
||||||
|
if parse_skip(field)? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
mappings.push(quote! {
|
||||||
|
crate::settings::uci_schema::UciOptionMapping {
|
||||||
|
uci_option: #field_name,
|
||||||
|
config_field: #field_name,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#[cfg(all(feature = "uci", unix))]
|
||||||
|
#[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 parse_skip(field: &Field) -> syn::Result<bool> {
|
||||||
|
let mut skip = false;
|
||||||
|
|
||||||
|
for attribute in &field.attrs {
|
||||||
|
if !attribute.path().is_ident("uci") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute.parse_nested_meta(|meta| {
|
||||||
|
if !meta.path.is_ident("skip") {
|
||||||
|
return Err(meta.error("expected `skip`"));
|
||||||
|
}
|
||||||
|
if skip {
|
||||||
|
return Err(meta.error("duplicate `skip` attribute"));
|
||||||
|
}
|
||||||
|
skip = true;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(skip)
|
||||||
|
}
|
||||||
|
|
||||||
|
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! {
|
||||||
|
#[cfg(all(feature = "uci", unix))]
|
||||||
|
#[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 = []
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
reflector_ip,ip_version,description
|
|
||||||
94.140.14.14,4,AdGuard
|
|
||||||
94.140.15.15,4,AdGuard
|
|
||||||
94.140.14.15,4,AdGuard Family
|
|
||||||
94.140.15.16,4,AdGuard Family
|
|
||||||
94.140.14.140,4,AdGuard Unfiltered
|
|
||||||
94.140.14.141,4,AdGuard Unfiltered
|
|
||||||
205.171.3.65,4,CenturyLink/Level3
|
|
||||||
205.171.2.65,4,CenturyLink/Level3
|
|
||||||
185.228.168.9,4,CleanBrowsing
|
|
||||||
185.228.169.9,4,CleanBrowsing
|
|
||||||
185.228.168.168,4,CleanBrowsing
|
|
||||||
185.228.169.168,4,CleanBrowsing
|
|
||||||
185.228.168.10,4,CleanBrowsing
|
|
||||||
185.228.169.11,4,CleanBrowsing
|
|
||||||
8.26.56.26,4,Comodo
|
|
||||||
8.20.247.20,4,Comodo
|
|
||||||
84.200.69.80,4,DNS.WATCH
|
|
||||||
84.200.70.40,4,DNS.WATCH
|
|
||||||
46.227.200.54,4,FAELIX Anycast
|
|
||||||
46.227.200.55,4,FAELIX Anycast
|
|
||||||
45.33.97.5,4,FreeDNS
|
|
||||||
37.235.1.177,4,FreeDNS
|
|
||||||
74.82.42.42,4,Hurricane Electric
|
|
||||||
194.242.2.2,4,Mullvad
|
|
||||||
194.242.2.3,4,Mullvad
|
|
||||||
193.19.108.2,4,Mullvad
|
|
||||||
193.19.108.3,4,Mullvad
|
|
||||||
208.67.222.222,4,OpenDNS
|
|
||||||
208.67.220.220,4,OpenDNS
|
|
||||||
208.67.222.123,4,OpenDNS Family
|
|
||||||
208.67.220.123,4,OpenDNS Family
|
|
||||||
208.67.222.2,4,OpenDNS Sbx
|
|
||||||
208.67.220.2,4,OpenDNS Sbx
|
|
||||||
159.89.120.99,4,OpenNIC
|
|
||||||
134.195.4.2,4,OpenNIC
|
|
||||||
9.9.9.9,4,Quad9
|
|
||||||
9.9.9.10,4,Quad9
|
|
||||||
9.9.9.11,4,Quad9
|
|
||||||
149.112.112.10,4,Quad9
|
|
||||||
149.112.112.11,4,Quad9
|
|
||||||
149.112.112.112,4,Quad9
|
|
||||||
176.9.93.198,4,dnsforge.de
|
|
||||||
176.9.1.117,4,dnsforge.de
|
|
||||||
104.236.210.29,4,DNSReactor
|
|
||||||
37.235.1.174,4,FreeDNS
|
|
||||||
81.218.119.1,4,GreenTeamDNS
|
|
||||||
194.8.57.12,4,Nuernberg Internet Exchange (N-IX)
|
|
||||||
51.89.30.51,4,PublicDNS.eu
|
|
||||||
51.89.93.25,4,PublicDNS.eu
|
|
||||||
130.59.31.248,4,SWITCH
|
|
||||||
130.59.31.251,4,SWITCH
|
|
||||||
185.49.141.37,4,GetDNS
|
|
||||||
158.64.1.29,4,Restena Foundation
|
|
||||||
89.234.186.112,4,Neutopia
|
|
||||||
185.95.218.42,4,Digitale Gesellschaft Schweiz
|
|
||||||
185.95.218.43,4,Digitale Gesellschaft Schweiz
|
|
||||||
140.238.215.192,4,post-factum
|
|
||||||
199.58.81.218,4,CMRG DNS
|
|
||||||
146.255.56.98,4,Foundation for Applied Privacy
|
|
||||||
185.222.222.222,4,dns.sb
|
|
||||||
185.184.222.222,4,dns.sb
|
|
||||||
5.1.66.255,4,Freifunk München e.V.
|
|
||||||
80.80.80.80,4,Freenom World
|
|
||||||
80.80.81.81,4,Freenom World
|
|
||||||
80.67.169.12,4,French Data Network (FDN)
|
|
||||||
80.67.169.40,4,French Data Network (FDN)
|
|
||||||
217.24.255.134,4,Albtelecom Sh.a.
|
|
||||||
213.163.127.229,4,Telekomi i Kosoves SH.A.
|
|
||||||
85.94.178.198,4,ANDORRA TELECOM SAU
|
|
||||||
186.148.128.86,4,ETERNET S.R.L.
|
|
||||||
190.151.144.21,4,Lima Video Cable S.A. (Cabletel)
|
|
||||||
200.45.184.43,4,Telecom Argentina S.A.
|
|
||||||
179.60.232.10,4,WICORP SA
|
|
||||||
170.210.83.110,4,Red de Interconexion Universitaria
|
|
||||||
190.57.234.194,4,Gigared S.A.
|
|
||||||
186.153.180.148,4,Telecom Argentina S.A.
|
|
||||||
81.16.8.110,4,Ucom CJSC
|
|
||||||
45.133.105.123,4,GNC-Alfa CJSC
|
|
||||||
61.8.0.113,4,Telstra Corporation Ltd
|
|
||||||
103.224.162.40,4,Ezi-Web
|
|
||||||
139.130.4.4,4,Telstra Corporation Ltd
|
|
||||||
139.134.5.51,4,Telstra Corporation Ltd
|
|
||||||
139.134.2.190,4,Telstra Corporation Ltd
|
|
||||||
203.50.2.71,4,Telstra Corporation Ltd
|
|
||||||
83.137.41.8,4,nemox.net Informationstechnologie OG
|
|
||||||
83.137.41.9,4,nemox.net Informationstechnologie OG
|
|
||||||
194.36.144.87,4,netcup GmbH
|
|
||||||
188.118.227.21,4,Hutchison Drei Austria GmbH
|
|
||||||
83.218.176.140,4,myNet GmbH
|
|
||||||
85.132.85.85,4,Delta Telecom Ltd
|
|
||||||
45.250.253.222,4,New Generation Internet Services Limited
|
|
||||||
103.77.188.18,4,NETSCOPE
|
|
||||||
103.77.188.19,4,NETSCOPE
|
|
||||||
103.80.1.2,4,SS Online
|
|
||||||
103.153.154.2,4,Pabna Cable Vision Dot Net
|
|
||||||
114.130.5.5,4,Tire-1 IP Transit Provider of Bangladesh
|
|
||||||
114.130.5.6,4,Tire-1 IP Transit Provider of Bangladesh
|
|
||||||
103.146.221.20,4,Kloud Technologies Limited
|
|
||||||
103.146.221.21,4,Kloud Technologies Limited
|
|
||||||
115.127.19.129,4,BRACNet Limited
|
|
||||||
115.127.19.137,4,BRACNet Limited
|
|
||||||
103.113.200.10,4,National University
|
|
||||||
177.131.114.86,4,ACESSOLINE TELECOMUNICACOES LTDA
|
|
||||||
177.66.203.10,4,Downup Telecomunicacoes e servico LTDA
|
|
||||||
177.184.131.180,4,SOBRALNET SERVICOS E TELECOMUNICACOES LTDA - ME
|
|
||||||
186.194.224.82,4,R&R PROVEDOR DE INTERNET LTDA
|
|
||||||
186.225.194.29,4,Pannet Serviços On Line Ltda
|
|
||||||
189.42.239.34,4,CLARO S.A.
|
|
||||||
200.99.138.94,4,TELEFÔNICA BRASIL S.A
|
|
||||||
200.99.138.103,4,TELEFÔNICA BRASIL S.A
|
|
||||||
200.167.191.114,4,CLARO S.A.
|
|
||||||
200.169.8.1,4,Century Telecom Ltda
|
|
||||||
141.0.102.202,4,Altibox AS
|
|
||||||
62.97.223.62,4,BKK Digitek AS
|
|
||||||
217.18.206.12,4,EVRY NORGE AS
|
|
||||||
217.18.206.22,4,EVRY NORGE AS
|
|
||||||
195.204.130.85,4,GLOBALCONNECT AS
|
|
||||||
81.175.38.1,4,GLOBALCONNECT AS
|
|
||||||
46.182.19.48,4,Martin Prager trading as NbIServ
|
|
||||||
82.96.64.2,4,Probe Networks
|
|
||||||
94.247.43.254,4,OpenNIC
|
|
||||||
195.10.195.195,4,meerfarbig GmbH & Co. KG
|
|
||||||
194.172.160.4,4,Baumann Technologie GmbH
|
|
||||||
185.233.106.232,4,netcup GmbH
|
|
||||||
185.233.107.4,4,netcup GmbH
|
|
||||||
141.1.1.1,4,Vodafone Group PLC
|
|
||||||
141.1.27.249,4,Vodafone Group PLC
|
|
||||||
176.103.130.130,4,Serveroid LLC
|
|
||||||
176.103.130.131,4,Serveroid LLC
|
|
||||||
80.243.64.67,4,PJSC Vimpelcom
|
|
||||||
195.218.133.88,4,PJSC Vimpelcom
|
|
||||||
83.69.120.1,4,PJSC Vimpelcom
|
|
||||||
46.16.229.223,4,JSC Elektrosvyaz
|
|
||||||
193.183.98.66,4,Prometeus di Daniela Agro
|
|
||||||
212.19.96.2,4,Leonet4cloud srl
|
|
||||||
217.12.181.97,4,IRIDEOS S.P.A.
|
|
||||||
62.149.128.4,4,Aruba S.p.A.
|
|
||||||
62.149.132.2,4,Aruba S.p.A.
|
|
||||||
85.47.198.82,4,Telecom Italia S.p.A.
|
|
||||||
202.136.162.11,4,ASN - NTT SINGAPORE PTE LTD
|
|
||||||
51.79.144.245,4,OVH SAS
|
|
||||||
61.46.244.211,4,Jupiter Telecommunications Co. Ltd.
|
|
||||||
1.33.184.173,4,NTT PC Communications Inc.
|
|
||||||
1.33.184.116,4,NTT PC Communications Inc.
|
|
||||||
1.33.184.174,4,NTT PC Communications Inc.
|
|
||||||
1.33.184.108,4,NTT PC Communications Inc.
|
|
||||||
1.33.184.220,4,NTT PC Communications Inc.
|
|
||||||
1.33.184.230,4,NTT PC Communications Inc.
|
|
||||||
1.33.184.158,4,NTT PC Communications Inc.
|
|
||||||
62.212.113.125,4,Nerim SAS
|
|
||||||
185.30.211.154,4,ENTREPRISE DECIMA SARL
|
|
||||||
87.240.90.128,4,KERTEL SAS
|
|
||||||
45.77.61.165,4,The Constant Company LLC
|
|
||||||
178.62.57.141,4,DigitalOcean LLC
|
|
||||||
194.145.240.6,4,M247 Ltd
|
|
||||||
195.27.1.1,4,Vodafone Group PLC
|
|
||||||
81.17.66.13,4,South West Communications Group Ltd
|
|
||||||
87.117.196.200,4,IOMART CLOUD SERVICES LIMITED
|
|
||||||
213.123.251.87,4,British Telecommunications PLC
|
|
||||||
81.149.21.254,4,British Telecommunications PLC
|
|
||||||
196.21.247.1,4,TENET (The UNINET Project)
|
|
||||||
89.140.69.49,4,VODAFONE ONO S.A.
|
|
||||||
62.97.123.107,4,COLT Technology Services Group Limited
|
|
||||||
194.177.210.210,4,National Infrastructures for Research and Technology S.A.
|
|
||||||
168.126.63.1,4,Korea Telecom
|
|
||||||
121.254.134.99,4,LG DACOM Corporation
|
|
||||||
203.239.131.1,4,ELIMNET INC.
|
|
||||||
202.57.32.49,4,A Multihomed ISP Company
|
|
||||||
81.17.31.34,4,Private Layer INC
|
|
||||||
79.141.82.250,4,NTD SA
|
|
||||||
80.254.77.39,4,Monzoon Networks AG
|
|
||||||
85.93.212.2,4,Visual Online S.A.
|
|
||||||
196.223.158.58,4,MAX NET FOR INTERNET SERVICES
|
|
||||||
89.150.195.195,4,Media Network i Halmstad AB
|
|
||||||
130.244.126.100,4,TELE2
|
|
||||||
87.96.144.179,4,A3 Sverige AB
|
|
||||||
146.247.80.144,4,A3 Sverige AB
|
|
||||||
46.246.29.69,4,GleSYS AB
|
|
||||||
203.210.237.165,4,VNPT Corp
|
|
||||||
113.190.253.229,4,VNPT Corp
|
|
||||||
119.29.29.29,4,Shenzhen Tencent Computer Systems Company Limited
|
|
||||||
209.193.4.7,4,Alaska Communications
|
|
||||||
209.112.160.2,4,Alaska Communications
|
|
||||||
216.68.4.10,4,Cincinnati Bell ZoomTown
|
|
||||||
216.68.5.10,4,Cincinnati Bell ZoomTown
|
|
||||||
1.250.0.14,4,Verizon (Newark NJ)
|
|
||||||
1.252.0.14,4,Verizon (Reston VA)
|
|
||||||
1.252.0.12,4,Verizon (Reston VA)
|
|
||||||
8.238.120.14,4,Verizon (Atlanta GA)
|
|
||||||
8.238.120.12,4,Verizon (Atlanta GA)
|
|
||||||
78.143.192.10,4,Fast (UK)
|
|
||||||
201.220.136.186,4,CABLECOLOR S.A.
|
|
||||||
190.124.166.8,4,Redes y Telecomunicaciones
|
|
||||||
190.64.140.243,4,Administracion Nacional de Telecomunicaciones
|
|
||||||
200.40.230.36,4,Administracion Nacional de Telecomunicaciones
|
|
||||||
134.249.156.228,4,Kyivstar PJSC
|
|
||||||
195.60.70.5,4,VAK Ltd.
|
|
||||||
62.140.239.1,4,LLC TRC FIORD
|
|
||||||
94.158.153.236,4,Comfo LTD
|
|
||||||
109.254.13.190,4,Donbass Electronic Communications Ltd.
|
|
||||||
176.67.0.60,4,LLC McLaut-Invest
|
|
||||||
193.34.173.206,4,New Information Systems PP
|
|
||||||
94.153.241.134,4,Kyivstar PJSC
|
|
||||||
216.104.206.136,4,Africa Online Uganda
|
|
||||||
154.72.212.38,4,National Information Technology Authority Uganda
|
|
||||||
217.174.226.78,4,State Company of Electro Communications Turkmentelecom
|
|
||||||
46.45.187.181,4,Radore Veri Merkezi Hizmetleri A.S.
|
|
||||||
46.45.187.163,4,Radore Veri Merkezi Hizmetleri A.S.
|
|
||||||
46.45.187.184,4,Radore Veri Merkezi Hizmetleri A.S.
|
|
||||||
46.45.187.186,4,Radore Veri Merkezi Hizmetleri A.S.
|
|
||||||
46.20.159.27,4,DoraTelekom
|
|
||||||
93.113.60.84,4,Netinternet Bilisim Teknolojileri AS
|
|
||||||
93.113.60.82,4,Netinternet Bilisim Teknolojileri AS
|
|
||||||
196.3.132.153,4,Telecommunication Services of Trinidad and Tobago
|
|
||||||
196.3.132.154,4,Telecommunication Services of Trinidad and Tobago
|
|
||||||
80.248.72.1,4,CAFE Informatique et telecommunications
|
|
||||||
117.121.215.102,4,UIH
|
|
||||||
1.20.151.94,4,TOT Public Company Limited
|
|
||||||
117.121.215.101,4,UIH
|
|
||||||
122.154.230.146,4,The Communication Authoity of Thailand CAT
|
|
||||||
101.101.101.101,4,Taiwan Network Information Center
|
|
||||||
101.102.103.104,4,Taiwan Network Information Center
|
|
||||||
113.196.55.130,4,New Century InfoComm Tech Co. Ltd.
|
|
||||||
210.242.68.59,4,Data Communication Business Group
|
|
||||||
92.60.50.40,4,BONET Systems s.r.o.
|
|
||||||
217.73.17.110,4,VNET a.s.
|
|
||||||
84.245.69.193,4,SWAN a.s.
|
|
||||||
178.254.129.65,4,Drustvo za telekomunikacije Orion telekom doo Beograd-Zemun
|
|
||||||
93.86.63.54,4,TELEKOM SRBIJA a.d.
|
|
||||||
109.92.22.97,4,TELEKOM SRBIJA a.d.
|
|
||||||
197.159.166.81,4,Companhia Santomense de Telecomunicacoes
|
|
||||||
208.84.200.28,4,Columbus Communication St. Vincent and the Grenadines Ltd.
|
|
||||||
208.94.176.225,4,Karib Cable
|
|
||||||
199.116.57.181,4,IP SOLUTIONS INC.
|
|
||||||
74.120.24.129,4,IP SOLUTIONS INC.
|
|
||||||
74.120.24.81,4,IP SOLUTIONS INC.
|
|
||||||
74.120.24.97,4,IP SOLUTIONS INC.
|
|
||||||
78.130.38.222,4,NOS COMUNICACOES S.A.
|
|
||||||
195.22.25.130,4,Claranet Limited
|
|
||||||
213.241.88.98,4,Netia SA
|
|
||||||
87.204.28.12,4,Netia SA
|
|
||||||
212.160.85.232,4,Orange Polska Spolka Akcyjna
|
|
||||||
45.173.200.10,4,INVERSIONES EN TELECOMUNICACIONES DIGITALES S.A.C
|
|
||||||
200.10.231.110,4,Centro Nacional de Computacion
|
|
||||||
201.222.48.206,4,TECNOLOGIA EN ELECTRONICA E INFORMATICA SOCIEDAD ANONIMA (T.E.I.S.A)
|
|
||||||
201.222.50.30,4,TECNOLOGIA EN ELECTRONICA E INFORMATICA SOCIEDAD ANONIMA (T.E.I.S.A)
|
|
||||||
201.222.55.246,4,TECNOLOGIA EN ELECTRONICA E INFORMATICA SOCIEDAD ANONIMA (T.E.I.S.A)
|
|
||||||
103.209.52.250,4,The Professional Communications Pvt Ltd
|
|
||||||
119.63.132.26,4,Transworld Associates (Pvt.) Ltd.
|
|
||||||
83.143.8.249,4,MTN NIGERIA Communication limited
|
|
||||||
165.98.138.194,4,Amnet Telecomunicaciones S.A.
|
|
||||||
203.81.75.37,4,Myanma Posts and Telecommunications
|
|
||||||
65.18.114.254,4,Telecom International Myanmar Co. Ltd
|
|
||||||
65.18.117.174,4,Telecom International Myanmar Co. Ltd
|
|
||||||
69.160.0.86,4,OOREDOO MYANMAR
|
|
||||||
103.154.241.252,4,Myanmar Link Telecommunication Ltd
|
|
||||||
66.181.167.232,4,The first E-commerce and TriplePlay Service ISP in Mongolia.
|
|
||||||
202.21.99.42,4,Mobinet LLC. AS Mobinet Internet Service Provider
|
|
||||||
202.131.241.74,4,Mobinet LLC. AS Mobinet Internet Service Provider
|
|
||||||
124.158.93.2,4,STXCitinet. Leading Internet & VOIP Service Provider. Ulaanbaatar. Mongolia
|
|
||||||
77.89.232.86,4,ORANGE MOLDOVA S.A.
|
|
||||||
200.56.224.11,4,Marcatel Com S.A. de C.V.
|
|
||||||
201.144.135.211,4,Marcatel Com S.A. de C.V.
|
|
||||||
201.134.115.36,4,Uninet S.A. de C.V.
|
|
||||||
1.32.58.251,4,TM Net. Internet Service Provider
|
|
||||||
202.86.149.20,4,Companhia de Telecomunicacoes de Macau SARL
|
|
||||||
202.175.45.2,4,Companhia de Telecomunicacoes de Macau SARL
|
|
||||||
202.86.191.50,4,Companhia de Telecomunicacoes de Macau SARL
|
|
||||||
80.250.60.241,4,Sia Nano IT
|
|
||||||
101.78.12.50,4,Enterprise of Telecommunications Lao
|
|
||||||
168.187.161.250,4,Zajil International Telecom Company W.L.L.
|
|
||||||
62.12.114.25,4,Angani Limited
|
|
||||||
103.134.220.158,4,PT Global Media Pratama Solusindo
|
|
||||||
121.100.28.163,4,PT. Sakti Putra Mandiri
|
|
||||||
103.28.114.57,4,Lintas Data Prima PT
|
|
||||||
202.51.111.149,4,PT iForte Global Internet
|
|
||||||
103.23.20.23,4,PT Infinys System Indonesia
|
|
||||||
103.98.176.24,4,Universitas PGRI Semarang
|
|
||||||
103.98.176.143,4,Universitas PGRI Semarang
|
|
||||||
119.235.48.3,4,VAINAVI INDUSTIES LTD. INTERNET SERVICE PROVIDER. INDIA
|
|
||||||
103.194.120.33,4,GOLDY DIGINET PRIVATE LIMITED
|
|
||||||
103.194.120.3,4,GOLDY DIGINET PRIVATE LIMITED
|
|
||||||
103.194.120.129,4,GOLDY DIGINET PRIVATE LIMITED
|
|
||||||
223.31.121.171,4,Power Grid Corporation of India Limited
|
|
||||||
169.38.73.5,4,SoftLayer Technologies Inc.
|
|
||||||
202.86.251.201,4,Artech Infosystems Pvt Ltd
|
|
||||||
202.164.44.246,4,Quadrant Televentures Limited
|
|
||||||
87.229.99.1,4,SigmaNet Szolgaltato es Kereskedelm Ltd
|
|
||||||
91.137.135.75,4,OPC Networks Kft.
|
|
||||||
85.90.160.69,4,OPC Networks Kft.
|
|
||||||
195.228.219.226,4,Magyar Telekom plc.
|
|
||||||
103.6.168.114,4,NewMedia Express Pte Ltd
|
|
||||||
45.71.185.100,4,NEDETEL S.A.
|
|
||||||
186.149.243.52,4,ALTICE DOMINICANA S.A.
|
|
||||||
193.17.47.1,4,CZ.NIC z.s.p.o.
|
|
||||||
195.113.144.194,4,CZ.NIC z.s.p.o.
|
|
||||||
213.211.50.1,4,itself s.r.o.
|
|
||||||
213.211.50.2,4,itself s.r.o.
|
|
||||||
185.43.135.1,4,CZ.NIC z.s.p.o.
|
|
||||||
90.183.151.107,4,O2 Czech Republic. a.s.
|
|
||||||
27.50.20.21,4,PT. Mora Telematika Indonesia
|
|
||||||
116.193.190.164,4,PT Cloud Hosting Indonesia
|
|
||||||
103.56.148.179,4,PT Beon Intermedia
|
|
||||||
8.215.26.147,4,Alibaba.com Singapore E-Commerce Private Limited
|
|
||||||
|
+307
@@ -0,0 +1,307 @@
|
|||||||
|
host,country,protocols,irtt_port,irtt_hmac
|
||||||
|
94.140.14.14,,icmp-echo|icmp-timestamps,,
|
||||||
|
94.140.15.15,,icmp-echo|icmp-timestamps,,
|
||||||
|
94.140.14.15,,icmp-echo|icmp-timestamps,,
|
||||||
|
94.140.15.16,,icmp-echo|icmp-timestamps,,
|
||||||
|
94.140.14.140,,icmp-echo|icmp-timestamps,,
|
||||||
|
94.140.14.141,,icmp-echo|icmp-timestamps,,
|
||||||
|
205.171.3.65,,icmp-echo|icmp-timestamps,,
|
||||||
|
205.171.2.65,,icmp-echo|icmp-timestamps,,
|
||||||
|
185.228.168.9,,icmp-echo|icmp-timestamps,,
|
||||||
|
185.228.169.9,,icmp-echo|icmp-timestamps,,
|
||||||
|
185.228.168.168,,icmp-echo|icmp-timestamps,,
|
||||||
|
185.228.169.168,,icmp-echo|icmp-timestamps,,
|
||||||
|
185.228.168.10,,icmp-echo|icmp-timestamps,,
|
||||||
|
185.228.169.11,,icmp-echo|icmp-timestamps,,
|
||||||
|
8.26.56.26,,icmp-echo|icmp-timestamps,,
|
||||||
|
8.20.247.20,,icmp-echo|icmp-timestamps,,
|
||||||
|
84.200.69.80,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
84.200.70.40,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
46.227.200.54,GB,icmp-echo|icmp-timestamps,,
|
||||||
|
46.227.200.55,,icmp-echo|icmp-timestamps,,
|
||||||
|
45.33.97.5,US,icmp-echo|icmp-timestamps,,
|
||||||
|
37.235.1.177,AT,icmp-echo|icmp-timestamps,,
|
||||||
|
74.82.42.42,,icmp-echo|icmp-timestamps,,
|
||||||
|
208.67.222.222,,icmp-echo|icmp-timestamps,,
|
||||||
|
208.67.220.220,,icmp-echo|icmp-timestamps,,
|
||||||
|
208.67.222.123,,icmp-echo|icmp-timestamps,,
|
||||||
|
208.67.220.123,,icmp-echo|icmp-timestamps,,
|
||||||
|
208.67.222.2,,icmp-echo|icmp-timestamps,,
|
||||||
|
208.67.220.2,,icmp-echo|icmp-timestamps,,
|
||||||
|
159.89.120.99,CA,icmp-echo|icmp-timestamps,,
|
||||||
|
134.195.4.2,,icmp-echo|icmp-timestamps,,
|
||||||
|
9.9.9.9,,icmp-echo|icmp-timestamps,,
|
||||||
|
9.9.9.10,,icmp-echo|icmp-timestamps,,
|
||||||
|
9.9.9.11,,icmp-echo|icmp-timestamps,,
|
||||||
|
149.112.112.10,,icmp-echo|icmp-timestamps,,
|
||||||
|
149.112.112.11,,icmp-echo|icmp-timestamps,,
|
||||||
|
149.112.112.112,,icmp-echo|icmp-timestamps,,
|
||||||
|
176.9.93.198,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
176.9.1.117,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
104.236.210.29,US,icmp-echo|icmp-timestamps,,
|
||||||
|
37.235.1.174,AT,icmp-echo|icmp-timestamps,,
|
||||||
|
81.218.119.1,IL,icmp-echo|icmp-timestamps,,
|
||||||
|
194.8.57.12,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
51.89.30.51,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
51.89.93.25,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
130.59.31.248,CH,icmp-echo|icmp-timestamps,,
|
||||||
|
130.59.31.251,CH,icmp-echo|icmp-timestamps,,
|
||||||
|
185.49.141.37,NL,icmp-echo|icmp-timestamps,,
|
||||||
|
158.64.1.29,LU,icmp-echo|icmp-timestamps,,
|
||||||
|
89.234.186.112,FR,icmp-echo|icmp-timestamps,,
|
||||||
|
185.95.218.42,CH,icmp-echo|icmp-timestamps,,
|
||||||
|
185.95.218.43,CH,icmp-echo|icmp-timestamps,,
|
||||||
|
140.238.215.192,CH,icmp-echo|icmp-timestamps,,
|
||||||
|
199.58.81.218,CA,icmp-echo|icmp-timestamps,,
|
||||||
|
146.255.56.98,AT,icmp-echo|icmp-timestamps,,
|
||||||
|
185.222.222.222,,icmp-echo|icmp-timestamps,,
|
||||||
|
185.184.222.222,NL,icmp-echo|icmp-timestamps,,
|
||||||
|
5.1.66.255,AT,icmp-echo|icmp-timestamps,,
|
||||||
|
80.80.80.80,,icmp-echo|icmp-timestamps,,
|
||||||
|
80.80.81.81,,icmp-echo|icmp-timestamps,,
|
||||||
|
80.67.169.12,FR,icmp-echo|icmp-timestamps,,
|
||||||
|
80.67.169.40,FR,icmp-echo|icmp-timestamps,,
|
||||||
|
217.24.255.134,AL,icmp-echo|icmp-timestamps,,
|
||||||
|
213.163.127.229,XK,icmp-echo|icmp-timestamps,,
|
||||||
|
85.94.178.198,AD,icmp-echo|icmp-timestamps,,
|
||||||
|
186.148.128.86,AR,icmp-echo|icmp-timestamps,,
|
||||||
|
190.151.144.21,AR,icmp-echo|icmp-timestamps,,
|
||||||
|
200.45.184.43,AR,icmp-echo|icmp-timestamps,,
|
||||||
|
179.60.232.10,AR,icmp-echo|icmp-timestamps,,
|
||||||
|
170.210.83.110,AR,icmp-echo|icmp-timestamps,,
|
||||||
|
190.57.234.194,AR,icmp-echo|icmp-timestamps,,
|
||||||
|
186.153.180.148,AR,icmp-echo|icmp-timestamps,,
|
||||||
|
81.16.8.110,AM,icmp-echo|icmp-timestamps,,
|
||||||
|
45.133.105.123,AM,icmp-echo|icmp-timestamps,,
|
||||||
|
61.8.0.113,AU,icmp-echo|icmp-timestamps,,
|
||||||
|
103.224.162.40,AU,icmp-echo|icmp-timestamps,,
|
||||||
|
139.130.4.4,,icmp-echo|icmp-timestamps,,
|
||||||
|
139.134.5.51,AU,icmp-echo|icmp-timestamps,,
|
||||||
|
139.134.2.190,AU,icmp-echo|icmp-timestamps,,
|
||||||
|
203.50.2.71,,icmp-echo|icmp-timestamps,,
|
||||||
|
83.137.41.8,AT,icmp-echo|icmp-timestamps,,
|
||||||
|
83.137.41.9,AT,icmp-echo|icmp-timestamps,,
|
||||||
|
194.36.144.87,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
188.118.227.21,AT,icmp-echo|icmp-timestamps,,
|
||||||
|
83.218.176.140,AT,icmp-echo|icmp-timestamps,,
|
||||||
|
85.132.85.85,AZ,icmp-echo|icmp-timestamps,,
|
||||||
|
45.250.253.222,US,icmp-echo|icmp-timestamps,,
|
||||||
|
103.77.188.18,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
103.77.188.19,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
103.80.1.2,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
103.153.154.2,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
114.130.5.5,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
114.130.5.6,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
103.146.221.20,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
103.146.221.21,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
115.127.19.129,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
115.127.19.137,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
103.113.200.10,BD,icmp-echo|icmp-timestamps,,
|
||||||
|
177.131.114.86,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
177.66.203.10,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
177.184.131.180,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
186.194.224.82,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
186.225.194.29,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
189.42.239.34,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
200.99.138.94,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
200.99.138.103,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
200.167.191.114,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
200.169.8.1,BR,icmp-echo|icmp-timestamps,,
|
||||||
|
141.0.102.202,NO,icmp-echo|icmp-timestamps,,
|
||||||
|
62.97.223.62,NO,icmp-echo|icmp-timestamps,,
|
||||||
|
217.18.206.12,NO,icmp-echo|icmp-timestamps,,
|
||||||
|
217.18.206.22,NO,icmp-echo|icmp-timestamps,,
|
||||||
|
195.204.130.85,NO,icmp-echo|icmp-timestamps,,
|
||||||
|
81.175.38.1,NO,icmp-echo|icmp-timestamps,,
|
||||||
|
46.182.19.48,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
82.96.64.2,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
94.247.43.254,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
195.10.195.195,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
194.172.160.4,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
185.233.106.232,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
185.233.107.4,DE,icmp-echo|icmp-timestamps,,
|
||||||
|
141.1.1.1,,icmp-echo|icmp-timestamps,,
|
||||||
|
141.1.27.249,,icmp-echo|icmp-timestamps,,
|
||||||
|
176.103.130.130,,icmp-echo|icmp-timestamps,,
|
||||||
|
176.103.130.131,,icmp-echo|icmp-timestamps,,
|
||||||
|
80.243.64.67,RU,icmp-echo|icmp-timestamps,,
|
||||||
|
195.218.133.88,RU,icmp-echo|icmp-timestamps,,
|
||||||
|
83.69.120.1,RU,icmp-echo|icmp-timestamps,,
|
||||||
|
46.16.229.223,RU,icmp-echo|icmp-timestamps,,
|
||||||
|
193.183.98.66,IT,icmp-echo|icmp-timestamps,,
|
||||||
|
212.19.96.2,IT,icmp-echo|icmp-timestamps,,
|
||||||
|
217.12.181.97,IT,icmp-echo|icmp-timestamps,,
|
||||||
|
62.149.128.4,IT,icmp-echo|icmp-timestamps,,
|
||||||
|
62.149.132.2,IT,icmp-echo|icmp-timestamps,,
|
||||||
|
85.47.198.82,IT,icmp-echo|icmp-timestamps,,
|
||||||
|
202.136.162.11,SG,icmp-echo|icmp-timestamps,,
|
||||||
|
51.79.144.245,SG,icmp-echo|icmp-timestamps,,
|
||||||
|
61.46.244.211,JP,icmp-echo|icmp-timestamps,,
|
||||||
|
1.33.184.173,JP,icmp-echo|icmp-timestamps,,
|
||||||
|
1.33.184.116,JP,icmp-echo|icmp-timestamps,,
|
||||||
|
1.33.184.174,JP,icmp-echo|icmp-timestamps,,
|
||||||
|
1.33.184.108,JP,icmp-echo|icmp-timestamps,,
|
||||||
|
1.33.184.220,JP,icmp-echo|icmp-timestamps,,
|
||||||
|
1.33.184.230,JP,icmp-echo|icmp-timestamps,,
|
||||||
|
1.33.184.158,JP,icmp-echo|icmp-timestamps,,
|
||||||
|
62.212.113.125,FR,icmp-echo|icmp-timestamps,,
|
||||||
|
185.30.211.154,FR,icmp-echo|icmp-timestamps,,
|
||||||
|
87.240.90.128,FR,icmp-echo|icmp-timestamps,,
|
||||||
|
45.77.61.165,FR,icmp-echo|icmp-timestamps,,
|
||||||
|
178.62.57.141,GB,icmp-echo|icmp-timestamps,,
|
||||||
|
194.145.240.6,GB,icmp-echo|icmp-timestamps,,
|
||||||
|
195.27.1.1,,icmp-echo|icmp-timestamps,,
|
||||||
|
81.17.66.13,GB,icmp-echo|icmp-timestamps,,
|
||||||
|
87.117.196.200,GB,icmp-echo|icmp-timestamps,,
|
||||||
|
213.123.251.87,GB,icmp-echo|icmp-timestamps,,
|
||||||
|
81.149.21.254,GB,icmp-echo|icmp-timestamps,,
|
||||||
|
196.21.247.1,ZA,icmp-echo|icmp-timestamps,,
|
||||||
|
89.140.69.49,ES,icmp-echo|icmp-timestamps,,
|
||||||
|
62.97.123.107,ES,icmp-echo|icmp-timestamps,,
|
||||||
|
194.177.210.210,GR,icmp-echo|icmp-timestamps,,
|
||||||
|
168.126.63.1,KR,icmp-echo|icmp-timestamps,,
|
||||||
|
121.254.134.99,KR,icmp-echo|icmp-timestamps,,
|
||||||
|
203.239.131.1,KR,icmp-echo|icmp-timestamps,,
|
||||||
|
202.57.32.49,PH,icmp-echo|icmp-timestamps,,
|
||||||
|
81.17.31.34,CH,icmp-echo|icmp-timestamps,,
|
||||||
|
79.141.82.250,CH,icmp-echo|icmp-timestamps,,
|
||||||
|
80.254.77.39,CH,icmp-echo|icmp-timestamps,,
|
||||||
|
85.93.212.2,LU,icmp-echo|icmp-timestamps,,
|
||||||
|
196.223.158.58,SD,icmp-echo|icmp-timestamps,,
|
||||||
|
89.150.195.195,HK,icmp-echo|icmp-timestamps,,
|
||||||
|
130.244.126.100,SE,icmp-echo|icmp-timestamps,,
|
||||||
|
87.96.144.179,SE,icmp-echo|icmp-timestamps,,
|
||||||
|
146.247.80.144,SE,icmp-echo|icmp-timestamps,,
|
||||||
|
46.246.29.69,SE,icmp-echo|icmp-timestamps,,
|
||||||
|
203.210.237.165,VN,icmp-echo|icmp-timestamps,,
|
||||||
|
113.190.253.229,VN,icmp-echo|icmp-timestamps,,
|
||||||
|
119.29.29.29,,icmp-echo|icmp-timestamps,,
|
||||||
|
209.193.4.7,US,icmp-echo|icmp-timestamps,,
|
||||||
|
209.112.160.2,US,icmp-echo|icmp-timestamps,,
|
||||||
|
216.68.4.10,US,icmp-echo|icmp-timestamps,,
|
||||||
|
216.68.5.10,US,icmp-echo|icmp-timestamps,,
|
||||||
|
1.250.0.14,KR,icmp-echo|icmp-timestamps,,
|
||||||
|
1.252.0.14,KR,icmp-echo|icmp-timestamps,,
|
||||||
|
1.252.0.12,KR,icmp-echo|icmp-timestamps,,
|
||||||
|
8.238.120.14,US,icmp-echo|icmp-timestamps,,
|
||||||
|
8.238.120.12,US,icmp-echo|icmp-timestamps,,
|
||||||
|
78.143.192.10,GB,icmp-echo|icmp-timestamps,,
|
||||||
|
201.220.136.186,HN,icmp-echo|icmp-timestamps,,
|
||||||
|
190.124.166.8,HN,icmp-echo|icmp-timestamps,,
|
||||||
|
190.64.140.243,UY,icmp-echo|icmp-timestamps,,
|
||||||
|
200.40.230.36,UY,icmp-echo|icmp-timestamps,,
|
||||||
|
134.249.156.228,UA,icmp-echo|icmp-timestamps,,
|
||||||
|
195.60.70.5,UA,icmp-echo|icmp-timestamps,,
|
||||||
|
62.140.239.1,RU,icmp-echo|icmp-timestamps,,
|
||||||
|
94.158.153.236,UA,icmp-echo|icmp-timestamps,,
|
||||||
|
109.254.13.190,UA,icmp-echo|icmp-timestamps,,
|
||||||
|
176.67.0.60,UA,icmp-echo|icmp-timestamps,,
|
||||||
|
193.34.173.206,UA,icmp-echo|icmp-timestamps,,
|
||||||
|
94.153.241.134,UA,icmp-echo|icmp-timestamps,,
|
||||||
|
216.104.206.136,UG,icmp-echo|icmp-timestamps,,
|
||||||
|
154.72.212.38,UG,icmp-echo|icmp-timestamps,,
|
||||||
|
217.174.226.78,TM,icmp-echo|icmp-timestamps,,
|
||||||
|
46.45.187.181,TR,icmp-echo|icmp-timestamps,,
|
||||||
|
46.45.187.163,TR,icmp-echo|icmp-timestamps,,
|
||||||
|
46.45.187.184,TR,icmp-echo|icmp-timestamps,,
|
||||||
|
46.45.187.186,TR,icmp-echo|icmp-timestamps,,
|
||||||
|
46.20.159.27,TR,icmp-echo|icmp-timestamps,,
|
||||||
|
93.113.60.84,TR,icmp-echo|icmp-timestamps,,
|
||||||
|
93.113.60.82,TR,icmp-echo|icmp-timestamps,,
|
||||||
|
196.3.132.153,TT,icmp-echo|icmp-timestamps,,
|
||||||
|
196.3.132.154,TT,icmp-echo|icmp-timestamps,,
|
||||||
|
80.248.72.1,TG,icmp-echo|icmp-timestamps,,
|
||||||
|
117.121.215.102,TH,icmp-echo|icmp-timestamps,,
|
||||||
|
1.20.151.94,TH,icmp-echo|icmp-timestamps,,
|
||||||
|
117.121.215.101,TH,icmp-echo|icmp-timestamps,,
|
||||||
|
122.154.230.146,TH,icmp-echo|icmp-timestamps,,
|
||||||
|
101.101.101.101,TW,icmp-echo|icmp-timestamps,,
|
||||||
|
101.102.103.104,TW,icmp-echo|icmp-timestamps,,
|
||||||
|
113.196.55.130,TW,icmp-echo|icmp-timestamps,,
|
||||||
|
210.242.68.59,TW,icmp-echo|icmp-timestamps,,
|
||||||
|
92.60.50.40,SK,icmp-echo|icmp-timestamps,,
|
||||||
|
217.73.17.110,SK,icmp-echo|icmp-timestamps,,
|
||||||
|
84.245.69.193,SK,icmp-echo|icmp-timestamps,,
|
||||||
|
178.254.129.65,RS,icmp-echo|icmp-timestamps,,
|
||||||
|
93.86.63.54,RS,icmp-echo|icmp-timestamps,,
|
||||||
|
109.92.22.97,RS,icmp-echo|icmp-timestamps,,
|
||||||
|
197.159.166.81,ST,icmp-echo|icmp-timestamps,,
|
||||||
|
208.84.200.28,VC,icmp-echo|icmp-timestamps,,
|
||||||
|
208.94.176.225,LC,icmp-echo|icmp-timestamps,,
|
||||||
|
199.116.57.181,PR,icmp-echo|icmp-timestamps,,
|
||||||
|
74.120.24.129,PR,icmp-echo|icmp-timestamps,,
|
||||||
|
74.120.24.81,PR,icmp-echo|icmp-timestamps,,
|
||||||
|
74.120.24.97,PR,icmp-echo|icmp-timestamps,,
|
||||||
|
78.130.38.222,PT,icmp-echo|icmp-timestamps,,
|
||||||
|
195.22.25.130,PT,icmp-echo|icmp-timestamps,,
|
||||||
|
213.241.88.98,PL,icmp-echo|icmp-timestamps,,
|
||||||
|
87.204.28.12,PL,icmp-echo|icmp-timestamps,,
|
||||||
|
212.160.85.232,PL,icmp-echo|icmp-timestamps,,
|
||||||
|
45.173.200.10,PE,icmp-echo|icmp-timestamps,,
|
||||||
|
200.10.231.110,PY,icmp-echo|icmp-timestamps,,
|
||||||
|
201.222.48.206,PY,icmp-echo|icmp-timestamps,,
|
||||||
|
201.222.50.30,PY,icmp-echo|icmp-timestamps,,
|
||||||
|
201.222.55.246,PY,icmp-echo|icmp-timestamps,,
|
||||||
|
103.209.52.250,PK,icmp-echo|icmp-timestamps,,
|
||||||
|
119.63.132.26,PK,icmp-echo|icmp-timestamps,,
|
||||||
|
83.143.8.249,NG,icmp-echo|icmp-timestamps,,
|
||||||
|
165.98.138.194,NI,icmp-echo|icmp-timestamps,,
|
||||||
|
203.81.75.37,MM,icmp-echo|icmp-timestamps,,
|
||||||
|
65.18.114.254,MM,icmp-echo|icmp-timestamps,,
|
||||||
|
65.18.117.174,MM,icmp-echo|icmp-timestamps,,
|
||||||
|
69.160.0.86,MM,icmp-echo|icmp-timestamps,,
|
||||||
|
103.154.241.252,MM,icmp-echo|icmp-timestamps,,
|
||||||
|
66.181.167.232,MN,icmp-echo|icmp-timestamps,,
|
||||||
|
202.21.99.42,MN,icmp-echo|icmp-timestamps,,
|
||||||
|
202.131.241.74,MN,icmp-echo|icmp-timestamps,,
|
||||||
|
124.158.93.2,SG,icmp-echo|icmp-timestamps,,
|
||||||
|
77.89.232.86,MD,icmp-echo|icmp-timestamps,,
|
||||||
|
200.56.224.11,MX,icmp-echo|icmp-timestamps,,
|
||||||
|
201.144.135.211,MX,icmp-echo|icmp-timestamps,,
|
||||||
|
201.134.115.36,MX,icmp-echo|icmp-timestamps,,
|
||||||
|
1.32.58.251,MY,icmp-echo|icmp-timestamps,,
|
||||||
|
202.86.149.20,MO,icmp-echo|icmp-timestamps,,
|
||||||
|
202.175.45.2,MO,icmp-echo|icmp-timestamps,,
|
||||||
|
202.86.191.50,MO,icmp-echo|icmp-timestamps,,
|
||||||
|
80.250.60.241,LV,icmp-echo|icmp-timestamps,,
|
||||||
|
101.78.12.50,LA,icmp-echo|icmp-timestamps,,
|
||||||
|
168.187.161.250,KW,icmp-echo|icmp-timestamps,,
|
||||||
|
62.12.114.25,KE,icmp-echo|icmp-timestamps,,
|
||||||
|
103.134.220.158,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
121.100.28.163,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
103.28.114.57,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
202.51.111.149,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
103.23.20.23,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
103.98.176.24,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
103.98.176.143,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
119.235.48.3,IN,icmp-echo|icmp-timestamps,,
|
||||||
|
103.194.120.33,IN,icmp-echo|icmp-timestamps,,
|
||||||
|
103.194.120.3,IN,icmp-echo|icmp-timestamps,,
|
||||||
|
103.194.120.129,IN,icmp-echo|icmp-timestamps,,
|
||||||
|
223.31.121.171,IN,icmp-echo|icmp-timestamps,,
|
||||||
|
169.38.73.5,IN,icmp-echo|icmp-timestamps,,
|
||||||
|
202.86.251.201,IN,icmp-echo|icmp-timestamps,,
|
||||||
|
202.164.44.246,IN,icmp-echo|icmp-timestamps,,
|
||||||
|
87.229.99.1,HU,icmp-echo|icmp-timestamps,,
|
||||||
|
91.137.135.75,HU,icmp-echo|icmp-timestamps,,
|
||||||
|
85.90.160.69,HU,icmp-echo|icmp-timestamps,,
|
||||||
|
195.228.219.226,HU,icmp-echo|icmp-timestamps,,
|
||||||
|
103.6.168.114,SG,icmp-echo|icmp-timestamps,,
|
||||||
|
45.71.185.100,EC,icmp-echo|icmp-timestamps,,
|
||||||
|
186.149.243.52,DO,icmp-echo|icmp-timestamps,,
|
||||||
|
193.17.47.1,CZ,icmp-echo|icmp-timestamps,,
|
||||||
|
195.113.144.194,CZ,icmp-echo|icmp-timestamps,,
|
||||||
|
213.211.50.1,CZ,icmp-echo|icmp-timestamps,,
|
||||||
|
213.211.50.2,CZ,icmp-echo|icmp-timestamps,,
|
||||||
|
185.43.135.1,CZ,icmp-echo|icmp-timestamps,,
|
||||||
|
90.183.151.107,CZ,icmp-echo|icmp-timestamps,,
|
||||||
|
27.50.20.21,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
116.193.190.164,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
103.56.148.179,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
8.215.26.147,ID,icmp-echo|icmp-timestamps,,
|
||||||
|
global.irtt.lochnair.net,,irtt,2112,
|
||||||
|
uk-cov1.irtt.lochnair.net,GB,irtt,2112,
|
||||||
|
uk-cov2.irtt.lochnair.net,GB,irtt,2112,
|
||||||
|
de-fra.irtt.lochnair.net,DE,irtt,2112,
|
||||||
|
nl-ams1.irtt.lochnair.net,NL,irtt,2112,
|
||||||
|
netperf-eu.bufferbloat.net,SE,irtt,2112,
|
||||||
|
@@ -2,5 +2,18 @@
|
|||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:recommended"
|
"config:recommended"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "Keep netlink-bindings and netlink-socket2 updates together",
|
||||||
|
"matchManagers": [
|
||||||
|
"cargo"
|
||||||
|
],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"netlink-bindings",
|
||||||
|
"netlink-socket2"
|
||||||
|
],
|
||||||
|
"groupName": "netlink dependencies"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+434
-108
@@ -1,159 +1,485 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
use crate::Config;
|
use crate::measurement::model::{MeasurementEvent, MeasurementObservation, MeasurementStream};
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::pinger::PingReply;
|
use crate::settings::Settings;
|
||||||
use crate::util::MutexExt;
|
use flume::{Receiver, Sender};
|
||||||
use log::info;
|
use log::{debug, info};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::{Receiver, Sender};
|
use std::time::Duration;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
const STALE_AFTER: Duration = Duration::from_secs(30);
|
||||||
pub struct ReflectorStats {
|
const ANOMALY_THRESHOLD_MS: f64 = 5_000.0;
|
||||||
pub down_ewma: f64,
|
const BASELINE_HALF_LIFE_SECS: f64 = 135.0;
|
||||||
pub up_ewma: f64,
|
const RECENT_HALF_LIFE_SECS: f64 = 0.4;
|
||||||
pub last_receive_time_s: Instant,
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct EwmaStats {
|
||||||
|
pub down: f64,
|
||||||
|
pub up: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EwmaStats {
|
||||||
|
fn from_reply(reply: &MeasurementObservation) -> Self {
|
||||||
|
Self {
|
||||||
|
down: reply
|
||||||
|
.one_way
|
||||||
|
.map(|o| o.downlink.as_nanos() as f64 / 1000000.0)
|
||||||
|
.unwrap_or(reply.rtt_ms() / 2.0),
|
||||||
|
up: reply
|
||||||
|
.one_way
|
||||||
|
.map(|o| o.uplink.as_nanos() as f64 / 1000000.0)
|
||||||
|
.unwrap_or(reply.rtt_ms() / 2.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<MeasurementStream, ReflectorState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Baseliner {
|
pub struct Baseliner {
|
||||||
pub config: Config,
|
pub settings: Settings,
|
||||||
pub owd_baseline: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
|
||||||
pub owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
|
||||||
pub reselect_trigger: Sender<bool>,
|
pub reselect_trigger: Sender<bool>,
|
||||||
pub start_time: Instant,
|
pub start_time: Instant,
|
||||||
pub stats_rx: Receiver<PingReply>,
|
pub stats_rx: Receiver<MeasurementEvent>,
|
||||||
|
pub control_tx: Sender<ControlSnapshot>,
|
||||||
|
pub selection_tx: Option<Sender<ControlSnapshot>>,
|
||||||
pub baseline_metrics: MetricsSender,
|
pub baseline_metrics: MetricsSender,
|
||||||
pub event_metrics: MetricsSender,
|
pub event_metrics: MetricsSender,
|
||||||
|
pub ping_metrics: MetricsSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ewma_factor(tick: f64, dur: f64) -> f64 {
|
fn ewma_factor(tick: f64, dur: f64) -> f64 {
|
||||||
((0.5_f64).ln() / (dur / tick)).exp()
|
((0.5_f64).ln() / (dur / tick)).exp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_reflector_state(
|
||||||
|
state: &mut ReflectorState,
|
||||||
|
sample: EwmaStats,
|
||||||
|
received_at: Instant,
|
||||||
|
start_time: Instant,
|
||||||
|
slow_factor: f64,
|
||||||
|
fast_factor: f64,
|
||||||
|
) -> bool {
|
||||||
|
if received_at.saturating_duration_since(state.last_receive_at) > STALE_AFTER {
|
||||||
|
state.reset(sample, received_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample.up > state.baseline.up + ANOMALY_THRESHOLD_MS
|
||||||
|
|| sample.down > state.baseline.down + ANOMALY_THRESHOLD_MS
|
||||||
|
{
|
||||||
|
state.last_receive_at = start_time;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.baseline.update(sample, slow_factor);
|
||||||
|
state.recent.update(sample, fast_factor);
|
||||||
|
|
||||||
|
state.baseline.down = state.baseline.down.min(state.recent.down);
|
||||||
|
state.baseline.up = state.baseline.up.min(state.recent.up);
|
||||||
|
state.last_receive_at = received_at;
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_reply(
|
||||||
|
reflectors: &mut HashMap<MeasurementStream, ReflectorState>,
|
||||||
|
reply: &MeasurementObservation,
|
||||||
|
start_time: Instant,
|
||||||
|
slow_factor: f64,
|
||||||
|
fast_factor: f64,
|
||||||
|
) -> (ReflectorState, bool) {
|
||||||
|
let sample = EwmaStats::from_reply(reply);
|
||||||
|
let state = reflectors
|
||||||
|
.entry(reply.stream)
|
||||||
|
.or_insert_with(|| ReflectorState::new(sample, reply.observed_at));
|
||||||
|
let anomaly = update_reflector_state(
|
||||||
|
state,
|
||||||
|
sample,
|
||||||
|
reply.observed_at,
|
||||||
|
start_time,
|
||||||
|
slow_factor,
|
||||||
|
fast_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
(*state, anomaly)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_snapshot(
|
||||||
|
reflectors: &HashMap<MeasurementStream, ReflectorState>,
|
||||||
|
generated_at: Instant,
|
||||||
|
) -> ControlSnapshot {
|
||||||
|
ControlSnapshot {
|
||||||
|
generated_at,
|
||||||
|
reflectors: reflectors.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Baseliner {
|
impl Baseliner {
|
||||||
pub fn run(&self) -> anyhow::Result<()> {
|
pub fn run(&self) -> anyhow::Result<()> {
|
||||||
/*
|
let mut reflectors = HashMap::<MeasurementStream, ReflectorState>::new();
|
||||||
* 135 seconds to decay to 50% for the slow factor and
|
|
||||||
* 0.4 seconds to decay to 50% for the fast factor.
|
let slow_factor = ewma_factor(
|
||||||
* The fast one can be adjusted to tune, try anything from 0.01 to 3.0 to get more or less sensitivity
|
self.settings.advanced_settings.tick_interval,
|
||||||
* with more sensitivity we respond faster to bloat, but are at risk from triggering due to lag spikes that
|
BASELINE_HALF_LIFE_SECS,
|
||||||
* aren't bloat related, with less sensitivity (bigger numbers) we smooth through quick spikes
|
);
|
||||||
* but take longer to respond to real bufferbloat
|
let fast_factor = ewma_factor(
|
||||||
*/
|
self.settings.advanced_settings.tick_interval,
|
||||||
let slow_factor = ewma_factor(self.config.tick_interval, 135.0);
|
RECENT_HALF_LIFE_SECS,
|
||||||
let fast_factor = ewma_factor(self.config.tick_interval, 0.4);
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||||
info!("Baseliner shutting down");
|
info!("Baseliner shutting down");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let time_data = self.stats_rx.recv()?;
|
|
||||||
|
|
||||||
let mut owd_baseline_map = self.owd_baseline.lock_anyhow()?;
|
let event = self.stats_rx.recv()?;
|
||||||
let mut owd_recent_map = self.owd_recent.lock_anyhow()?;
|
|
||||||
|
|
||||||
let owd_baseline_new = ReflectorStats {
|
let reply = match event {
|
||||||
down_ewma: time_data.down_time,
|
MeasurementEvent::Observation(reply) => reply,
|
||||||
up_ewma: time_data.up_time,
|
MeasurementEvent::Loss(_) => continue,
|
||||||
last_receive_time_s: time_data.last_receive_time_s,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let owd_recent_new = ReflectorStats {
|
let reflector = reply.stream.peer;
|
||||||
down_ewma: time_data.down_time,
|
let (state, anomaly) = process_reply(
|
||||||
up_ewma: time_data.up_time,
|
&mut reflectors,
|
||||||
last_receive_time_s: time_data.last_receive_time_s,
|
&reply,
|
||||||
};
|
self.start_time,
|
||||||
|
slow_factor,
|
||||||
|
fast_factor,
|
||||||
|
);
|
||||||
|
|
||||||
let owd_baseline = owd_baseline_map
|
if anomaly {
|
||||||
.entry(time_data.reflector)
|
|
||||||
.or_insert(owd_baseline_new);
|
|
||||||
|
|
||||||
let owd_recent = owd_recent_map
|
|
||||||
.entry(time_data.reflector)
|
|
||||||
.or_insert(owd_recent_new);
|
|
||||||
|
|
||||||
if time_data
|
|
||||||
.last_receive_time_s
|
|
||||||
.duration_since(owd_baseline.last_receive_time_s)
|
|
||||||
.as_secs_f64()
|
|
||||||
> 30.0
|
|
||||||
|| time_data
|
|
||||||
.last_receive_time_s
|
|
||||||
.duration_since(owd_recent.last_receive_time_s)
|
|
||||||
.as_secs_f64()
|
|
||||||
> 30.0
|
|
||||||
{
|
|
||||||
owd_baseline.down_ewma = time_data.down_time;
|
|
||||||
owd_baseline.up_ewma = time_data.up_time;
|
|
||||||
owd_baseline.last_receive_time_s = time_data.last_receive_time_s;
|
|
||||||
owd_recent.down_ewma = time_data.down_time;
|
|
||||||
owd_recent.up_ewma = time_data.up_time;
|
|
||||||
owd_recent.last_receive_time_s = time_data.last_receive_time_s;
|
|
||||||
}
|
|
||||||
|
|
||||||
owd_baseline.last_receive_time_s = time_data.last_receive_time_s;
|
|
||||||
owd_recent.last_receive_time_s = time_data.last_receive_time_s;
|
|
||||||
|
|
||||||
// if this reflection is more than 5 seconds higher than baseline... mark it no good and trigger a reselection
|
|
||||||
if time_data.up_time > owd_baseline.up_ewma + 5000.0
|
|
||||||
|| time_data.down_time > owd_baseline.down_ewma + 5000.0
|
|
||||||
{
|
|
||||||
// mark the data as bad by setting the receive time to the time autorate was started
|
|
||||||
owd_baseline.last_receive_time_s = self.start_time;
|
|
||||||
owd_recent.last_receive_time_s = self.start_time;
|
|
||||||
info!(
|
info!(
|
||||||
"Reflector {} has OWD > 5 seconds more than baseline, triggering reselection",
|
"Reflector {} has OWD > 5 seconds more than baseline, triggering reselection",
|
||||||
time_data.reflector
|
reflector
|
||||||
);
|
);
|
||||||
self.event_metrics.send(Metric::Event {
|
self.event_metrics.send(Metric::Event {
|
||||||
name: "reselection",
|
name: "reselection",
|
||||||
reason: "anomaly",
|
reason: "anomaly",
|
||||||
reflector: Some(time_data.reflector),
|
reflector: Some(reflector),
|
||||||
tags: &[],
|
tags: &[],
|
||||||
});
|
});
|
||||||
// If reselection is disabled this would trigger an error
|
let _ = self.reselect_trigger.try_send(true);
|
||||||
// so just ignore the result
|
|
||||||
let _ = self.reselect_trigger.send(true);
|
|
||||||
} else {
|
|
||||||
owd_baseline.down_ewma = owd_baseline.down_ewma * slow_factor
|
|
||||||
+ (1.0 - slow_factor) * time_data.down_time;
|
|
||||||
owd_baseline.up_ewma =
|
|
||||||
owd_baseline.up_ewma * slow_factor + (1.0 - slow_factor) * time_data.up_time;
|
|
||||||
|
|
||||||
owd_recent.down_ewma =
|
|
||||||
owd_recent.down_ewma * fast_factor + (1.0 - fast_factor) * time_data.down_time;
|
|
||||||
owd_recent.up_ewma =
|
|
||||||
owd_recent.up_ewma * fast_factor + (1.0 - fast_factor) * time_data.up_time;
|
|
||||||
|
|
||||||
if owd_baseline.down_ewma > owd_recent.down_ewma {
|
|
||||||
owd_baseline.down_ewma = owd_recent.down_ewma;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if owd_baseline.up_ewma > owd_recent.up_ewma {
|
self.ping_metrics.send(Metric::Ping {
|
||||||
owd_baseline.up_ewma = owd_recent.up_ewma;
|
reflector,
|
||||||
}
|
measurement_type: reply.stream.source,
|
||||||
}
|
rtt: reply.rtt_ms(),
|
||||||
|
up_time: reply
|
||||||
self.baseline_metrics.send(Metric::Baseline {
|
.one_way
|
||||||
reflector: time_data.reflector,
|
.map(|o| o.uplink.as_nanos() as f64 / 1000000.0)
|
||||||
baseline_up_ewma: owd_baseline.up_ewma,
|
.unwrap_or(reply.rtt_ms() / 2.0),
|
||||||
baseline_down_ewma: owd_baseline.down_ewma,
|
down_time: reply
|
||||||
recent_up_ewma: owd_recent.up_ewma,
|
.one_way
|
||||||
recent_down_ewma: owd_recent.down_ewma,
|
.map(|o| o.downlink.as_nanos() as f64 / 1000000.0)
|
||||||
|
.unwrap_or(reply.rtt_ms() / 2.0),
|
||||||
});
|
});
|
||||||
|
|
||||||
info!(
|
self.baseline_metrics.send(Metric::Baseline {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!(
|
||||||
"Reflector {} up baseline = {} down baseline = {}",
|
"Reflector {} up baseline = {} down baseline = {}",
|
||||||
time_data.reflector, owd_baseline.up_ewma, owd_baseline.down_ewma
|
reflector, state.baseline.up, state.baseline.down
|
||||||
);
|
);
|
||||||
info!(
|
debug!(
|
||||||
"Reflector {} up recent = {} down recent = {}",
|
"Reflector {} up recent = {} down recent = {}",
|
||||||
time_data.reflector, owd_recent.up_ewma, owd_recent.down_ewma
|
reflector, state.recent.up, state.recent.down
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let snapshot = control_snapshot(&reflectors, Instant::now());
|
||||||
|
if let Some(selection_tx) = &self.selection_tx {
|
||||||
|
let _ = selection_tx.send(snapshot.clone());
|
||||||
|
}
|
||||||
|
self.control_tx.send(snapshot)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::measurement::model::{
|
||||||
|
MeasurementSource, MeasurementStream, MeasurementStreamId, OneWayClock, OneWayLatency,
|
||||||
|
SignedDuration,
|
||||||
|
};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
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,
|
||||||
|
) -> MeasurementObservation {
|
||||||
|
MeasurementObservation {
|
||||||
|
stream: MeasurementStream {
|
||||||
|
id: MeasurementStreamId(0),
|
||||||
|
peer: reflector,
|
||||||
|
source: MeasurementSource::IcmpEcho,
|
||||||
|
},
|
||||||
|
started_at: received_at,
|
||||||
|
observed_at: received_at,
|
||||||
|
round_trip: None,
|
||||||
|
one_way: Some(OneWayLatency {
|
||||||
|
uplink: SignedDuration::from_millis(up_time as i64),
|
||||||
|
downlink: SignedDuration::from_millis(down_time as i64),
|
||||||
|
clock: OneWayClock::Uncalibrated,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
let stream = reply(reflector, 0.0, 0.0, start).stream;
|
||||||
|
reflectors.insert(stream, 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[&stream].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);
|
||||||
|
let stream = reply(reflector, 0.0, 0.0, received_at).stream;
|
||||||
|
|
||||||
|
assert!(!anomaly);
|
||||||
|
assert_eq!(snapshot.generated_at, generated_at);
|
||||||
|
assert_close(
|
||||||
|
snapshot.reflectors[&stream].baseline.down,
|
||||||
|
updated.baseline.down,
|
||||||
|
);
|
||||||
|
assert_close(snapshot.reflectors[&stream].recent.up, updated.recent.up);
|
||||||
|
assert_eq!(snapshot.reflectors[&stream].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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use flume::{Receiver, SendError, Sender};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
use crate::measurement::irtt::IrttBinding;
|
||||||
|
use crate::{
|
||||||
|
measurement::{
|
||||||
|
icmp::IcmpBinding,
|
||||||
|
model::{MeasurementSource, MeasurementStream, MeasurementStreamId, ProbeSchedule},
|
||||||
|
},
|
||||||
|
settings::ReflectorDefinition,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ControllerError {
|
||||||
|
#[error("ICMP channel error")]
|
||||||
|
IcmpChannelError(#[from] SendError<Vec<IcmpBinding>>),
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
#[error("IRTT channel error")]
|
||||||
|
IrttChannelError(#[from] SendError<Vec<IrttBinding>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Controller {
|
||||||
|
reflector_rx: Receiver<Vec<SocketAddr>>,
|
||||||
|
icmp_binding_tx: Sender<Vec<IcmpBinding>>,
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
irtt_binding_tx: Sender<Vec<IrttBinding>>,
|
||||||
|
reflectors: HashMap<SocketAddr, ReflectorDefinition>,
|
||||||
|
active_streams: Arc<RwLock<Vec<MeasurementStream>>>,
|
||||||
|
endpoint_streams: Arc<RwLock<HashMap<SocketAddr, MeasurementStream>>>,
|
||||||
|
source: MeasurementSource,
|
||||||
|
period: Duration,
|
||||||
|
next_stream_id: u64,
|
||||||
|
stream_ids: HashMap<(SocketAddr, MeasurementSource), MeasurementStreamId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Controller {
|
||||||
|
pub fn new(
|
||||||
|
reflector_rx: Receiver<Vec<SocketAddr>>,
|
||||||
|
icmp_binding_tx: Sender<Vec<IcmpBinding>>,
|
||||||
|
#[cfg(feature = "irtt")] irtt_binding_tx: Sender<Vec<IrttBinding>>,
|
||||||
|
reflectors: HashMap<SocketAddr, ReflectorDefinition>,
|
||||||
|
active_streams: Arc<RwLock<Vec<MeasurementStream>>>,
|
||||||
|
endpoint_streams: Arc<RwLock<HashMap<SocketAddr, MeasurementStream>>>,
|
||||||
|
source: MeasurementSource,
|
||||||
|
period: Duration,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
reflector_rx,
|
||||||
|
icmp_binding_tx,
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
irtt_binding_tx,
|
||||||
|
reflectors,
|
||||||
|
active_streams,
|
||||||
|
endpoint_streams,
|
||||||
|
source,
|
||||||
|
period,
|
||||||
|
next_stream_id: 0,
|
||||||
|
stream_ids: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(mut self, initial_reflectors: Vec<SocketAddr>) -> Result<(), ControllerError> {
|
||||||
|
self.update_reflectors(initial_reflectors).await?;
|
||||||
|
while let Ok(reflectors) = self.reflector_rx.recv_async().await {
|
||||||
|
self.update_reflectors(reflectors).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream_for(&mut self, endpoint: SocketAddr) -> MeasurementStream {
|
||||||
|
let key = (endpoint, self.source);
|
||||||
|
let id = *self.stream_ids.entry(key).or_insert_with(|| {
|
||||||
|
let id = MeasurementStreamId(self.next_stream_id);
|
||||||
|
self.next_stream_id += 1;
|
||||||
|
id
|
||||||
|
});
|
||||||
|
MeasurementStream {
|
||||||
|
id,
|
||||||
|
peer: endpoint.ip(),
|
||||||
|
source: self.source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_reflectors(
|
||||||
|
&mut self,
|
||||||
|
endpoints: Vec<SocketAddr>,
|
||||||
|
) -> Result<(), ControllerError> {
|
||||||
|
let count = endpoints.len();
|
||||||
|
let mut icmp = Vec::new();
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
let mut irtt = Vec::new();
|
||||||
|
let mut streams = Vec::with_capacity(count);
|
||||||
|
let mut endpoint_streams = HashMap::new();
|
||||||
|
for (index, endpoint) in endpoints.into_iter().enumerate() {
|
||||||
|
let Some(_definition) = self.reflectors.get(&endpoint) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
let (port, hmac) = {
|
||||||
|
(
|
||||||
|
_definition.irtt_port.unwrap_or(2112),
|
||||||
|
_definition.irtt_hmac.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let stream = self.stream_for(endpoint);
|
||||||
|
let schedule = ProbeSchedule {
|
||||||
|
period: self.period,
|
||||||
|
offset: evenly_spaced_offset(self.period, index, count),
|
||||||
|
};
|
||||||
|
match self.source {
|
||||||
|
MeasurementSource::IcmpEcho | MeasurementSource::IcmpTimestamp => {
|
||||||
|
icmp.push(IcmpBinding::new(stream, schedule))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
MeasurementSource::Irtt => irtt.push(IrttBinding::new(stream, port, hmac)),
|
||||||
|
#[cfg(not(feature = "irtt"))]
|
||||||
|
MeasurementSource::Irtt => unreachable!("IRTT feature is checked before startup"),
|
||||||
|
_ => unreachable!("unsupported reflector measurement source"),
|
||||||
|
}
|
||||||
|
streams.push(stream);
|
||||||
|
endpoint_streams.insert(endpoint, stream);
|
||||||
|
}
|
||||||
|
*self
|
||||||
|
.active_streams
|
||||||
|
.write()
|
||||||
|
.expect("active stream lock poisoned") = streams;
|
||||||
|
*self
|
||||||
|
.endpoint_streams
|
||||||
|
.write()
|
||||||
|
.expect("endpoint stream lock poisoned") = endpoint_streams;
|
||||||
|
self.icmp_binding_tx.send_async(icmp).await?;
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
self.irtt_binding_tx.send_async(irtt).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evenly_spaced_offset(period: Duration, index: usize, count: usize) -> Duration {
|
||||||
|
let nanos = period.as_nanos() * index as u128 / count as u128;
|
||||||
|
Duration::new(
|
||||||
|
(nanos / 1_000_000_000) as u64,
|
||||||
|
(nanos % 1_000_000_000) as u32,
|
||||||
|
)
|
||||||
|
}
|
||||||
+8
-5
@@ -1,7 +1,10 @@
|
|||||||
use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
use rustix::thread::ClockId;
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::time::Time;
|
use log::{Level, Metadata, Record, SetLoggerError};
|
||||||
|
|
||||||
|
use crate::time::{ClockSample, Realtime};
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct SimpleLogger {
|
pub struct SimpleLogger {
|
||||||
@@ -15,7 +18,7 @@ impl log::Log for SimpleLogger {
|
|||||||
|
|
||||||
fn log(&self, record: &Record) {
|
fn log(&self, record: &Record) {
|
||||||
if self.enabled(record.metadata()) {
|
if self.enabled(record.metadata()) {
|
||||||
let now = Time::new(ClockId::Realtime);
|
let now = ClockSample::<Realtime>::now();
|
||||||
println!(
|
println!(
|
||||||
"{} {:5} {}:{}: {}",
|
"{} {:5} {}:{}: {}",
|
||||||
now.as_secs_f64(),
|
now.as_secs_f64(),
|
||||||
@@ -32,5 +35,5 @@ impl log::Log for SimpleLogger {
|
|||||||
|
|
||||||
pub fn init(level: Level) -> Result<(), SetLoggerError> {
|
pub fn init(level: Level) -> Result<(), SetLoggerError> {
|
||||||
log::set_boxed_logger(Box::new(SimpleLogger { level }))
|
log::set_boxed_logger(Box::new(SimpleLogger { level }))
|
||||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
.map(|()| log::set_max_level(level.to_level_filter()))
|
||||||
}
|
}
|
||||||
+436
-233
@@ -1,117 +1,415 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
extern crate core;
|
extern crate core;
|
||||||
|
|
||||||
mod baseliner;
|
mod baseliner;
|
||||||
mod config;
|
mod controller;
|
||||||
mod log;
|
mod log;
|
||||||
|
mod measurement;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod netlink;
|
mod platform;
|
||||||
mod pinger;
|
|
||||||
mod pinger_icmp;
|
|
||||||
mod pinger_icmp_ts;
|
|
||||||
mod ratecontroller;
|
mod ratecontroller;
|
||||||
mod reflector_selector;
|
mod reflector_selector;
|
||||||
|
mod settings;
|
||||||
mod time;
|
mod time;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::baseliner::{Baseliner, ReflectorStats};
|
use crate::baseliner::Baseliner;
|
||||||
|
use crate::controller::Controller;
|
||||||
|
use crate::measurement::icmp::IcmpEngine;
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
use crate::measurement::irtt::IrttEngine;
|
||||||
|
use crate::measurement::model::{MeasurementSource, MeasurementStream};
|
||||||
use crate::metrics::{Metric, Metrics, MetricsSender};
|
use crate::metrics::{Metric, Metrics, MetricsSender};
|
||||||
use ::log::{debug, info};
|
use crate::platform::{TrafficControlBackend, traffic_control_backend, warn_platform_limitations};
|
||||||
|
use crate::ratecontroller::Ratecontroller;
|
||||||
|
use crate::reflector_selector::ReflectorSelector;
|
||||||
|
use crate::settings::{ReflectorDefinition, ReflectorProtocol, Settings};
|
||||||
|
use crate::util::RwLockExt;
|
||||||
|
use ::log::{info, warn};
|
||||||
|
use flume::RecvTimeoutError;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||||
use std::str::FromStr;
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::sync::mpsc::{channel, sync_channel, RecvTimeoutError};
|
use std::thread;
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::thread::{JoinHandle, sleep};
|
||||||
use std::thread::sleep;
|
use std::time::{Duration, Instant};
|
||||||
use std::time::Duration;
|
|
||||||
use std::time::Instant;
|
|
||||||
use std::{process, thread};
|
|
||||||
|
|
||||||
|
struct ReflectorSetup {
|
||||||
|
peers: Arc<RwLock<Vec<SocketAddr>>>,
|
||||||
|
active_streams: Arc<RwLock<Vec<MeasurementStream>>>,
|
||||||
|
endpoint_streams: Arc<RwLock<HashMap<SocketAddr, MeasurementStream>>>,
|
||||||
|
pool: Vec<SocketAddr>,
|
||||||
|
reflectors: HashMap<SocketAddr, ReflectorDefinition>,
|
||||||
|
reselection_enabled: bool,
|
||||||
|
}
|
||||||
pub static SHUTDOWN: AtomicBool = AtomicBool::new(false);
|
pub static SHUTDOWN: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
extern "C" fn signal_handler(_: libc::c_int) {
|
extern "C" fn signal_handler(_: libc::c_int) {
|
||||||
SHUTDOWN.store(true, Ordering::Relaxed);
|
SHUTDOWN.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::config::{Config, MeasurementType};
|
|
||||||
use crate::netlink::Netlink;
|
|
||||||
use crate::pinger::{PingListener, PingSender};
|
|
||||||
use crate::pinger_icmp::{PingerICMPEchoListener, PingerICMPEchoSender};
|
|
||||||
use crate::pinger_icmp_ts::{PingerICMPTimestampListener, PingerICMPTimestampSender};
|
|
||||||
use crate::ratecontroller::{Ratecontroller, StatsDirection};
|
|
||||||
use crate::reflector_selector::ReflectorSelector;
|
|
||||||
|
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn initialize_shaper<T: TrafficControlBackend>(
|
||||||
println!("Starting sqm-autorate version {}", VERSION);
|
settings: &Settings,
|
||||||
|
traffic_control: &mut T,
|
||||||
|
) -> anyhow::Result<(T::Handle, T::Handle)> {
|
||||||
|
initialize_shaper_with_settle_time(settings, traffic_control, Duration::from_secs(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_shaper_with_settle_time<T: TrafficControlBackend>(
|
||||||
|
settings: &Settings,
|
||||||
|
traffic_control: &mut T,
|
||||||
|
settle_time: Duration,
|
||||||
|
) -> anyhow::Result<(T::Handle, T::Handle)> {
|
||||||
|
let down = traffic_control.find_shaper(&settings.network.download_interface)?;
|
||||||
|
let up = traffic_control.find_shaper(&settings.network.upload_interface)?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Requesting minimum shaper rates (D/U): {} / {}",
|
||||||
|
settings.network.download_min_kbits(),
|
||||||
|
settings.network.upload_min_kbits(),
|
||||||
|
);
|
||||||
|
|
||||||
|
traffic_control.set_rate(
|
||||||
|
&down,
|
||||||
|
settings.network.download_min_kbits() as u64,
|
||||||
|
settings.advanced_settings.dry_run,
|
||||||
|
)?;
|
||||||
|
traffic_control.set_rate(
|
||||||
|
&up,
|
||||||
|
settings.network.upload_min_kbits() as u64,
|
||||||
|
settings.advanced_settings.dry_run,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Sleeping for {} seconds to give the shaper a chance to control existing bloat",
|
||||||
|
settle_time.as_secs_f64(),
|
||||||
|
);
|
||||||
|
|
||||||
|
sleep(settle_time);
|
||||||
|
|
||||||
|
Ok((down, up))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_signal_handlers() {
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::signal(libc::SIGINT, signal_handler as *const () as libc::sighandler_t);
|
libc::signal(
|
||||||
libc::signal(libc::SIGTERM, signal_handler as *const () as libc::sighandler_t);
|
libc::SIGINT,
|
||||||
|
signal_handler as *const () as libc::sighandler_t,
|
||||||
|
);
|
||||||
|
libc::signal(
|
||||||
|
libc::SIGTERM,
|
||||||
|
signal_handler as *const () as libc::sighandler_t,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = Config::new()?;
|
if let Err(error) = traffic_control.set_rate(
|
||||||
log::init(config.log_level)?;
|
up,
|
||||||
if config.dry_run {
|
settings.network.upload_base_kbits as u64,
|
||||||
|
settings.advanced_settings.dry_run,
|
||||||
|
) {
|
||||||
|
warn!("Failed to restore upload shaper rate: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_reflectors(
|
||||||
|
definitions: &[ReflectorDefinition],
|
||||||
|
protocol: ReflectorProtocol,
|
||||||
|
countries: &[String],
|
||||||
|
filter_country: bool,
|
||||||
|
) -> anyhow::Result<HashMap<SocketAddr, ReflectorDefinition>> {
|
||||||
|
let mut resolved = HashMap::new();
|
||||||
|
for definition in definitions {
|
||||||
|
if !definition.protocols.contains(&protocol) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if protocol != ReflectorProtocol::Irtt
|
||||||
|
&& definition
|
||||||
|
.host
|
||||||
|
.parse::<IpAddr>()
|
||||||
|
.is_ok_and(|ip| ip.is_ipv6())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if filter_country
|
||||||
|
&& !countries.is_empty()
|
||||||
|
&& definition.country.as_ref().is_some_and(|country| {
|
||||||
|
!countries
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| allowed.eq_ignore_ascii_case(country))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let port = if protocol == ReflectorProtocol::Irtt {
|
||||||
|
definition.irtt_port.unwrap_or(2112)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let endpoints = match (definition.host.as_str(), port).to_socket_addrs() {
|
||||||
|
Ok(endpoints) => endpoints,
|
||||||
|
Err(error) => {
|
||||||
|
warn!("Failed to resolve reflector {}: {error}", definition.host);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for endpoint in endpoints {
|
||||||
|
if protocol != ReflectorProtocol::Irtt && endpoint.is_ipv6() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
insert_reflector(&mut resolved, endpoint, definition.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_reflector(
|
||||||
|
reflectors: &mut HashMap<SocketAddr, ReflectorDefinition>,
|
||||||
|
endpoint: SocketAddr,
|
||||||
|
definition: ReflectorDefinition,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if let Some(existing) = reflectors.get_mut(&endpoint) {
|
||||||
|
let same_country = match (&existing.country, &definition.country) {
|
||||||
|
(Some(a), Some(b)) => a.eq_ignore_ascii_case(b),
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
let same_protocols = existing
|
||||||
|
.protocols
|
||||||
|
.iter()
|
||||||
|
.all(|p| definition.protocols.contains(p))
|
||||||
|
&& definition
|
||||||
|
.protocols
|
||||||
|
.iter()
|
||||||
|
.all(|p| existing.protocols.contains(p));
|
||||||
|
if !same_country
|
||||||
|
|| !same_protocols
|
||||||
|
|| existing.irtt_port.unwrap_or(2112) != definition.irtt_port.unwrap_or(2112)
|
||||||
|
|| existing.irtt_hmac != definition.irtt_hmac
|
||||||
|
{
|
||||||
|
anyhow::bail!("conflicting reflector definitions for resolved endpoint {endpoint}");
|
||||||
|
}
|
||||||
|
if existing.country.is_none() {
|
||||||
|
existing.country = definition.country;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reflectors.insert(endpoint, definition);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_reflectors(settings: &Settings) -> anyhow::Result<ReflectorSetup> {
|
||||||
|
let protocol = settings.advanced_settings.measurement_type;
|
||||||
|
let configured = resolve_reflectors(
|
||||||
|
&settings.reflectors.entries,
|
||||||
|
protocol,
|
||||||
|
&settings.reflectors.countries,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
let bootstrap = resolve_reflectors(&settings.reflectors.bootstrap, protocol, &[], false)?;
|
||||||
|
let mut reflectors = bootstrap.clone();
|
||||||
|
for (endpoint, definition) in &configured {
|
||||||
|
insert_reflector(&mut reflectors, *endpoint, definition.clone())?;
|
||||||
|
}
|
||||||
|
if reflectors.is_empty() {
|
||||||
|
anyhow::bail!("no usable reflectors for measurement protocol {protocol}");
|
||||||
|
}
|
||||||
|
let mut pool = configured.keys().copied().collect::<Vec<_>>();
|
||||||
|
pool.sort();
|
||||||
|
let mut initial = bootstrap.keys().copied().collect::<Vec<_>>();
|
||||||
|
initial.sort();
|
||||||
|
if initial.is_empty() {
|
||||||
|
initial = pool
|
||||||
|
.iter()
|
||||||
|
.take(settings.reflectors.num_reflectors as usize)
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
let reselection_enabled = pool.len() > settings.reflectors.num_reflectors as usize;
|
||||||
|
|
||||||
|
Ok(ReflectorSetup {
|
||||||
|
peers: Arc::new(RwLock::new(initial)),
|
||||||
|
active_streams: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
endpoint_streams: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
pool,
|
||||||
|
reflectors,
|
||||||
|
reselection_enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_task<F, E>(
|
||||||
|
rt: &tokio::runtime::Runtime,
|
||||||
|
error_tx: &flume::Sender<anyhow::Error>,
|
||||||
|
task: F,
|
||||||
|
) -> tokio::task::JoinHandle<()>
|
||||||
|
where
|
||||||
|
F: Future<Output = Result<(), E>> + Send + 'static,
|
||||||
|
E: Into<anyhow::Error> + Send + 'static,
|
||||||
|
{
|
||||||
|
let error_tx = error_tx.clone();
|
||||||
|
|
||||||
|
rt.spawn(async move {
|
||||||
|
if let Err(error) = task.await {
|
||||||
|
let _ = error_tx.send(error.into());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_worker<F>(
|
||||||
|
name: &str,
|
||||||
|
error_tx: &flume::Sender<anyhow::Error>,
|
||||||
|
worker: F,
|
||||||
|
) -> anyhow::Result<JoinHandle<()>>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> anyhow::Result<()> + Send + 'static,
|
||||||
|
{
|
||||||
|
let error_tx = error_tx.clone();
|
||||||
|
|
||||||
|
thread::Builder::new()
|
||||||
|
.name(name.to_owned())
|
||||||
|
.spawn(move || {
|
||||||
|
if let Err(error) = worker() {
|
||||||
|
let _ = error_tx.send(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|e| anyhow::anyhow!(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
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!("worker exited with error: {error}"));
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Timeout) => {
|
||||||
|
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||||
|
info!("Received shutdown signal");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(settings: &Settings) -> anyhow::Result<()> {
|
||||||
|
#[cfg(not(feature = "irtt"))]
|
||||||
|
if settings.advanced_settings.measurement_type == ReflectorProtocol::Irtt {
|
||||||
|
anyhow::bail!(
|
||||||
|
"IRTT measurement selected, but this binary was built without the `irtt` feature"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
|
||||||
|
if settings.advanced_settings.dry_run {
|
||||||
info!("*** MONITORING MODE ACTIVE — qdisc rates will NOT be changed ***");
|
info!("*** MONITORING MODE ACTIVE — qdisc rates will NOT be changed ***");
|
||||||
}
|
}
|
||||||
let mut reflectors = config.load_reflectors()?;
|
|
||||||
let start_t = Instant::now();
|
|
||||||
|
|
||||||
// The identifier field in ICMP is only 2 bytes
|
let start_time = Instant::now();
|
||||||
// so take the last 2 bytes of the PID as the ID
|
|
||||||
let id = (process::id() & 0xFFFF) as u16;
|
|
||||||
|
|
||||||
// Create data structures shared by different threads
|
let ReflectorSetup {
|
||||||
let owd_baseline = Arc::new(Mutex::new(HashMap::<IpAddr, ReflectorStats>::new()));
|
peers: reflector_peers,
|
||||||
let owd_recent = Arc::new(Mutex::new(HashMap::<IpAddr, ReflectorStats>::new()));
|
active_streams,
|
||||||
let reflector_peers_lock = Arc::new(RwLock::new(Vec::<IpAddr>::new()));
|
endpoint_streams,
|
||||||
let mut reflector_pool = Vec::<IpAddr>::new();
|
pool: reflector_pool,
|
||||||
let reflector_pool_size = reflectors.len();
|
reflectors,
|
||||||
|
reselection_enabled,
|
||||||
|
} = setup_reflectors(settings)?;
|
||||||
|
|
||||||
let default_reflectors = [
|
let (control_snapshot_tx, control_snapshot_rx) = flume::unbounded();
|
||||||
IpAddr::from_str("9.9.9.9")?,
|
let (error_tx, error_rx) = flume::unbounded::<anyhow::Error>();
|
||||||
IpAddr::from_str("8.238.120.14")?,
|
let (icmp_binding_tx, icmp_binding_rx) = flume::unbounded();
|
||||||
IpAddr::from_str("74.82.42.42")?,
|
#[cfg(feature = "irtt")]
|
||||||
IpAddr::from_str("194.242.2.2")?,
|
let (irtt_binding_tx, irtt_binding_rx) = flume::unbounded();
|
||||||
IpAddr::from_str("208.67.222.222")?,
|
let (measurement_tx, measurement_rx) = flume::unbounded();
|
||||||
IpAddr::from_str("94.140.14.14")?,
|
let (reflector_tx, reflector_rx) = flume::unbounded();
|
||||||
];
|
let (reselect_tx, reselect_rx) = flume::bounded(1);
|
||||||
|
let (selection_snapshot_tx, selection_snapshot_rx) = if reselection_enabled {
|
||||||
|
let (tx, rx) = flume::unbounded();
|
||||||
|
(Some(tx), Some(rx))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
match reflector_pool_size > config.num_reflectors as usize {
|
let source = match settings.advanced_settings.measurement_type {
|
||||||
true => {
|
ReflectorProtocol::IcmpEcho => MeasurementSource::IcmpEcho,
|
||||||
let mut peers = reflector_peers_lock.write().unwrap();
|
ReflectorProtocol::IcmpTimestamps => MeasurementSource::IcmpTimestamp,
|
||||||
peers.extend_from_slice(&default_reflectors);
|
ReflectorProtocol::Irtt => MeasurementSource::Irtt,
|
||||||
reflector_pool.append(reflectors.as_mut());
|
};
|
||||||
}
|
let controller = Controller::new(
|
||||||
false => {
|
reflector_rx,
|
||||||
let mut peers = reflector_peers_lock.write().unwrap();
|
icmp_binding_tx,
|
||||||
peers.extend_from_slice(&default_reflectors);
|
#[cfg(feature = "irtt")]
|
||||||
}
|
irtt_binding_tx,
|
||||||
}
|
reflectors,
|
||||||
|
Arc::clone(&active_streams),
|
||||||
|
Arc::clone(&endpoint_streams),
|
||||||
|
source,
|
||||||
|
Duration::from_secs_f64(settings.advanced_settings.tick_interval),
|
||||||
|
);
|
||||||
|
let icmp_engine = {
|
||||||
|
let _guard = rt.enter();
|
||||||
|
IcmpEngine::new(
|
||||||
|
start_time,
|
||||||
|
&settings.network,
|
||||||
|
measurement_tx.clone(),
|
||||||
|
icmp_binding_rx,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
let (irtt_task, irtt_engine) = {
|
||||||
|
let _guard = rt.enter();
|
||||||
|
IrttEngine::new(
|
||||||
|
Duration::from_secs_f64(settings.advanced_settings.tick_interval),
|
||||||
|
&settings.network,
|
||||||
|
measurement_tx,
|
||||||
|
irtt_binding_rx,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
let (baseliner_stats_tx, baseliner_stats_rx) = channel();
|
spawn_task(&rt, &error_tx, icmp_engine.run());
|
||||||
let (error_tx, error_rx) = channel::<anyhow::Error>();
|
#[cfg(feature = "irtt")]
|
||||||
let (reselect_tx, reselect_rx) = channel();
|
spawn_task(&rt, &error_tx, irtt_engine.run(irtt_task));
|
||||||
|
|
||||||
let dropped = Arc::new(AtomicU64::new(0));
|
let dropped = Arc::new(AtomicU32::new(0));
|
||||||
|
|
||||||
let (metrics_tx, metrics_thread_handle) = if config.observability_enabled {
|
let (metrics_tx, metrics_thread_handle) = if settings.observability.enabled {
|
||||||
let (tx, rx) = sync_channel(1000);
|
let (tx, rx) = flume::bounded(1000);
|
||||||
let metrics = Metrics {
|
let metrics = Metrics {
|
||||||
config: config.clone(),
|
settings: settings.clone(),
|
||||||
metrics_rx: rx,
|
metrics_rx: rx,
|
||||||
metrics_dropped: Arc::clone(&dropped),
|
metrics_dropped: Arc::clone(&dropped),
|
||||||
};
|
};
|
||||||
let err_tx = error_tx.clone();
|
let handle = spawn_worker("metrics", &error_tx, move || metrics.run())?;
|
||||||
let handle = thread::Builder::new()
|
|
||||||
.name("metrics".to_string())
|
|
||||||
.spawn(move || {
|
|
||||||
if let Err(e) = metrics.run() {
|
|
||||||
let _ = err_tx.send(e);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
(Some(tx), Some(handle))
|
(Some(tx), Some(handle))
|
||||||
} else {
|
} else {
|
||||||
(None, None)
|
(None, None)
|
||||||
@@ -125,190 +423,81 @@ fn main() -> anyhow::Result<()> {
|
|||||||
.unwrap_or_else(MetricsSender::disabled)
|
.unwrap_or_else(MetricsSender::disabled)
|
||||||
};
|
};
|
||||||
|
|
||||||
let ping_metrics = make_sender(config.observability_export_ping_metrics);
|
let ping_metrics = make_sender(settings.observability.export_ping_metrics);
|
||||||
let baseline_metrics = make_sender(config.observability_export_baseline_metrics);
|
let baseline_metrics = make_sender(settings.observability.export_baseline_metrics);
|
||||||
let event_metrics = make_sender(config.observability_export_events);
|
let event_metrics = make_sender(settings.observability.export_events);
|
||||||
let rate_metrics = make_sender(config.observability_export_rate_metrics);
|
let rate_metrics = make_sender(settings.observability.export_rate_metrics);
|
||||||
|
|
||||||
event_metrics.send(Metric::Event {
|
event_metrics.send(Metric::Event {
|
||||||
name: "starting",
|
name: "starting",
|
||||||
reason: "",
|
reason: "",
|
||||||
reflector: None,
|
reflector: None,
|
||||||
tags: if config.dry_run { &[("dry_run", "true")] } else { &[] },
|
tags: if settings.advanced_settings.dry_run {
|
||||||
|
&[("dry_run", "true")]
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let (mut ping_listener, mut ping_sender) = match config.measurement_type {
|
|
||||||
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 baseliner = Baseliner {
|
let baseliner = Baseliner {
|
||||||
config: config.clone(),
|
settings: settings.clone(),
|
||||||
owd_baseline: owd_baseline.clone(),
|
|
||||||
owd_recent: owd_recent.clone(),
|
|
||||||
reselect_trigger: reselect_tx.clone(),
|
reselect_trigger: reselect_tx.clone(),
|
||||||
start_time: start_t,
|
start_time,
|
||||||
stats_rx: baseliner_stats_rx,
|
stats_rx: measurement_rx,
|
||||||
|
control_tx: control_snapshot_tx,
|
||||||
|
selection_tx: selection_snapshot_tx,
|
||||||
baseline_metrics,
|
baseline_metrics,
|
||||||
event_metrics: event_metrics.clone(),
|
event_metrics: event_metrics.clone(),
|
||||||
|
ping_metrics,
|
||||||
};
|
};
|
||||||
|
|
||||||
let down_qdisc = Netlink::qdisc_from_ifname(config.download_interface.as_str())?;
|
let mut main_traffic_control = traffic_control_backend();
|
||||||
let up_qdisc = Netlink::qdisc_from_ifname(config.upload_interface.as_str())?;
|
let (down_shaper, up_shaper) = initialize_shaper(settings, &mut main_traffic_control)?;
|
||||||
|
|
||||||
/* Set initial TC values to minimum
|
spawn_worker("baseliner", &error_tx, move || baseliner.run())?;
|
||||||
* so there should be no initial bufferbloat to
|
spawn_task(
|
||||||
* fool the baseliner
|
&rt,
|
||||||
*/
|
&error_tx,
|
||||||
info!(
|
controller.run(reflector_peers.read_anyhow()?.clone()),
|
||||||
"Setting shaper rates to minimum (D/L): {} / {}",
|
|
||||||
config.download_min_kbits, config.upload_min_kbits
|
|
||||||
);
|
);
|
||||||
Netlink::set_qdisc_rate(down_qdisc, config.download_min_kbits as u64, config.dry_run)?;
|
|
||||||
Netlink::set_qdisc_rate(up_qdisc, config.upload_min_kbits as u64, config.dry_run)?;
|
|
||||||
|
|
||||||
// Sleep for a few seconds to give the shaper a chance
|
|
||||||
// to control the queue if load is heavy
|
|
||||||
let settle_sleep_time = Duration::new(2, 0);
|
|
||||||
info!(
|
|
||||||
"Sleeping for {} to give the shaper a chance to get in control if there's bloat",
|
|
||||||
settle_sleep_time.as_secs_f64()
|
|
||||||
);
|
|
||||||
sleep(settle_sleep_time);
|
|
||||||
|
|
||||||
let err_tx = error_tx.clone();
|
|
||||||
let reflector_peers_lock_clone = reflector_peers_lock.clone();
|
|
||||||
thread::Builder::new()
|
|
||||||
.name("receiver".to_string())
|
|
||||||
.spawn(move || {
|
|
||||||
if let Err(e) = ping_listener.listen(
|
|
||||||
id,
|
|
||||||
config.measurement_type,
|
|
||||||
reflector_peers_lock_clone,
|
|
||||||
baseliner_stats_tx,
|
|
||||||
ping_metrics,
|
|
||||||
) {
|
|
||||||
let _ = err_tx.send(e);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let err_tx = error_tx.clone();
|
|
||||||
thread::Builder::new()
|
|
||||||
.name("baseliner".to_string())
|
|
||||||
.spawn(move || {
|
|
||||||
if let Err(e) = baseliner.run() {
|
|
||||||
let _ = err_tx.send(e);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let err_tx = error_tx.clone();
|
|
||||||
let reflector_peers_lock_clone = reflector_peers_lock.clone();
|
|
||||||
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,
|
|
||||||
) {
|
|
||||||
let _ = err_tx.send(e);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let main_event_metrics = event_metrics.clone();
|
let main_event_metrics = event_metrics.clone();
|
||||||
|
|
||||||
if reflector_pool_size > config.num_reflectors as usize {
|
if reselection_enabled {
|
||||||
let reflector_selector = ReflectorSelector {
|
let reflector_selector = ReflectorSelector {
|
||||||
config: config.clone(),
|
settings: settings.clone(),
|
||||||
owd_recent: owd_recent.clone(),
|
snapshot_rx: selection_snapshot_rx
|
||||||
reflector_peers_lock: reflector_peers_lock.clone(),
|
.expect("reselection snapshot receiver must exist when reselection is enabled"),
|
||||||
|
reflector_peers_lock: Arc::clone(&reflector_peers),
|
||||||
reflector_pool,
|
reflector_pool,
|
||||||
|
endpoint_streams,
|
||||||
trigger_channel: reselect_rx,
|
trigger_channel: reselect_rx,
|
||||||
metrics: event_metrics,
|
metrics: event_metrics,
|
||||||
|
reflector_tx,
|
||||||
};
|
};
|
||||||
let err_tx = error_tx.clone();
|
spawn_worker("reselection", &error_tx, move || reflector_selector.run())?;
|
||||||
thread::Builder::new()
|
|
||||||
.name("reselection".to_string())
|
|
||||||
.spawn(move || {
|
|
||||||
if let Err(e) = reflector_selector.run() {
|
|
||||||
let _ = err_tx.send(e);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sleep 10 seconds before we start adjusting speeds
|
// Give the baseliner time to collect initial samples before adjusting rates.
|
||||||
sleep(Duration::new(10, 0));
|
sleep(Duration::from_secs(10));
|
||||||
|
|
||||||
let dl_direction = if config.download_interface.starts_with("ifb")
|
|
||||||
|| config.download_interface.starts_with("veth")
|
|
||||||
{
|
|
||||||
StatsDirection::TX
|
|
||||||
} else {
|
|
||||||
StatsDirection::RX
|
|
||||||
};
|
|
||||||
let ul_direction = if config.upload_interface.starts_with("ifb")
|
|
||||||
|| config.upload_interface.starts_with("veth")
|
|
||||||
{
|
|
||||||
StatsDirection::RX
|
|
||||||
} else {
|
|
||||||
StatsDirection::TX
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut ratecontroller = Ratecontroller::new(
|
let mut ratecontroller = Ratecontroller::new(
|
||||||
config.clone(),
|
settings.clone(),
|
||||||
owd_baseline,
|
control_snapshot_rx,
|
||||||
owd_recent,
|
active_streams,
|
||||||
reflector_peers_lock,
|
|
||||||
reselect_tx,
|
reselect_tx,
|
||||||
dl_direction,
|
|
||||||
ul_direction,
|
|
||||||
rate_metrics,
|
rate_metrics,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
debug!(
|
spawn_worker("ratecontroller", &error_tx, move || ratecontroller.run())?;
|
||||||
"Download direction: {}:{:?}",
|
|
||||||
config.download_interface, dl_direction
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!(
|
// Drop the original sender so the channel disconnects if all workers exit cleanly.
|
||||||
"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);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Drop original sender so the channel disconnects if all threads exit cleanly
|
|
||||||
drop(error_tx);
|
drop(error_tx);
|
||||||
|
|
||||||
let result = loop {
|
let result = wait_for_exit(&error_rx);
|
||||||
match error_rx.recv_timeout(Duration::from_secs(1)) {
|
|
||||||
Ok(e) => break Err(anyhow::anyhow!("thread exited with error: {e}")),
|
// Make the error path request shutdown too, rather than leaving the other workers running.
|
||||||
Err(RecvTimeoutError::Disconnected) => break Ok(()),
|
SHUTDOWN.store(true, Ordering::Relaxed);
|
||||||
Err(RecvTimeoutError::Timeout) => {
|
|
||||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
|
||||||
info!("Received shutdown signal");
|
|
||||||
break Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let stopping_reason = if result.is_err() { "error" } else { "signal" };
|
let stopping_reason = if result.is_err() { "error" } else { "signal" };
|
||||||
main_event_metrics.send(Metric::Event {
|
main_event_metrics.send(Metric::Event {
|
||||||
@@ -319,20 +508,34 @@ fn main() -> anyhow::Result<()> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Drop all MetricsSender instances and the raw tx so the metrics channel
|
// Drop all MetricsSender instances and the raw tx so the metrics channel
|
||||||
// disconnects once all threads also drop their copies, allowing the
|
// disconnects once all worker threads also drop their copies.
|
||||||
// metrics thread to drain and exit cleanly.
|
|
||||||
drop(main_event_metrics);
|
drop(main_event_metrics);
|
||||||
drop(metrics_tx);
|
drop(metrics_tx);
|
||||||
|
|
||||||
if let Some(handle) = metrics_thread_handle {
|
if let Some(handle) = metrics_thread_handle {
|
||||||
let _ = handle.join();
|
let _ = handle.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
restore_shaper(
|
||||||
"Restoring base shaper rates (D/L): {} / {}",
|
settings,
|
||||||
config.download_base_kbits, config.upload_base_kbits
|
&mut main_traffic_control,
|
||||||
|
&down_shaper,
|
||||||
|
&up_shaper,
|
||||||
);
|
);
|
||||||
let _ = Netlink::set_qdisc_rate(down_qdisc, config.download_base_kbits as u64, config.dry_run);
|
|
||||||
let _ = Netlink::set_qdisc_rate(up_qdisc, config.upload_base_kbits as u64, config.dry_run);
|
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
println!("Starting sqm-autorate-rust version {}", VERSION);
|
||||||
|
|
||||||
|
install_signal_handlers();
|
||||||
|
|
||||||
|
let settings = Settings::load()?;
|
||||||
|
settings.validate()?;
|
||||||
|
|
||||||
|
log::init(settings.output.log_level)?;
|
||||||
|
warn_platform_limitations();
|
||||||
|
|
||||||
|
run(&settings)
|
||||||
|
}
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
io,
|
||||||
|
net::IpAddr,
|
||||||
|
time::{Duration, Instant, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use icmp_socket2::{
|
||||||
|
AsyncIcmpSocket, IcmpSocket4, Icmpv4Message, Icmpv4Packet,
|
||||||
|
packet::{IcmpPacketBuildError, WithEchoRequest, WithTimestampRequest},
|
||||||
|
tokio::AsyncIcmpV4Socket,
|
||||||
|
};
|
||||||
|
use log::debug;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
measurement::model::{
|
||||||
|
MeasurementEvent, MeasurementLoss, MeasurementObservation, MeasurementSource,
|
||||||
|
MeasurementStream, MeasurementStreamId, OneWayClock, OneWayLatency, ProbeSchedule,
|
||||||
|
SignedDuration,
|
||||||
|
},
|
||||||
|
settings::NetworkSettings,
|
||||||
|
time::{ClockSample, Realtime},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct IcmpBinding {
|
||||||
|
pub stream: MeasurementStream,
|
||||||
|
pub schedule: ProbeSchedule,
|
||||||
|
|
||||||
|
next_probe_at: Instant,
|
||||||
|
next_sequence: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IcmpBinding {
|
||||||
|
pub fn new(stream: MeasurementStream, schedule: ProbeSchedule) -> Self {
|
||||||
|
Self {
|
||||||
|
stream,
|
||||||
|
schedule,
|
||||||
|
next_probe_at: Instant::now(),
|
||||||
|
next_sequence: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum IcmpError {
|
||||||
|
#[error("I/O error")]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
|
||||||
|
#[error("failed to build ICMP packet")]
|
||||||
|
PacketBuild(#[from] IcmpPacketBuildError),
|
||||||
|
|
||||||
|
#[error("invalid ICMP packet: {0}")]
|
||||||
|
InvalidPacket(String),
|
||||||
|
|
||||||
|
#[error("unsupported measurement source: {0:?}")]
|
||||||
|
UnsupportedSource(MeasurementSource),
|
||||||
|
|
||||||
|
#[error("probe interval must be non-zero for stream {0:?}")]
|
||||||
|
InvalidInterval(MeasurementStreamId),
|
||||||
|
|
||||||
|
#[error("probe offset must be less than the period for stream {0:?}")]
|
||||||
|
InvalidOffset(MeasurementStreamId),
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"no matching probe found for peer {:?}, source: {:?}, sequence: {}",
|
||||||
|
.0.0,
|
||||||
|
.0.1,
|
||||||
|
.0.2
|
||||||
|
)]
|
||||||
|
NoMatchingProbe(InFlightProbeKey),
|
||||||
|
|
||||||
|
#[error("wrong identifier: expected {expected}, found {found}")]
|
||||||
|
WrongIdentifier { expected: u16, found: u16 },
|
||||||
|
|
||||||
|
#[error("unsupported ICMP peer address: {0}")]
|
||||||
|
UnsupportedPeer(IpAddr),
|
||||||
|
|
||||||
|
#[error("event channel closed")]
|
||||||
|
EventChannelClosed,
|
||||||
|
|
||||||
|
#[error("binding channel closed")]
|
||||||
|
BindingChannelClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct InFlightProbe {
|
||||||
|
stream: MeasurementStream,
|
||||||
|
sent_at: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
type InFlightProbeKey = (IpAddr, MeasurementSource, u16);
|
||||||
|
type InFlightProbes = HashMap<InFlightProbeKey, InFlightProbe>;
|
||||||
|
|
||||||
|
pub struct IcmpEngine {
|
||||||
|
socket: AsyncIcmpV4Socket,
|
||||||
|
|
||||||
|
identifier: u16,
|
||||||
|
|
||||||
|
bindings: HashMap<MeasurementStreamId, IcmpBinding>,
|
||||||
|
inflight: HashMap<InFlightProbeKey, InFlightProbe>,
|
||||||
|
|
||||||
|
origin: Instant,
|
||||||
|
|
||||||
|
binding_rx: Receiver<Vec<IcmpBinding>>,
|
||||||
|
observation_tx: Sender<MeasurementEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_deadline(deadline: Option<Instant>) {
|
||||||
|
match deadline {
|
||||||
|
Some(deadline) => {
|
||||||
|
tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IcmpEngine {
|
||||||
|
pub fn new(
|
||||||
|
origin: Instant,
|
||||||
|
#[allow(unused)] settings: &NetworkSettings,
|
||||||
|
observation_tx: Sender<MeasurementEvent>,
|
||||||
|
binding_rx: Receiver<Vec<IcmpBinding>>,
|
||||||
|
) -> Result<Self, IcmpError> {
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut socket = IcmpSocket4::new()?;
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||||
|
if let Some(device) = &settings.measurement_bind_device {
|
||||||
|
icmp_socket2::IcmpSocket::bind_device(&mut socket, device)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||||
|
if let Some(mark) = settings.measurement_mark {
|
||||||
|
icmp_socket2::IcmpSocket::set_mark(&mut socket, mark)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
if let Some(fib) = settings.measurement_fib {
|
||||||
|
icmp_socket2::IcmpSocket::set_fib(&mut socket, fib)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.enable_receive_metadata()?;
|
||||||
|
let identifier = (std::process::id() & 0xffff) as u16;
|
||||||
|
let inflight: InFlightProbes = HashMap::new();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
socket: socket.into_tokio()?,
|
||||||
|
identifier,
|
||||||
|
bindings: HashMap::new(),
|
||||||
|
inflight,
|
||||||
|
origin,
|
||||||
|
binding_rx,
|
||||||
|
observation_tx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_bindings(&mut self, desired: Vec<IcmpBinding>) -> Result<(), IcmpError> {
|
||||||
|
// Validate the complete desired state before mutating anything.
|
||||||
|
for binding in &desired {
|
||||||
|
if binding.schedule.period.is_zero() {
|
||||||
|
return Err(IcmpError::InvalidInterval(binding.stream.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if binding.schedule.offset >= binding.schedule.period {
|
||||||
|
return Err(IcmpError::InvalidOffset(binding.stream.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matches!(
|
||||||
|
binding.stream.source,
|
||||||
|
MeasurementSource::IcmpEcho | MeasurementSource::IcmpTimestamp
|
||||||
|
) {
|
||||||
|
return Err(IcmpError::UnsupportedSource(binding.stream.source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let desired_ids = desired
|
||||||
|
.iter()
|
||||||
|
.map(|binding| binding.stream.id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
// Remove bindings that are no longer desired.
|
||||||
|
self.bindings.retain(|id, _| desired_ids.contains(id));
|
||||||
|
self.inflight
|
||||||
|
.retain(|_, probe| desired_ids.contains(&probe.stream.id));
|
||||||
|
|
||||||
|
for desired in desired {
|
||||||
|
match self.bindings.get_mut(&desired.stream.id) {
|
||||||
|
Some(current) if current.schedule == desired.schedule => {
|
||||||
|
// Same schedule, so leave next_probe_at alone.
|
||||||
|
current.stream = desired.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(current) => {
|
||||||
|
// Same stream, but its schedule changed. Rejoin the new grid.
|
||||||
|
current.stream = desired.stream;
|
||||||
|
current.schedule = desired.schedule;
|
||||||
|
current.next_probe_at = desired.schedule.next_probe_at(self.origin, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
// New stream. Join its configured schedule at the next
|
||||||
|
// occurrence after now.
|
||||||
|
let mut binding = desired;
|
||||||
|
binding.next_probe_at = binding.schedule.next_probe_at(self.origin, now);
|
||||||
|
|
||||||
|
self.bindings.insert(binding.stream.id, binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_due_probes(&mut self, now: Instant) -> Result<(), IcmpError> {
|
||||||
|
let due = self
|
||||||
|
.bindings
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(&stream_id, binding)| (binding.next_probe_at <= now).then_some(stream_id))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for stream_id in due {
|
||||||
|
self.send_probe(stream_id).await?;
|
||||||
|
|
||||||
|
let binding = self
|
||||||
|
.bindings
|
||||||
|
.get_mut(&stream_id)
|
||||||
|
.expect("due binding disappeared");
|
||||||
|
|
||||||
|
binding.next_probe_at = binding.schedule.next_probe_at(self.origin, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_probe(&mut self, stream_id: MeasurementStreamId) -> Result<(), IcmpError> {
|
||||||
|
let (stream, sequence) = {
|
||||||
|
let binding = self
|
||||||
|
.bindings
|
||||||
|
.get_mut(&stream_id)
|
||||||
|
.expect("binding disappeared");
|
||||||
|
|
||||||
|
let sequence = binding.next_sequence;
|
||||||
|
binding.next_sequence = binding.next_sequence.wrapping_add(1);
|
||||||
|
|
||||||
|
(binding.stream, sequence)
|
||||||
|
};
|
||||||
|
|
||||||
|
let IpAddr::V4(peer) = stream.peer else {
|
||||||
|
return Err(IcmpError::UnsupportedPeer(stream.peer));
|
||||||
|
};
|
||||||
|
|
||||||
|
let packet = match stream.source {
|
||||||
|
MeasurementSource::IcmpEcho => {
|
||||||
|
Icmpv4Packet::with_echo_request(self.identifier, sequence, Vec::new())?
|
||||||
|
}
|
||||||
|
MeasurementSource::IcmpTimestamp => {
|
||||||
|
let originate = ClockSample::<Realtime>::now().as_time_since_midnight() as u32;
|
||||||
|
Icmpv4Packet::with_timestamp_request(self.identifier, sequence, originate, 0, 0)?
|
||||||
|
}
|
||||||
|
source => return Err(IcmpError::UnsupportedSource(source)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sent_at = Instant::now();
|
||||||
|
let key = (stream.peer, stream.source, sequence);
|
||||||
|
|
||||||
|
self.socket.send_to(peer, packet).await?;
|
||||||
|
self.inflight.insert(key, InFlightProbe { stream, sent_at });
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_deadline(&self) -> Option<Instant> {
|
||||||
|
self.bindings
|
||||||
|
.values()
|
||||||
|
.map(|binding| binding.next_probe_at)
|
||||||
|
.min()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_reply(
|
||||||
|
&mut self,
|
||||||
|
packet: Icmpv4Packet,
|
||||||
|
peer: IpAddr,
|
||||||
|
observed_at: Instant,
|
||||||
|
kernel_rx_timestamp: Option<SystemTime>,
|
||||||
|
) -> Result<MeasurementObservation, IcmpError> {
|
||||||
|
match packet.message {
|
||||||
|
Icmpv4Message::EchoReply {
|
||||||
|
identifier,
|
||||||
|
sequence,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if identifier != self.identifier {
|
||||||
|
return Err(IcmpError::WrongIdentifier {
|
||||||
|
expected: self.identifier,
|
||||||
|
found: identifier,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = (peer, MeasurementSource::IcmpEcho, sequence);
|
||||||
|
let probe = self
|
||||||
|
.inflight
|
||||||
|
.remove(&key)
|
||||||
|
.ok_or(IcmpError::NoMatchingProbe(key))?;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"ICMP engine > Type: {:4} | Reflector IP: {:>15} | Seq: {:5} | Current time: {:8} | Originate: {:>8} | Received time: {:>8} | Transmit time: {:>8} | RTT: {:8.3} ms | UL time: {:>8} | DL time: {:>8}",
|
||||||
|
"ICMP",
|
||||||
|
peer,
|
||||||
|
sequence,
|
||||||
|
ClockSample::<Realtime>::now().as_time_since_midnight(),
|
||||||
|
"N/A",
|
||||||
|
"N/A",
|
||||||
|
"N/A",
|
||||||
|
observed_at.duration_since(probe.sent_at).as_secs_f64() * 1_000.0,
|
||||||
|
"N/A",
|
||||||
|
"N/A",
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(MeasurementObservation {
|
||||||
|
stream: probe.stream,
|
||||||
|
started_at: probe.sent_at,
|
||||||
|
observed_at,
|
||||||
|
round_trip: None,
|
||||||
|
one_way: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Icmpv4Message::TimestampReply {
|
||||||
|
identifier,
|
||||||
|
sequence,
|
||||||
|
originate,
|
||||||
|
receive,
|
||||||
|
transmit,
|
||||||
|
} => {
|
||||||
|
if identifier != self.identifier {
|
||||||
|
return Err(IcmpError::WrongIdentifier {
|
||||||
|
expected: self.identifier,
|
||||||
|
found: identifier,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if originate >= MS_PER_DAY as u32
|
||||||
|
|| receive >= MS_PER_DAY as u32
|
||||||
|
|| transmit >= MS_PER_DAY as u32
|
||||||
|
{
|
||||||
|
return Err(IcmpError::InvalidPacket(
|
||||||
|
"non-standard RFC 792 timestamp".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = (peer, MeasurementSource::IcmpTimestamp, sequence);
|
||||||
|
let probe = self
|
||||||
|
.inflight
|
||||||
|
.remove(&key)
|
||||||
|
.ok_or(IcmpError::NoMatchingProbe(key))?;
|
||||||
|
|
||||||
|
let now_ms = if let Some(timestamp) = kernel_rx_timestamp {
|
||||||
|
ClockSample::<Realtime>::from(timestamp).as_time_since_midnight()
|
||||||
|
} else {
|
||||||
|
ClockSample::<Realtime>::now().as_time_since_midnight()
|
||||||
|
};
|
||||||
|
|
||||||
|
let uplink_ms = timestamp_delta(receive as i64, originate as i64);
|
||||||
|
let downlink_ms = timestamp_delta(now_ms, transmit as i64);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"ICMP engine > Type: {:4} | Reflector IP: {:>15} | Seq: {:5} | Current time: {:8} | Originate: {:8} | Received time: {:8} | Transmit time: {:8} | RTT: {:8.3} ms | UL time: {:5} ms | DL time: {:5} ms",
|
||||||
|
"ICMP",
|
||||||
|
peer,
|
||||||
|
sequence,
|
||||||
|
now_ms,
|
||||||
|
originate,
|
||||||
|
receive,
|
||||||
|
transmit,
|
||||||
|
observed_at.duration_since(probe.sent_at).as_secs_f64() * 1_000.0,
|
||||||
|
uplink_ms,
|
||||||
|
downlink_ms,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(MeasurementObservation {
|
||||||
|
stream: probe.stream,
|
||||||
|
started_at: probe.sent_at,
|
||||||
|
observed_at,
|
||||||
|
round_trip: None,
|
||||||
|
one_way: Some(OneWayLatency {
|
||||||
|
uplink: SignedDuration::from_millis(uplink_ms),
|
||||||
|
downlink: SignedDuration::from_millis(downlink_ms),
|
||||||
|
clock: OneWayClock::Uncalibrated,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(IcmpError::InvalidPacket(
|
||||||
|
"unsupported ICMP message type".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expire_inflight(&mut self, now: Instant) -> Result<(), IcmpError> {
|
||||||
|
const PROBE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
let expired = {
|
||||||
|
let mut expired = Vec::new();
|
||||||
|
|
||||||
|
self.inflight.retain(|_, probe| {
|
||||||
|
if now.saturating_duration_since(probe.sent_at) >= PROBE_TIMEOUT {
|
||||||
|
expired.push(*probe);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expired
|
||||||
|
};
|
||||||
|
|
||||||
|
for probe in expired {
|
||||||
|
self.observation_tx
|
||||||
|
.send_async(MeasurementEvent::Loss(MeasurementLoss {
|
||||||
|
stream: probe.stream,
|
||||||
|
started_at: probe.sent_at,
|
||||||
|
detected_at: now,
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.map_err(|_| IcmpError::EventChannelClosed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(mut self) -> Result<(), IcmpError> {
|
||||||
|
const HOUSEKEEPING_INTERVAL: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
|
let mut housekeeping = tokio::time::interval_at(
|
||||||
|
tokio::time::Instant::now() + HOUSEKEEPING_INTERVAL,
|
||||||
|
HOUSEKEEPING_INTERVAL,
|
||||||
|
);
|
||||||
|
|
||||||
|
housekeeping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let deadline = self.next_deadline();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
bindings = self.binding_rx.recv_async() => {
|
||||||
|
let bindings = bindings
|
||||||
|
.map_err(|_| IcmpError::BindingChannelClosed)?;
|
||||||
|
|
||||||
|
self.set_bindings(bindings)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = wait_for_deadline(deadline) => {
|
||||||
|
self.send_due_probes(Instant::now()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.socket.rcv_from_with_meta() => {
|
||||||
|
let result = result?;
|
||||||
|
|
||||||
|
match self.handle_reply(
|
||||||
|
result.packet,
|
||||||
|
result.peer.as_socket().unwrap().ip(),
|
||||||
|
result.received_at,
|
||||||
|
result.kernel_rx_timestamp,
|
||||||
|
) {
|
||||||
|
Ok(observation) => {
|
||||||
|
self.observation_tx
|
||||||
|
.send_async(MeasurementEvent::Observation(observation))
|
||||||
|
.await
|
||||||
|
.map_err(|_| IcmpError::EventChannelClosed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(
|
||||||
|
IcmpError::WrongIdentifier { .. }
|
||||||
|
| IcmpError::NoMatchingProbe { .. }
|
||||||
|
| IcmpError::InvalidPacket(_),
|
||||||
|
) => {
|
||||||
|
// Not ours, malformed, or late.
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = housekeeping.tick() => {
|
||||||
|
self.expire_inflight(Instant::now()).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
use std::{collections::HashMap, net::SocketAddr};
|
||||||
|
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use irtt_client::{
|
||||||
|
ClientConfig, ClientEvent, NegotiationPolicy, OpenPolicy,
|
||||||
|
managed::{
|
||||||
|
ManagedClient, ManagedClientConfig, ManagedClientHandle, ManagedClientTask,
|
||||||
|
ManagedCommandApplyError, ManagedCommandError, ManagedCompletionPolicy, ManagedConfigError,
|
||||||
|
ManagedEndReason, ManagedEvent, ManagedLifecycle, ManagedSubscribeError,
|
||||||
|
ManagedTargetConfig, ManagedTargetEndReason, ManagedTargetLifecycle, TargetAuth, TargetId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use log::{debug, trace, warn};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::sync::broadcast::error::RecvError;
|
||||||
|
|
||||||
|
use crate::SHUTDOWN;
|
||||||
|
use crate::measurement::model::{
|
||||||
|
MeasurementEvent, MeasurementLoss, MeasurementObservation, MeasurementStream,
|
||||||
|
MeasurementStreamId, OneWayClock, OneWayLatency, SignedDuration,
|
||||||
|
};
|
||||||
|
use crate::settings::NetworkSettings;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum IrttError {
|
||||||
|
#[error("failed to build IRTT config")]
|
||||||
|
InvalidConfig(#[from] ManagedConfigError),
|
||||||
|
|
||||||
|
#[error("event channel closed")]
|
||||||
|
EventChannelClosed,
|
||||||
|
|
||||||
|
#[error("binding channel closed")]
|
||||||
|
BindingChannelClosed,
|
||||||
|
|
||||||
|
#[error("error while reciving on channel")]
|
||||||
|
ChannelRecvError(#[from] RecvError),
|
||||||
|
|
||||||
|
#[error("unable to subscribe to irtt events")]
|
||||||
|
SubscriptionError(#[from] ManagedSubscribeError),
|
||||||
|
|
||||||
|
#[error("failed to submit IRTT target update")]
|
||||||
|
CommandError(#[from] ManagedCommandError),
|
||||||
|
|
||||||
|
#[error("failed to apply IRTT target update")]
|
||||||
|
CommandApplyError(#[from] ManagedCommandApplyError),
|
||||||
|
|
||||||
|
#[error("IRTT managed client failed: {0:?}")]
|
||||||
|
ManagedFailure(ManagedEndReason),
|
||||||
|
|
||||||
|
#[error("IRTT managed task was abandoned")]
|
||||||
|
ManagedAbandoned,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IrttBinding {
|
||||||
|
stream: MeasurementStream,
|
||||||
|
port: u16,
|
||||||
|
hmac: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IrttBinding {
|
||||||
|
pub fn new(stream: MeasurementStream, port: u16, hmac: Option<String>) -> Self {
|
||||||
|
Self { stream, port, hmac }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IrttEngine {
|
||||||
|
client: ManagedClientHandle,
|
||||||
|
|
||||||
|
bindings: HashMap<MeasurementStreamId, IrttBinding>,
|
||||||
|
targets: Vec<ManagedTargetConfig>,
|
||||||
|
|
||||||
|
binding_rx: Receiver<Vec<IrttBinding>>,
|
||||||
|
observation_tx: Sender<MeasurementEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IrttEngine {
|
||||||
|
pub fn new(
|
||||||
|
interval: std::time::Duration,
|
||||||
|
settings: &NetworkSettings,
|
||||||
|
observation_tx: Sender<MeasurementEvent>,
|
||||||
|
binding_rx: Receiver<Vec<IrttBinding>>,
|
||||||
|
) -> Result<(ManagedClientTask, Self), IrttError> {
|
||||||
|
let config = managed_client_config(interval, settings);
|
||||||
|
let (task, handle) = ManagedClient::task(config, Vec::new())?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
task,
|
||||||
|
Self {
|
||||||
|
client: handle,
|
||||||
|
bindings: HashMap::new(),
|
||||||
|
targets: Vec::new(),
|
||||||
|
binding_rx,
|
||||||
|
observation_tx,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_bindings(&mut self, bindings: Vec<IrttBinding>) -> Result<(), IrttError> {
|
||||||
|
let targets = bindings
|
||||||
|
.iter()
|
||||||
|
.map(|binding| {
|
||||||
|
let mut target = ManagedTargetConfig::new(
|
||||||
|
TargetId::new(binding.stream.id.0.to_string()),
|
||||||
|
SocketAddr::new(binding.stream.peer, binding.port).to_string(),
|
||||||
|
);
|
||||||
|
if let Some(key) = binding.hmac.as_ref().filter(|key| !key.trim().is_empty()) {
|
||||||
|
target.auth = TargetAuth::Override(irtt_client::Authentication::Hmac(
|
||||||
|
irtt_client::HmacKey::new(key.as_bytes().to_vec().into_boxed_slice()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
target
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
self.client.update_targets(targets.clone())?.await?;
|
||||||
|
self.targets = targets;
|
||||||
|
|
||||||
|
self.bindings = bindings
|
||||||
|
.into_iter()
|
||||||
|
.map(|binding| (binding.stream.id, binding))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(mut self, task: ManagedClientTask) -> Result<(), IrttError> {
|
||||||
|
let mut task = Box::pin(task);
|
||||||
|
let client = self.client.clone();
|
||||||
|
|
||||||
|
// Keep driving the client even while the event loop awaits command receipts.
|
||||||
|
let result = tokio::select! {
|
||||||
|
biased;
|
||||||
|
outcome = task.as_mut() => {
|
||||||
|
return match outcome.end_reason {
|
||||||
|
ManagedEndReason::StopRequested => Ok(()),
|
||||||
|
end_reason => Err(IrttError::ManagedFailure(end_reason)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
result = self.run_events() => result,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Every event-loop exit stops and joins the driver, including channel errors.
|
||||||
|
let stop = client.stop();
|
||||||
|
let (outcome, _) = tokio::join!(task, stop);
|
||||||
|
result?;
|
||||||
|
match outcome.end_reason {
|
||||||
|
ManagedEndReason::StopRequested => Ok(()),
|
||||||
|
end_reason => Err(IrttError::ManagedFailure(end_reason)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_events(&mut self) -> Result<(), IrttError> {
|
||||||
|
let mut events = self.client.subscribe()?;
|
||||||
|
// Durable status catches failures even if their presentation events were lost.
|
||||||
|
// Resubmitting the full desired set restarts terminal targets only.
|
||||||
|
let mut retry = tokio::time::interval(std::time::Duration::from_secs(5));
|
||||||
|
retry.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||||
|
retry.tick().await;
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
bindings = self.binding_rx.recv_async() => {
|
||||||
|
let bindings = bindings
|
||||||
|
.map_err(|_| IrttError::BindingChannelClosed)?;
|
||||||
|
|
||||||
|
self.set_bindings(bindings).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = retry.tick() => {
|
||||||
|
if SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.client.status().targets.iter().any(|target| {
|
||||||
|
target.desired && target.lifecycle == ManagedTargetLifecycle::Terminal
|
||||||
|
}) {
|
||||||
|
self.client.update_targets(self.targets.clone())?.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event = events.recv() => {
|
||||||
|
let event = match event {
|
||||||
|
Ok(event) => event,
|
||||||
|
|
||||||
|
Err(RecvError::Lagged(n)) => {
|
||||||
|
warn!("IRTT event receiver dropped {n} events");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(RecvError::Closed) => {
|
||||||
|
let status = self.client.status();
|
||||||
|
|
||||||
|
match status.lifecycle {
|
||||||
|
ManagedLifecycle::Completed => return Ok(()),
|
||||||
|
|
||||||
|
ManagedLifecycle::Failed => {
|
||||||
|
if let Some(outcome) = &status.final_outcome {
|
||||||
|
return Err(IrttError::ManagedFailure(
|
||||||
|
outcome.end_reason.clone()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(IrttError::ManagedAbandoned);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => return Err(IrttError::ManagedAbandoned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
ManagedEvent::Started => {
|
||||||
|
debug!("IRTT managed client started");
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::TargetStateChanged {
|
||||||
|
target,
|
||||||
|
lifecycle,
|
||||||
|
} => {
|
||||||
|
debug!("IRTT target {target:?}: {lifecycle:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Client { target, event } => {
|
||||||
|
let stream = target.id.as_str()
|
||||||
|
.parse::<u64>()
|
||||||
|
.ok()
|
||||||
|
.and_then(|id| {
|
||||||
|
self.bindings.get(&MeasurementStreamId(id))
|
||||||
|
})
|
||||||
|
.map(|binding| binding.stream);
|
||||||
|
|
||||||
|
match event {
|
||||||
|
ClientEvent::SessionStarted(session) => {
|
||||||
|
debug!(
|
||||||
|
"IRTT session started for {}: {}",
|
||||||
|
target.id,
|
||||||
|
session.remote
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::NoTestCompleted(outcome) => {
|
||||||
|
debug!(
|
||||||
|
"IRTT no-test completed for {}: {}",
|
||||||
|
target.id,
|
||||||
|
outcome.remote
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::SessionClosed { .. } => {
|
||||||
|
debug!("IRTT session closed for {}", target.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::EchoSent { seq, .. } => {
|
||||||
|
trace!("IRTT {} sent probe {seq}", target.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::EchoReply {
|
||||||
|
sent_at,
|
||||||
|
received_at,
|
||||||
|
one_way,
|
||||||
|
rtt,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let Some(stream) = stream else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let observation = MeasurementObservation {
|
||||||
|
stream,
|
||||||
|
started_at: sent_at.mono,
|
||||||
|
observed_at: received_at.mono,
|
||||||
|
round_trip: i64::try_from(rtt.effective.as_nanos())
|
||||||
|
.ok().map(SignedDuration::from_nanos),
|
||||||
|
one_way: one_way.and_then(map_one_way),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.observation_tx
|
||||||
|
.send_async(MeasurementEvent::Observation(observation))
|
||||||
|
.await
|
||||||
|
.map_err(|_| IrttError::EventChannelClosed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::EchoLoss {
|
||||||
|
sent_at,
|
||||||
|
timeout_at,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let Some(stream) = stream else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let loss = MeasurementLoss {
|
||||||
|
stream,
|
||||||
|
started_at: sent_at.mono,
|
||||||
|
detected_at: timeout_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.observation_tx
|
||||||
|
.send_async(MeasurementEvent::Loss(loss))
|
||||||
|
.await
|
||||||
|
.map_err(|_| IrttError::EventChannelClosed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::DuplicateReply { seq, .. } => {
|
||||||
|
trace!(
|
||||||
|
"IRTT {} duplicate reply {seq}",
|
||||||
|
target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::LateReply { seq, .. } => {
|
||||||
|
trace!(
|
||||||
|
"IRTT {} late reply {seq}",
|
||||||
|
target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::Warning { kind, message, .. } => {
|
||||||
|
debug!(
|
||||||
|
"IRTT {} warning ({kind:?}): {message}",
|
||||||
|
target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::TargetFinished { outcome } => {
|
||||||
|
match &outcome.end_reason {
|
||||||
|
ManagedTargetEndReason::Failed(failure) => {
|
||||||
|
warn!(
|
||||||
|
"IRTT target {} failed: {failure:?}",
|
||||||
|
outcome.target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reason => {
|
||||||
|
debug!(
|
||||||
|
"IRTT target {} finished: {reason:?}",
|
||||||
|
outcome.target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Stopping => {
|
||||||
|
debug!("IRTT managed client stopping");
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Completed { outcome } => {
|
||||||
|
debug!(
|
||||||
|
"IRTT managed client completed: {:?}",
|
||||||
|
outcome.end_reason
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Failed { outcome } => {
|
||||||
|
return Err(IrttError::ManagedFailure(
|
||||||
|
outcome.end_reason.clone()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Abandoned => {
|
||||||
|
return Err(IrttError::ManagedAbandoned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {
|
||||||
|
if SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn managed_client_config(
|
||||||
|
interval: std::time::Duration,
|
||||||
|
#[allow(unused)] settings: &NetworkSettings,
|
||||||
|
) -> ManagedClientConfig {
|
||||||
|
ManagedClientConfig {
|
||||||
|
client: ClientConfig {
|
||||||
|
socket: irtt_client::SocketConfig {
|
||||||
|
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||||
|
bind_to_device: settings.measurement_bind_device.clone(),
|
||||||
|
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||||
|
mark: settings.measurement_mark,
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
fib: settings.measurement_fib,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
request: irtt_client::SessionRequest {
|
||||||
|
duration: None,
|
||||||
|
interval,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
open: OpenPolicy {
|
||||||
|
negotiation: NegotiationPolicy::Strict,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
completion: ManagedCompletionPolicy::ExplicitStop,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_one_way(sample: irtt_client::OneWayDelaySample) -> Option<OneWayLatency> {
|
||||||
|
let uplink = i64::try_from(sample.client_to_server?.as_nanos()).ok()?;
|
||||||
|
|
||||||
|
let downlink = i64::try_from(sample.server_to_client?.as_nanos()).ok()?;
|
||||||
|
|
||||||
|
Some(OneWayLatency {
|
||||||
|
uplink: SignedDuration::from_nanos(uplink),
|
||||||
|
downlink: SignedDuration::from_nanos(downlink),
|
||||||
|
clock: OneWayClock::Uncalibrated,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub(crate) mod icmp;
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
pub(crate) mod irtt;
|
||||||
|
pub(crate) mod model;
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
use std::fmt::{self, Display};
|
||||||
|
use std::{
|
||||||
|
net::IpAddr,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct MeasurementStreamId(pub u64);
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum MeasurementSource {
|
||||||
|
IcmpEcho,
|
||||||
|
IcmpTimestamp,
|
||||||
|
Irtt,
|
||||||
|
TcpSeqAck,
|
||||||
|
TcpTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MeasurementSource {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
MeasurementSource::IcmpEcho => write!(f, "icmp-echo"),
|
||||||
|
MeasurementSource::IcmpTimestamp => write!(f, "icmp-timestamps"),
|
||||||
|
MeasurementSource::Irtt => write!(f, "irtt"),
|
||||||
|
MeasurementSource::TcpSeqAck => write!(f, "tcp-seqack"),
|
||||||
|
MeasurementSource::TcpTimestamp => write!(f, "tcp-timestamp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct MeasurementStream {
|
||||||
|
pub id: MeasurementStreamId,
|
||||||
|
pub peer: IpAddr,
|
||||||
|
pub source: MeasurementSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum OneWayClock {
|
||||||
|
Uncalibrated,
|
||||||
|
Calibrated,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ProbeSchedule {
|
||||||
|
pub period: Duration,
|
||||||
|
pub offset: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProbeSchedule {
|
||||||
|
pub(crate) fn next_probe_at(&self, origin: Instant, now: Instant) -> Instant {
|
||||||
|
const NANOS_PER_SEC: u128 = 1_000_000_000;
|
||||||
|
|
||||||
|
let first = origin + self.offset;
|
||||||
|
|
||||||
|
if now < first {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed_ns = now.duration_since(first).as_nanos();
|
||||||
|
let period_ns = self.period.as_nanos();
|
||||||
|
let remainder = elapsed_ns % period_ns;
|
||||||
|
|
||||||
|
let until_next = if remainder == 0 {
|
||||||
|
period_ns
|
||||||
|
} else {
|
||||||
|
period_ns - remainder
|
||||||
|
};
|
||||||
|
|
||||||
|
let until_next = Duration::new(
|
||||||
|
(until_next / NANOS_PER_SEC) as u64,
|
||||||
|
(until_next % NANOS_PER_SEC) as u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
now + until_next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct SignedDuration {
|
||||||
|
nanos: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl SignedDuration {
|
||||||
|
pub const fn from_nanos(nanos: i64) -> Self {
|
||||||
|
Self { nanos }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_millis(millis: i64) -> Self {
|
||||||
|
Self {
|
||||||
|
nanos: millis * 1_000_000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_nanos(self) -> i64 {
|
||||||
|
self.nanos
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_secs_f64(self) -> f64 {
|
||||||
|
self.nanos as f64 / 1_000_000_000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct OneWayLatency {
|
||||||
|
pub uplink: SignedDuration,
|
||||||
|
pub downlink: SignedDuration,
|
||||||
|
pub clock: OneWayClock,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct MeasurementObservation {
|
||||||
|
pub stream: MeasurementStream,
|
||||||
|
|
||||||
|
/// Local monotonic time at which measurement started.
|
||||||
|
pub started_at: Instant,
|
||||||
|
|
||||||
|
/// Local monotonic time at which the result was observed.
|
||||||
|
pub observed_at: Instant,
|
||||||
|
|
||||||
|
/// Measured round-trip latency when it differs from elapsed local time.
|
||||||
|
pub round_trip: Option<SignedDuration>,
|
||||||
|
|
||||||
|
/// Directional timing, when the measurement mechanism can provide it.
|
||||||
|
pub one_way: Option<OneWayLatency>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeasurementObservation {
|
||||||
|
pub fn rtt(&self) -> Duration {
|
||||||
|
self.observed_at.duration_since(self.started_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rtt_ms(&self) -> f64 {
|
||||||
|
self.round_trip
|
||||||
|
.map(SignedDuration::as_secs_f64)
|
||||||
|
.unwrap_or_else(|| self.rtt().as_secs_f64())
|
||||||
|
* 1000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct MeasurementLoss {
|
||||||
|
pub stream: MeasurementStream,
|
||||||
|
pub started_at: Instant,
|
||||||
|
pub detected_at: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum MeasurementEvent {
|
||||||
|
Observation(MeasurementObservation),
|
||||||
|
Loss(MeasurementLoss),
|
||||||
|
}
|
||||||
+32
-30
@@ -1,13 +1,17 @@
|
|||||||
use crate::config::{MeasurementType, ObservabilityProtocol};
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
use crate::time::Time;
|
//
|
||||||
use crate::Config;
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use crate::measurement::model::MeasurementSource;
|
||||||
|
use crate::settings::ObservabilityProtocol;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use crate::time::{ClockSample, Realtime};
|
||||||
|
use flume::{Receiver, RecvTimeoutError, Sender};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use rustix::time::ClockId;
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::net::{IpAddr, TcpStream, UdpSocket};
|
use std::net::{IpAddr, TcpStream, UdpSocket};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const MAX_RECONNECT_BACKOFF: u64 = 60;
|
const MAX_RECONNECT_BACKOFF: u64 = 60;
|
||||||
@@ -74,8 +78,10 @@ impl Transport {
|
|||||||
|
|
||||||
let full_data = format!("{}\n", data);
|
let full_data = format!("{}\n", data);
|
||||||
// take the stream out, write, put it back or set None on failure
|
// take the stream out, write, put it back or set None on failure
|
||||||
if let Some(s) = stream {
|
let write_error = stream
|
||||||
if let Err(e) = std::io::Write::write_all(s, full_data.as_bytes()) {
|
.as_mut()
|
||||||
|
.and_then(|s| std::io::Write::write_all(s, full_data.as_bytes()).err());
|
||||||
|
if let Some(e) = write_error {
|
||||||
warn!("TCP send failed: {}", e);
|
warn!("TCP send failed: {}", e);
|
||||||
*stream = None;
|
*stream = None;
|
||||||
*reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
|
*reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
|
||||||
@@ -83,13 +89,12 @@ impl Transport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Metric {
|
pub enum Metric {
|
||||||
Ping {
|
Ping {
|
||||||
reflector: IpAddr,
|
reflector: IpAddr,
|
||||||
measurement_type: MeasurementType,
|
measurement_type: MeasurementSource,
|
||||||
rtt: f64,
|
rtt: f64,
|
||||||
up_time: f64,
|
up_time: f64,
|
||||||
down_time: f64,
|
down_time: f64,
|
||||||
@@ -116,18 +121,18 @@ pub enum Metric {
|
|||||||
tags: &'static [(&'static str, &'static str)],
|
tags: &'static [(&'static str, &'static str)],
|
||||||
},
|
},
|
||||||
Dropped {
|
Dropped {
|
||||||
count: u64,
|
count: u32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MetricsSender {
|
pub struct MetricsSender {
|
||||||
tx: Option<SyncSender<(Metric, u64)>>,
|
tx: Option<Sender<(Metric, u64)>>,
|
||||||
dropped: Arc<AtomicU64>,
|
dropped: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetricsSender {
|
impl MetricsSender {
|
||||||
pub fn new(tx: SyncSender<(Metric, u64)>, dropped: Arc<AtomicU64>) -> Self {
|
pub fn new(tx: Sender<(Metric, u64)>, dropped: Arc<AtomicU32>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tx: Some(tx),
|
tx: Some(tx),
|
||||||
dropped,
|
dropped,
|
||||||
@@ -137,13 +142,13 @@ impl MetricsSender {
|
|||||||
pub fn disabled() -> Self {
|
pub fn disabled() -> Self {
|
||||||
Self {
|
Self {
|
||||||
tx: None,
|
tx: None,
|
||||||
dropped: Arc::new(AtomicU64::new(0)),
|
dropped: Arc::new(AtomicU32::new(0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&self, metric: Metric) {
|
pub fn send(&self, metric: Metric) {
|
||||||
if let Some(ref tx) = self.tx {
|
if let Some(ref tx) = self.tx {
|
||||||
let ts = Time::new(ClockId::Realtime).as_nanos();
|
let ts = ClockSample::<Realtime>::now().as_nanos();
|
||||||
if tx.try_send((metric, ts)).is_err() {
|
if tx.try_send((metric, ts)).is_err() {
|
||||||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
@@ -152,14 +157,14 @@ impl MetricsSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Metrics {
|
pub struct Metrics {
|
||||||
pub config: Config,
|
pub settings: Settings,
|
||||||
pub metrics_dropped: Arc<AtomicU64>,
|
pub metrics_dropped: Arc<AtomicU32>,
|
||||||
pub metrics_rx: Receiver<(Metric, u64)>,
|
pub metrics_rx: Receiver<(Metric, u64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metrics {
|
impl Metrics {
|
||||||
pub fn run(self) -> anyhow::Result<()> {
|
pub fn run(self) -> anyhow::Result<()> {
|
||||||
let host = match &self.config.observability_host {
|
let host = match &self.settings.observability.host {
|
||||||
Some(h) => h.clone(),
|
Some(h) => h.clone(),
|
||||||
None => {
|
None => {
|
||||||
error!("Observability host not configured - metrics exporter disabled");
|
error!("Observability host not configured - metrics exporter disabled");
|
||||||
@@ -167,12 +172,12 @@ impl Metrics {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let port = self.config.observability_port;
|
let port = self.settings.observability.port;
|
||||||
let batch_size = self.config.observability_batch_size;
|
let batch_size = self.settings.observability.batch_size;
|
||||||
let timeout = Duration::from_millis(self.config.observability_batch_timeout_ms);
|
let timeout = Duration::from_millis(self.settings.observability.batch_timeout_ms);
|
||||||
let host_tag = &self.config.observability_host_tag.clone();
|
let host_tag = &self.settings.observability.host_tag.clone();
|
||||||
|
|
||||||
let mut transport = match self.config.observability_protocol {
|
let mut transport = match self.settings.observability.protocol {
|
||||||
ObservabilityProtocol::Udp => {
|
ObservabilityProtocol::Udp => {
|
||||||
info!("Metrics exporter configured for UDP to {}:{}", host, port);
|
info!("Metrics exporter configured for UDP to {}:{}", host, port);
|
||||||
Transport::new_udp(&host, port)?
|
Transport::new_udp(&host, port)?
|
||||||
@@ -192,7 +197,7 @@ impl Metrics {
|
|||||||
Err(RecvTimeoutError::Timeout) => {
|
Err(RecvTimeoutError::Timeout) => {
|
||||||
let dropped = self.metrics_dropped.swap(0, Ordering::Relaxed);
|
let dropped = self.metrics_dropped.swap(0, Ordering::Relaxed);
|
||||||
if dropped > 0 {
|
if dropped > 0 {
|
||||||
let ts = Time::new(ClockId::Realtime).as_nanos();
|
let ts = ClockSample::<Realtime>::now().as_nanos();
|
||||||
batch.push((Metric::Dropped { count: dropped }, ts));
|
batch.push((Metric::Dropped { count: dropped }, ts));
|
||||||
}
|
}
|
||||||
if !batch.is_empty() {
|
if !batch.is_empty() {
|
||||||
@@ -205,11 +210,8 @@ impl Metrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// greedily drain without blocking
|
// greedily drain without blocking
|
||||||
loop {
|
while let Ok(metric) = self.metrics_rx.try_recv() {
|
||||||
match self.metrics_rx.try_recv() {
|
batch.push(metric);
|
||||||
Ok(metric) => batch.push(metric),
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
if batch.len() >= batch_size {
|
if batch.len() >= batch_size {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
-145
@@ -1,145 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
use std::str::Utf8Error;
|
|
||||||
|
|
||||||
use log::info;
|
|
||||||
use netlink_bindings::{rt_link, tc};
|
|
||||||
use netlink_socket2::NetlinkSocket;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum NetlinkError {
|
|
||||||
#[error("Couldn't find interface `{0}`")]
|
|
||||||
InterfaceNotFound(String),
|
|
||||||
|
|
||||||
#[error("Netlink error: {0}")]
|
|
||||||
Netlink(#[from] io::Error),
|
|
||||||
|
|
||||||
#[error("Netlink reply error: {0}")]
|
|
||||||
Reply(#[from] netlink_socket2::ReplyError),
|
|
||||||
|
|
||||||
#[error("Something went wrong while finding qdisc")]
|
|
||||||
NlQdiscError(String),
|
|
||||||
|
|
||||||
#[error("Couldn't find CAKE qdisc on interface `{0}`")]
|
|
||||||
NoQdiscFound(String),
|
|
||||||
|
|
||||||
#[error("Couldn't find interface statistics: `{0}`")]
|
|
||||||
NoInterfaceStatsFound(String),
|
|
||||||
|
|
||||||
#[error("Error happened while parsing UTF-8 string")]
|
|
||||||
Utf8Error(#[from] Utf8Error),
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Qdisc {
|
|
||||||
pub ifindex: i32,
|
|
||||||
pub parent: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Netlink {}
|
|
||||||
|
|
||||||
impl Netlink {
|
|
||||||
pub fn find_interface(ifname: &str) -> Result<i32, NetlinkError> {
|
|
||||||
let mut socket = NetlinkSocket::new();
|
|
||||||
|
|
||||||
let mut request = rt_link::Request::new()
|
|
||||||
.op_getlink_do_request(&Default::default());
|
|
||||||
request.encode().push_ifname_bytes(ifname.as_bytes());
|
|
||||||
|
|
||||||
let mut iter = socket.request(&request)?;
|
|
||||||
let (header, _) = iter.recv_one()
|
|
||||||
.map_err(|_| NetlinkError::InterfaceNotFound(ifname.to_string()))?;
|
|
||||||
Ok(header.ifi_index())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_interface_stats(ifname: &str) -> Result<(u64, u64), NetlinkError> {
|
|
||||||
let mut socket = NetlinkSocket::new();
|
|
||||||
|
|
||||||
let mut request = rt_link::Request::new()
|
|
||||||
.op_getlink_do_request(&Default::default());
|
|
||||||
request.encode()
|
|
||||||
.push_ifname_bytes(ifname.as_bytes())
|
|
||||||
.push_ext_mask(1 /* RTEXT_FILTER_VF */);
|
|
||||||
|
|
||||||
let mut iter = socket.request(&request)?;
|
|
||||||
while let Some(reply) = iter.recv() {
|
|
||||||
let (_, attrs) = reply?;
|
|
||||||
for attr in attrs {
|
|
||||||
if let Ok(rt_link::OpGetlinkDoReply::Stats64(stats)) = attr {
|
|
||||||
return Ok((stats.rx_bytes(), stats.tx_bytes()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(NetlinkError::NoInterfaceStatsFound(ifname.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn qdisc_from_ifindex(ifindex: i32, ifname: &str) -> Result<Qdisc, NetlinkError> {
|
|
||||||
let mut socket = NetlinkSocket::new();
|
|
||||||
let header = tc::PushTcmsg::new();
|
|
||||||
let request = tc::Request::new().op_getqdisc_dump_request(&header);
|
|
||||||
|
|
||||||
let mut iter = socket.request(&request)?;
|
|
||||||
while let Some(reply) = iter.recv() {
|
|
||||||
let (header, attrs) = reply?;
|
|
||||||
|
|
||||||
if header.ifindex() == ifindex {
|
|
||||||
let mut is_cake = false;
|
|
||||||
for attr in attrs {
|
|
||||||
if let Ok(tc::OpGetqdiscDumpReply::Kind(kind)) = attr {
|
|
||||||
if kind.to_str().map_err(|_| NetlinkError::NlQdiscError("Invalid UTF-8".to_string()))? == "cake" {
|
|
||||||
is_cake = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_cake {
|
|
||||||
return Ok(Qdisc {
|
|
||||||
ifindex,
|
|
||||||
parent: header.parent(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(NetlinkError::NoQdiscFound(ifname.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn qdisc_from_ifname(ifname: &str) -> Result<Qdisc, NetlinkError> {
|
|
||||||
let ifindex = Netlink::find_interface(ifname)?;
|
|
||||||
Netlink::qdisc_from_ifindex(ifindex, ifname)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_qdisc_rate(qdisc: Qdisc, bandwidth_kbit: u64, dry_run: bool) -> Result<(), NetlinkError> {
|
|
||||||
if dry_run {
|
|
||||||
info!(
|
|
||||||
"dry-run: would set qdisc ifindex={} to {} kbit/s",
|
|
||||||
qdisc.ifindex, bandwidth_kbit
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut socket = NetlinkSocket::new();
|
|
||||||
let bandwidth = bandwidth_kbit * 1000 / 8;
|
|
||||||
|
|
||||||
let mut header = tc::PushTcmsg::new();
|
|
||||||
header.set_ifindex(qdisc.ifindex);
|
|
||||||
header.set_parent(qdisc.parent);
|
|
||||||
|
|
||||||
let mut request = tc::Request::new()
|
|
||||||
.set_change()
|
|
||||||
.op_newqdisc_do_request(&header);
|
|
||||||
request.encode()
|
|
||||||
.push_kind(c"cake")
|
|
||||||
.nested_options_cake()
|
|
||||||
.push_base_rate64(bandwidth)
|
|
||||||
.end_nested();
|
|
||||||
|
|
||||||
let mut iter = socket.request(&request)?;
|
|
||||||
iter.recv_ack()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-176
@@ -1,176 +0,0 @@
|
|||||||
use crate::SHUTDOWN;
|
|
||||||
use crate::MeasurementType;
|
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
|
||||||
use crate::util::RwLockExt;
|
|
||||||
use icmp_socket2::socket::IcmpSocket;
|
|
||||||
use icmp_socket2::{IcmpSocket4, Icmpv4Packet};
|
|
||||||
use log::{debug, info, warn};
|
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use std::{io, thread};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum PingError {
|
|
||||||
#[error("Couldn't parse number")]
|
|
||||||
InvalidNumber(#[from] io::Error),
|
|
||||||
#[error("Error parsing packet")]
|
|
||||||
InvalidPacket(String),
|
|
||||||
#[error("Invalid packet type")]
|
|
||||||
InvalidType(String),
|
|
||||||
#[error("Wrong ICMP identifier (expected {expected:?}, found {found:?})")]
|
|
||||||
WrongID { expected: u16, found: u16 },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PingReply {
|
|
||||||
pub reflector: IpAddr,
|
|
||||||
pub measurement_type: MeasurementType,
|
|
||||||
pub seq: u16,
|
|
||||||
pub rtt: f64,
|
|
||||||
pub current_time: i64,
|
|
||||||
pub down_time: f64,
|
|
||||||
pub up_time: f64,
|
|
||||||
pub originate_timestamp: i64,
|
|
||||||
pub receive_timestamp: i64,
|
|
||||||
pub transmit_timestamp: i64,
|
|
||||||
pub last_receive_time_s: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_socket(type_: MeasurementType) -> io::Result<IcmpSocket4> {
|
|
||||||
match type_ {
|
|
||||||
MeasurementType::Icmp | MeasurementType::IcmpTimestamps => IcmpSocket4::new(),
|
|
||||||
_ => {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PingListener {
|
|
||||||
fn listen(
|
|
||||||
&mut self,
|
|
||||||
id: u16,
|
|
||||||
type_: MeasurementType,
|
|
||||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
|
||||||
stats_tx: Sender<PingReply>,
|
|
||||||
ping_metrics: MetricsSender,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let socket = &mut open_socket(type_)?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
|
||||||
info!("Ping listener shutting down");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let (pkt, sender) = match socket.rcv_from() {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let addr: IpAddr = sender.as_socket().unwrap().ip();
|
|
||||||
|
|
||||||
let reflectors = reflectors_lock.read_anyhow()?;
|
|
||||||
if !reflectors.contains(&addr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let reply = match self.parse_packet(id, addr, type_, pkt) {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(_) => {
|
|
||||||
// parse_packet will throw an error if it's an unknown protocol etc.
|
|
||||||
// so just quietly move on
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ping_metrics.send(Metric::Ping {
|
|
||||||
reflector: reply.reflector,
|
|
||||||
measurement_type: reply.measurement_type,
|
|
||||||
rtt: reply.rtt,
|
|
||||||
up_time: reply.up_time,
|
|
||||||
down_time: reply.down_time,
|
|
||||||
});
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Type: {:4} | Reflector IP: {:>15} | Seq: {:5} | Current time: {:8} | Originate: {:8} | Received time: {:8} | Transmit time : {:8} | RTT: {:8} | UL time: {:5} | DL time: {:5}",
|
|
||||||
"ICMP",
|
|
||||||
addr.to_string(),
|
|
||||||
reply.seq,
|
|
||||||
reply.current_time,
|
|
||||||
reply.originate_timestamp,
|
|
||||||
reply.receive_timestamp,
|
|
||||||
reply.transmit_timestamp,
|
|
||||||
reply.rtt,
|
|
||||||
reply.up_time,
|
|
||||||
reply.down_time
|
|
||||||
);
|
|
||||||
if let Err(e) = stats_tx.send(reply) {
|
|
||||||
warn!("Stats channel closed, stopping listener: {}", e);
|
|
||||||
break Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_packet(
|
|
||||||
&self,
|
|
||||||
id: u16,
|
|
||||||
reflector: IpAddr,
|
|
||||||
measurement_type: MeasurementType,
|
|
||||||
pkt: Icmpv4Packet,
|
|
||||||
) -> Result<PingReply, PingError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PingSender {
|
|
||||||
fn send(
|
|
||||||
&mut self,
|
|
||||||
id: u16,
|
|
||||||
type_: MeasurementType,
|
|
||||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
|
||||||
tick_interval: f64,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut socket = open_socket(type_)?;
|
|
||||||
|
|
||||||
let mut seq: u16 = 0;
|
|
||||||
let tick_duration_ms: u16 = (tick_interval * 1000.0) as u16;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
|
||||||
info!("Ping sender shutting down");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// Clone the reflectors vec and drop the read lock immediately —
|
|
||||||
// holding it for the entire tick would starve the reflector selector's write lock.
|
|
||||||
let reflectors_unlocked = reflectors_lock.read_anyhow()?;
|
|
||||||
let reflectors = reflectors_unlocked.clone();
|
|
||||||
drop(reflectors_unlocked);
|
|
||||||
|
|
||||||
if reflectors.is_empty() {
|
|
||||||
thread::sleep(Duration::from_millis(tick_duration_ms as u64));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sleep_duration =
|
|
||||||
Duration::from_millis((tick_duration_ms / reflectors.len() as u16) as u64);
|
|
||||||
|
|
||||||
for reflector in reflectors.iter() {
|
|
||||||
let addr: Ipv4Addr = match reflector {
|
|
||||||
IpAddr::V4(ipv4) => *ipv4,
|
|
||||||
IpAddr::V6(_) => unimplemented!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.send_to(addr, self.craft_packet(id, seq))?;
|
|
||||||
thread::sleep(sleep_duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
if seq == u16::MAX {
|
|
||||||
seq = 0;
|
|
||||||
} else {
|
|
||||||
seq += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn craft_packet(&self, id: u16, seq: u16) -> icmp_socket2::packet::Icmpv4Packet;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
use std::net::IpAddr;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use crate::config::MeasurementType;
|
|
||||||
use crate::pinger::{PingError, PingListener, PingReply, PingSender};
|
|
||||||
use crate::time::Time;
|
|
||||||
use icmp_socket2::Icmpv4Message;
|
|
||||||
use icmp_socket2::Icmpv4Packet;
|
|
||||||
use icmp_socket2::packet::WithEchoRequest;
|
|
||||||
use rustix::thread::ClockId;
|
|
||||||
|
|
||||||
pub struct PingerICMPEchoListener {}
|
|
||||||
|
|
||||||
pub struct PingerICMPEchoSender {}
|
|
||||||
|
|
||||||
impl PingListener for PingerICMPEchoListener {
|
|
||||||
// Result: RTT, down time, up time
|
|
||||||
fn parse_packet(
|
|
||||||
&self,
|
|
||||||
id: u16,
|
|
||||||
reflector: IpAddr,
|
|
||||||
measurement_type: MeasurementType,
|
|
||||||
pkt: Icmpv4Packet,
|
|
||||||
) -> Result<PingReply, PingError> {
|
|
||||||
match pkt.typ {
|
|
||||||
0 => {
|
|
||||||
if let Icmpv4Message::EchoReply {
|
|
||||||
identifier,
|
|
||||||
sequence,
|
|
||||||
payload,
|
|
||||||
} = pkt.message
|
|
||||||
{
|
|
||||||
if identifier != id {
|
|
||||||
return Err(PingError::WrongID {
|
|
||||||
expected: id,
|
|
||||||
found: identifier,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let time_sent = match payload.as_slice().try_into() {
|
|
||||||
Ok(bytes) => u64::from_be_bytes(bytes) as i64,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(PingError::InvalidPacket(format!(
|
|
||||||
"Expected 8 bytes payload, but found {}",
|
|
||||||
payload.len()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let clock = Time::new(ClockId::Monotonic);
|
|
||||||
let time_ms = clock.to_milliseconds() as i64;
|
|
||||||
|
|
||||||
let rtt = (time_ms - time_sent) as f64;
|
|
||||||
Ok(PingReply {
|
|
||||||
reflector,
|
|
||||||
measurement_type,
|
|
||||||
seq: sequence,
|
|
||||||
rtt,
|
|
||||||
current_time: time_ms,
|
|
||||||
down_time: (rtt / 2.0) as f64,
|
|
||||||
up_time: (rtt / 2.0) as f64,
|
|
||||||
originate_timestamp: time_sent,
|
|
||||||
receive_timestamp: 0,
|
|
||||||
transmit_timestamp: 0,
|
|
||||||
last_receive_time_s: Instant::now(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(PingError::InvalidPacket(format!(
|
|
||||||
"Packet had type {:?}, but did not match the structure",
|
|
||||||
pkt.typ
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type_ => Err(PingError::InvalidType(format!("{:?}", type_))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PingSender for PingerICMPEchoSender {
|
|
||||||
fn craft_packet(&self, id: u16, seq: u16) -> Icmpv4Packet {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
use crate::config::MeasurementType;
|
|
||||||
use crate::pinger::{PingError, PingListener, PingReply, PingSender};
|
|
||||||
use crate::time::Time;
|
|
||||||
use icmp_socket2::Icmpv4Message;
|
|
||||||
use icmp_socket2::Icmpv4Packet;
|
|
||||||
use icmp_socket2::packet::WithTimestampRequest;
|
|
||||||
use rustix::thread::ClockId;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
pub struct PingerICMPTimestampListener {}
|
|
||||||
|
|
||||||
pub struct PingerICMPTimestampSender {}
|
|
||||||
|
|
||||||
impl PingListener for PingerICMPTimestampListener {
|
|
||||||
// Result: RTT, down time, up time
|
|
||||||
fn parse_packet(
|
|
||||||
&self,
|
|
||||||
id: u16,
|
|
||||||
reflector: IpAddr,
|
|
||||||
measurement_type: MeasurementType,
|
|
||||||
pkt: Icmpv4Packet,
|
|
||||||
) -> Result<PingReply, PingError> {
|
|
||||||
match pkt.typ {
|
|
||||||
// 14 = Timestamp reply
|
|
||||||
14 => {
|
|
||||||
if let Icmpv4Message::TimestampReply {
|
|
||||||
identifier,
|
|
||||||
sequence,
|
|
||||||
originate,
|
|
||||||
receive,
|
|
||||||
transmit,
|
|
||||||
} = pkt.message
|
|
||||||
{
|
|
||||||
if identifier != id {
|
|
||||||
return Err(PingError::WrongID {
|
|
||||||
expected: id,
|
|
||||||
found: identifier,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let time_now = Time::new(ClockId::Realtime);
|
|
||||||
let time_since_midnight = time_now.get_time_since_midnight();
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
Ok(PingReply {
|
|
||||||
reflector,
|
|
||||||
measurement_type,
|
|
||||||
seq: sequence,
|
|
||||||
rtt,
|
|
||||||
current_time: time_since_midnight,
|
|
||||||
down_time: dl_time,
|
|
||||||
up_time: ul_time,
|
|
||||||
originate_timestamp: originate as i64,
|
|
||||||
receive_timestamp: receive as i64,
|
|
||||||
transmit_timestamp: transmit as i64,
|
|
||||||
last_receive_time_s: Instant::now(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(PingError::InvalidPacket(format!(
|
|
||||||
"Packet had type {:?}, but did not match the structure",
|
|
||||||
pkt.typ
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type_ => Err(PingError::InvalidType(format!("{:?}", type_))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PingSender for PingerICMPTimestampSender {
|
|
||||||
fn craft_packet(&self, id: u16, seq: u16) -> Icmpv4Packet {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,63 @@
|
|||||||
|
// 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(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,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."
|
||||||
|
);
|
||||||
|
}
|
||||||
+364
-208
@@ -1,21 +1,28 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
|
use crate::baseliner::ControlSnapshot;
|
||||||
|
use crate::measurement::model::MeasurementStream;
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::netlink::{Netlink, NetlinkError, Qdisc};
|
use crate::platform::{
|
||||||
use crate::time::Time;
|
InterfaceStatsProvider, PlatformInterfaceStats, PlatformTrafficControl, TrafficControlBackend,
|
||||||
use crate::util::{MutexExt, RwLockExt};
|
interface_stats_provider, traffic_control_backend,
|
||||||
use crate::{Config, ReflectorStats};
|
};
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use crate::time::{ClockSample, Realtime};
|
||||||
|
use crate::util::{ArcRwLock, RwLockExt};
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use rustix::thread::ClockId;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
enum Direction {
|
enum Direction {
|
||||||
@@ -23,56 +30,32 @@ enum Direction {
|
|||||||
Up,
|
Up,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
fn generate_initial_speeds(rng: &mut fastrand::Rng, base_speed: f64, size: u32) -> Vec<f64> {
|
||||||
pub enum RatecontrolError {
|
|
||||||
#[error("Netlink error")]
|
|
||||||
Netlink(#[from] NetlinkError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum StatsDirection {
|
|
||||||
RX,
|
|
||||||
TX,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_initial_speeds(base_speed: f64, size: u32) -> Vec<f64> {
|
|
||||||
let mut rates = Vec::new();
|
let mut rates = Vec::new();
|
||||||
|
|
||||||
for _ in 0..size {
|
for _ in 0..size {
|
||||||
rates.push((fastrand::f64() * 0.2 + 0.75) * base_speed);
|
rates.push((rng.f64() * 0.2 + 0.75) * base_speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
rates
|
rates
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_interface_stats(
|
fn get_interface_stats<S: InterfaceStatsProvider>(
|
||||||
config: &Config,
|
stats_provider: &mut S,
|
||||||
down_direction: StatsDirection,
|
upload_interface: &str,
|
||||||
up_direction: StatsDirection,
|
) -> Result<(i128, i128), S::Error> {
|
||||||
) -> Result<(i128, i128), RatecontrolError> {
|
let stats = stats_provider.read_stats(upload_interface)?;
|
||||||
let (down_rx, down_tx) = Netlink::get_interface_stats(config.download_interface.as_str())?;
|
|
||||||
let (up_rx, up_tx) = Netlink::get_interface_stats(config.upload_interface.as_str())?;
|
|
||||||
|
|
||||||
let rx_bytes = match down_direction {
|
Ok((stats.rx_bytes.into(), stats.tx_bytes.into()))
|
||||||
StatsDirection::RX => down_rx,
|
|
||||||
StatsDirection::TX => down_tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
let tx_bytes = match up_direction {
|
|
||||||
StatsDirection::RX => up_rx,
|
|
||||||
StatsDirection::TX => up_tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((rx_bytes.into(), tx_bytes.into()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct State {
|
struct State<H> {
|
||||||
current_bytes: i128,
|
current_bytes: i128,
|
||||||
current_rate: f64,
|
current_rate: f64,
|
||||||
delta_stat: f64,
|
delta_stat: f64,
|
||||||
deltas: Vec<f64>,
|
deltas: Vec<f64>,
|
||||||
qdisc: Qdisc,
|
shaper: H,
|
||||||
load: f64,
|
load: f64,
|
||||||
next_rate: f64,
|
next_rate: f64,
|
||||||
nrate: usize,
|
nrate: usize,
|
||||||
@@ -82,8 +65,8 @@ struct State {
|
|||||||
utilisation: f64,
|
utilisation: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl<H> State<H> {
|
||||||
fn new(qdisc: Qdisc, previous_bytes: i128, safe_rates: Vec<f64>) -> Self {
|
fn new(shaper: H, previous_bytes: i128, safe_rates: Vec<f64>) -> Self {
|
||||||
State {
|
State {
|
||||||
current_bytes: 0,
|
current_bytes: 0,
|
||||||
current_rate: 0.0,
|
current_rate: 0.0,
|
||||||
@@ -92,7 +75,7 @@ impl State {
|
|||||||
load: 0.0,
|
load: 0.0,
|
||||||
next_rate: 0.0,
|
next_rate: 0.0,
|
||||||
nrate: 0,
|
nrate: 0,
|
||||||
qdisc,
|
shaper,
|
||||||
previous_bytes,
|
previous_bytes,
|
||||||
prev_t: Instant::now(),
|
prev_t: Instant::now(),
|
||||||
safe_rates,
|
safe_rates,
|
||||||
@@ -101,33 +84,146 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Ratecontroller {
|
fn drain_latest_snapshot(
|
||||||
config: Config,
|
snapshot_rx: &Receiver<ControlSnapshot>,
|
||||||
down_direction: StatsDirection,
|
latest: &mut Option<ControlSnapshot>,
|
||||||
owd_baseline: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
) {
|
||||||
owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
while let Ok(snapshot) = snapshot_rx.try_recv() {
|
||||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
if latest
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|current| snapshot.generated_at >= current.generated_at)
|
||||||
|
{
|
||||||
|
*latest = Some(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deltas_from_snapshot(
|
||||||
|
snapshot: Option<&ControlSnapshot>,
|
||||||
|
reflectors: &[MeasurementStream],
|
||||||
|
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.peer, reflector.source, 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<MeasurementStream>>,
|
||||||
reselect_trigger: Sender<bool>,
|
reselect_trigger: Sender<bool>,
|
||||||
state_dl: State,
|
state_dl: State<T::Handle>,
|
||||||
state_ul: State,
|
state_ul: State<T::Handle>,
|
||||||
up_direction: StatsDirection,
|
rng: fastrand::Rng,
|
||||||
|
stats_provider: S,
|
||||||
|
traffic_control: T,
|
||||||
metrics: MetricsSender,
|
metrics: MetricsSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ratecontroller {
|
impl<S: InterfaceStatsProvider, T: TrafficControlBackend> Ratecontroller<S, T> {
|
||||||
|
fn new_with_backends(
|
||||||
|
settings: Settings,
|
||||||
|
control_snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
|
reflectors_lock: ArcRwLock<Vec<MeasurementStream>>,
|
||||||
|
reselect_trigger: Sender<bool>,
|
||||||
|
metrics: MetricsSender,
|
||||||
|
mut stats_provider: S,
|
||||||
|
mut traffic_control: T,
|
||||||
|
mut rng: fastrand::Rng,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let dl_shaper =
|
||||||
|
traffic_control.find_shaper(settings.network.download_interface.as_str())?;
|
||||||
|
let dl_safe_rates = generate_initial_speeds(
|
||||||
|
&mut rng,
|
||||||
|
settings.network.download_base_kbits,
|
||||||
|
settings.advanced_settings.speed_hist_size,
|
||||||
|
);
|
||||||
|
let ul_shaper = traffic_control.find_shaper(settings.network.upload_interface.as_str())?;
|
||||||
|
let ul_safe_rates = generate_initial_speeds(
|
||||||
|
&mut rng,
|
||||||
|
settings.network.upload_base_kbits,
|
||||||
|
settings.advanced_settings.speed_hist_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (cur_rx, cur_tx) =
|
||||||
|
get_interface_stats(&mut stats_provider, &settings.network.upload_interface)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
settings,
|
||||||
|
control_snapshot: None,
|
||||||
|
control_snapshot_rx,
|
||||||
|
reflectors_lock,
|
||||||
|
reselect_trigger,
|
||||||
|
state_dl: State::new(dl_shaper, cur_rx, dl_safe_rates),
|
||||||
|
state_ul: State::new(ul_shaper, cur_tx, ul_safe_rates),
|
||||||
|
rng,
|
||||||
|
stats_provider,
|
||||||
|
traffic_control,
|
||||||
|
metrics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_initial_rates(&mut self) -> anyhow::Result<()> {
|
||||||
|
// Set rates to 60% of base rate to make sure we start with sane baselines.
|
||||||
|
self.state_dl.current_rate = self.settings.network.download_base_kbits * 0.6;
|
||||||
|
self.state_ul.current_rate = self.settings.network.upload_base_kbits * 0.6;
|
||||||
|
|
||||||
|
self.traffic_control.set_rate(
|
||||||
|
&self.state_dl.shaper,
|
||||||
|
self.state_dl.current_rate.round() as u64,
|
||||||
|
self.settings.advanced_settings.dry_run,
|
||||||
|
)?;
|
||||||
|
self.traffic_control.set_rate(
|
||||||
|
&self.state_ul.shaper,
|
||||||
|
self.state_ul.current_rate.round() as u64,
|
||||||
|
self.settings.advanced_settings.dry_run,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn calculate_rate(&mut self, direction: Direction) -> anyhow::Result<()> {
|
fn calculate_rate(&mut self, direction: Direction) -> anyhow::Result<()> {
|
||||||
let (base_rate, delay_ms, min_rate, state) = if direction == Direction::Down {
|
let (base_rate, delay_ms, min_rate, state) = if direction == Direction::Down {
|
||||||
(
|
(
|
||||||
self.config.download_base_kbits,
|
self.settings.network.download_base_kbits,
|
||||||
self.config.download_delay_ms,
|
self.settings.advanced_settings.download_delay_ms,
|
||||||
self.config.download_min_kbits,
|
self.settings.network.download_min_kbits(),
|
||||||
&mut self.state_dl,
|
&mut self.state_dl,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
self.config.upload_base_kbits,
|
self.settings.network.upload_base_kbits,
|
||||||
self.config.upload_delay_ms,
|
self.settings.advanced_settings.upload_delay_ms,
|
||||||
self.config.upload_min_kbits,
|
self.settings.network.upload_min_kbits(),
|
||||||
&mut self.state_ul,
|
&mut self.state_ul,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -155,7 +251,9 @@ impl Ratecontroller {
|
|||||||
/ dur.as_secs_f64();
|
/ dur.as_secs_f64();
|
||||||
state.load = state.utilisation / state.current_rate;
|
state.load = state.utilisation / state.current_rate;
|
||||||
|
|
||||||
if state.delta_stat < delay_ms && state.load > self.config.high_load_level {
|
if state.delta_stat < delay_ms
|
||||||
|
&& state.load > self.settings.advanced_settings.high_load_level
|
||||||
|
{
|
||||||
state.safe_rates[state.nrate] = (state.current_rate * state.load).floor();
|
state.safe_rates[state.nrate] = (state.current_rate * state.load).floor();
|
||||||
let max_rate = state
|
let max_rate = state
|
||||||
.safe_rates
|
.safe_rates
|
||||||
@@ -166,13 +264,13 @@ impl Ratecontroller {
|
|||||||
* (1.0 + 0.1 * (1.0_f64 - state.current_rate / max_rate).max(0.0))
|
* (1.0 + 0.1 * (1.0_f64 - state.current_rate / max_rate).max(0.0))
|
||||||
+ (base_rate * 0.03);
|
+ (base_rate * 0.03);
|
||||||
state.nrate += 1;
|
state.nrate += 1;
|
||||||
state.nrate %= self.config.speed_hist_size as usize;
|
state.nrate %= self.settings.advanced_settings.speed_hist_size as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.delta_stat > delay_ms {
|
if state.delta_stat > delay_ms {
|
||||||
match state
|
match state
|
||||||
.safe_rates
|
.safe_rates
|
||||||
.get(fastrand::usize(..state.safe_rates.len()))
|
.get(self.rng.usize(..state.safe_rates.len()))
|
||||||
{
|
{
|
||||||
Some(rnd_rate) => {
|
Some(rnd_rate) => {
|
||||||
state.next_rate = rnd_rate.min(0.9 * state.current_rate * state.load);
|
state.next_rate = rnd_rate.min(0.9 * state.current_rate * state.load);
|
||||||
@@ -192,121 +290,26 @@ impl Ratecontroller {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_deltas(&mut self) -> anyhow::Result<()> {
|
|
||||||
let state_dl = &mut self.state_dl;
|
|
||||||
let state_ul = &mut self.state_ul;
|
|
||||||
|
|
||||||
state_dl.deltas.clear();
|
|
||||||
state_ul.deltas.clear();
|
|
||||||
|
|
||||||
let now_t = Instant::now();
|
|
||||||
let owd_baseline = self.owd_baseline.lock_anyhow()?;
|
|
||||||
let owd_recent = self.owd_recent.lock_anyhow()?;
|
|
||||||
let reflectors = self.reflectors_lock.read_anyhow()?;
|
|
||||||
|
|
||||||
for reflector in reflectors.iter() {
|
|
||||||
// only consider this data if it's less than 2 * tick_duration seconds old
|
|
||||||
if owd_baseline.contains_key(reflector)
|
|
||||||
&& owd_recent.contains_key(reflector)
|
|
||||||
&& now_t
|
|
||||||
.duration_since(owd_recent[reflector].last_receive_time_s)
|
|
||||||
.as_secs_f64()
|
|
||||||
< self.config.tick_interval * 2.0
|
|
||||||
{
|
|
||||||
state_dl
|
|
||||||
.deltas
|
|
||||||
.push(owd_recent[reflector].down_ewma - owd_baseline[reflector].down_ewma);
|
|
||||||
state_ul
|
|
||||||
.deltas
|
|
||||||
.push(owd_recent[reflector].up_ewma - owd_baseline[reflector].up_ewma);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Reflector: {} down_delay: {} up_delay: {}",
|
|
||||||
reflector,
|
|
||||||
state_dl.deltas.last().unwrap(),
|
|
||||||
state_ul.deltas.last().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort owd's lowest to highest
|
|
||||||
state_dl.deltas.sort_by(|a, b| a.total_cmp(b));
|
|
||||||
state_ul.deltas.sort_by(|a, b| a.total_cmp(b));
|
|
||||||
|
|
||||||
if state_dl.deltas.len() < 5 || state_ul.deltas.len() < 5 {
|
|
||||||
// trigger reselection
|
|
||||||
warn!("Not enough delta values, triggering reselection");
|
|
||||||
let _ = self.reselect_trigger.send(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
config: Config,
|
|
||||||
owd_baseline: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
|
||||||
owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
|
||||||
reflectors_lock: Arc<RwLock<Vec<IpAddr>>>,
|
|
||||||
reselect_trigger: Sender<bool>,
|
|
||||||
down_direction: StatsDirection,
|
|
||||||
up_direction: StatsDirection,
|
|
||||||
metrics: MetricsSender,
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
let dl_qdisc = Netlink::qdisc_from_ifname(config.download_interface.as_str())?;
|
|
||||||
let dl_safe_rates =
|
|
||||||
generate_initial_speeds(config.download_base_kbits, config.speed_hist_size);
|
|
||||||
let ul_qdisc = Netlink::qdisc_from_ifname(config.upload_interface.as_str())?;
|
|
||||||
let ul_safe_rates =
|
|
||||||
generate_initial_speeds(config.upload_base_kbits, config.speed_hist_size);
|
|
||||||
|
|
||||||
let (cur_rx, cur_tx) = get_interface_stats(&config, down_direction, up_direction)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
down_direction,
|
|
||||||
owd_baseline,
|
|
||||||
owd_recent,
|
|
||||||
reflectors_lock,
|
|
||||||
reselect_trigger,
|
|
||||||
state_dl: State::new(dl_qdisc, cur_rx, dl_safe_rates),
|
|
||||||
state_ul: State::new(ul_qdisc, cur_tx, ul_safe_rates),
|
|
||||||
up_direction,
|
|
||||||
metrics,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(&mut self) -> anyhow::Result<()> {
|
pub fn run(&mut self) -> anyhow::Result<()> {
|
||||||
let sleep_time = Duration::from_secs_f64(self.config.min_change_interval);
|
let sleep_time =
|
||||||
|
Duration::from_secs_f64(self.settings.advanced_settings.min_change_interval);
|
||||||
|
|
||||||
let mut lastchg_t = Instant::now();
|
let mut lastchg_t = Instant::now();
|
||||||
let mut lastdump_t = Instant::now();
|
let mut lastdump_t = Instant::now();
|
||||||
|
|
||||||
// set qdisc rates to 60% of base rate to make sure we start with sane baselines
|
self.request_initial_rates()?;
|
||||||
self.state_dl.current_rate = self.config.download_base_kbits * 0.6;
|
|
||||||
self.state_ul.current_rate = self.config.upload_base_kbits * 0.6;
|
|
||||||
|
|
||||||
Netlink::set_qdisc_rate(
|
|
||||||
self.state_dl.qdisc,
|
|
||||||
self.state_dl.current_rate.round() as u64,
|
|
||||||
self.config.dry_run,
|
|
||||||
)?;
|
|
||||||
Netlink::set_qdisc_rate(
|
|
||||||
self.state_ul.qdisc,
|
|
||||||
self.state_ul.current_rate.round() as u64,
|
|
||||||
self.config.dry_run,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut speed_hist_fd: Option<File> = None;
|
let mut speed_hist_fd: Option<File> = None;
|
||||||
let mut speed_hist_fd_inner: File;
|
let mut speed_hist_fd_inner: File;
|
||||||
let mut stats_fd: Option<File> = None;
|
let mut stats_fd: Option<File> = None;
|
||||||
let mut stats_fd_inner: File;
|
let mut stats_fd_inner: File;
|
||||||
|
|
||||||
if !self.config.suppress_statistics {
|
if !self.settings.output.suppress_statistics {
|
||||||
speed_hist_fd_inner = File::options()
|
speed_hist_fd_inner = File::options()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.open(self.config.speed_hist_file.as_str())?;
|
.open(self.settings.output.speed_hist_file.as_str())?;
|
||||||
|
|
||||||
speed_hist_fd_inner.write_all("time,counter,upspeed,downspeed\n".as_bytes())?;
|
speed_hist_fd_inner.write_all("time,counter,upspeed,downspeed\n".as_bytes())?;
|
||||||
speed_hist_fd_inner.flush()?;
|
speed_hist_fd_inner.flush()?;
|
||||||
@@ -317,7 +320,7 @@ impl Ratecontroller {
|
|||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.open(self.config.stats_file.as_str())?;
|
.open(self.settings.output.stats_file.as_str())?;
|
||||||
|
|
||||||
stats_fd_inner.write_all(
|
stats_fd_inner.write_all(
|
||||||
"times,timens,rxload,txload,deltadelaydown,deltadelayup,dlrate,uprate\n".as_bytes(),
|
"times,timens,rxload,txload,deltadelaydown,deltadelayup,dlrate,uprate\n".as_bytes(),
|
||||||
@@ -335,25 +338,16 @@ impl Ratecontroller {
|
|||||||
sleep(sleep_time);
|
sleep(sleep_time);
|
||||||
let now_t = Instant::now();
|
let now_t = Instant::now();
|
||||||
|
|
||||||
if now_t.duration_since(lastchg_t).as_secs_f64() > self.config.min_change_interval {
|
if now_t.duration_since(lastchg_t).as_secs_f64()
|
||||||
|
> self.settings.advanced_settings.min_change_interval
|
||||||
|
{
|
||||||
// if it's been long enough, and the stats indicate needing to change speeds
|
// if it's been long enough, and the stats indicate needing to change speeds
|
||||||
// change speeds here
|
// change speeds here
|
||||||
|
|
||||||
(self.state_dl.current_bytes, self.state_ul.current_bytes) =
|
(self.state_dl.current_bytes, self.state_ul.current_bytes) = get_interface_stats(
|
||||||
get_interface_stats(&self.config, self.down_direction, self.up_direction)?;
|
&mut self.stats_provider,
|
||||||
if self.state_dl.current_bytes == -1 || self.state_ul.current_bytes == -1 {
|
&self.settings.network.upload_interface,
|
||||||
warn!(
|
)?;
|
||||||
"One or both Netlink stats could not be read. Skipping rate control algorithm"
|
|
||||||
);
|
|
||||||
self.metrics.send(Metric::Event {
|
|
||||||
name: "netlink_read_failure",
|
|
||||||
reason: "",
|
|
||||||
reflector: None,
|
|
||||||
tags: &[],
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_deltas()?;
|
self.update_deltas()?;
|
||||||
|
|
||||||
if self.state_dl.deltas.is_empty() || self.state_ul.deltas.is_empty() {
|
if self.state_dl.deltas.is_empty() || self.state_ul.deltas.is_empty() {
|
||||||
@@ -364,18 +358,18 @@ impl Ratecontroller {
|
|||||||
reflector: None,
|
reflector: None,
|
||||||
tags: &[],
|
tags: &[],
|
||||||
});
|
});
|
||||||
self.state_dl.next_rate = self.config.download_min_kbits;
|
self.state_dl.next_rate = self.settings.network.download_min_kbits();
|
||||||
self.state_ul.next_rate = self.config.upload_min_kbits;
|
self.state_ul.next_rate = self.settings.network.upload_min_kbits();
|
||||||
|
|
||||||
Netlink::set_qdisc_rate(
|
self.traffic_control.set_rate(
|
||||||
self.state_dl.qdisc,
|
&self.state_dl.shaper,
|
||||||
self.state_dl.next_rate as u64,
|
self.state_dl.next_rate as u64,
|
||||||
self.config.dry_run,
|
self.settings.advanced_settings.dry_run,
|
||||||
)?;
|
)?;
|
||||||
Netlink::set_qdisc_rate(
|
self.traffic_control.set_rate(
|
||||||
self.state_ul.qdisc,
|
&self.state_ul.shaper,
|
||||||
self.state_ul.next_rate as u64,
|
self.state_ul.next_rate as u64,
|
||||||
self.config.dry_run,
|
self.settings.advanced_settings.dry_run,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.state_dl.current_rate = self.state_dl.next_rate;
|
self.state_dl.current_rate = self.state_dl.next_rate;
|
||||||
@@ -390,31 +384,31 @@ impl Ratecontroller {
|
|||||||
|| self.state_ul.next_rate != self.state_ul.current_rate
|
|| self.state_ul.next_rate != self.state_ul.current_rate
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"self.state_ul.next_rate {} self.state_dl.next_rate {}",
|
"Requesting rates (D/U): {} / {} kbit/s",
|
||||||
self.state_ul.next_rate, self.state_dl.next_rate
|
self.state_dl.next_rate as u64, self.state_ul.next_rate as u64
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.state_dl.next_rate != self.state_dl.current_rate {
|
if self.state_dl.next_rate != self.state_dl.current_rate {
|
||||||
Netlink::set_qdisc_rate(
|
self.traffic_control.set_rate(
|
||||||
self.state_dl.qdisc,
|
&self.state_dl.shaper,
|
||||||
self.state_dl.next_rate as u64,
|
self.state_dl.next_rate as u64,
|
||||||
self.config.dry_run,
|
self.settings.advanced_settings.dry_run,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.state_ul.next_rate != self.state_ul.current_rate {
|
if self.state_ul.next_rate != self.state_ul.current_rate {
|
||||||
Netlink::set_qdisc_rate(
|
self.traffic_control.set_rate(
|
||||||
self.state_ul.qdisc,
|
&self.state_ul.shaper,
|
||||||
self.state_ul.next_rate as u64,
|
self.state_ul.next_rate as u64,
|
||||||
self.config.dry_run,
|
self.settings.advanced_settings.dry_run,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state_dl.current_rate = self.state_dl.next_rate;
|
self.state_dl.current_rate = self.state_dl.next_rate;
|
||||||
self.state_ul.current_rate = self.state_ul.next_rate;
|
self.state_ul.current_rate = self.state_ul.next_rate;
|
||||||
|
|
||||||
let stats_time = Time::new(ClockId::Realtime);
|
let stats_time = ClockSample::<Realtime>::now();
|
||||||
debug!(
|
debug!(
|
||||||
"{},{},{},{},{},{},{},{}",
|
"{},{},{},{},{},{},{},{}",
|
||||||
stats_time.secs(),
|
stats_time.secs(),
|
||||||
@@ -436,8 +430,8 @@ impl Ratecontroller {
|
|||||||
delta_delay_up: self.state_ul.delta_stat,
|
delta_delay_up: self.state_ul.delta_stat,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(ref mut fd) = stats_fd {
|
let stats_write_error = stats_fd.as_mut().and_then(|fd| {
|
||||||
if let Err(e) = fd.write_all(
|
fd.write_all(
|
||||||
format!(
|
format!(
|
||||||
"{},{},{},{},{},{},{},{}\n",
|
"{},{},{},{},{},{},{},{}\n",
|
||||||
stats_time.secs(),
|
stats_time.secs(),
|
||||||
@@ -450,18 +444,21 @@ impl Ratecontroller {
|
|||||||
self.state_ul.current_rate as u64
|
self.state_ul.current_rate as u64
|
||||||
)
|
)
|
||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
) {
|
)
|
||||||
|
.err()
|
||||||
|
});
|
||||||
|
if let Some(e) = stats_write_error {
|
||||||
warn!("Failed to write statistics: {}", e);
|
warn!("Failed to write statistics: {}", e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
lastchg_t = now_t;
|
lastchg_t = now_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mut fd) = speed_hist_fd {
|
if let Some(ref mut fd) = speed_hist_fd
|
||||||
if now_t.duration_since(lastdump_t).as_secs_f64() > 300.0 {
|
&& now_t.duration_since(lastdump_t).as_secs_f64() > 300.0
|
||||||
for i in 0..self.config.speed_hist_size as usize {
|
{
|
||||||
let hist_time = Time::new(ClockId::Realtime);
|
for i in 0..self.settings.advanced_settings.speed_hist_size as usize {
|
||||||
|
let hist_time = ClockSample::<Realtime>::now();
|
||||||
if let Err(e) = fd.write_all(
|
if let Err(e) = fd.write_all(
|
||||||
format!(
|
format!(
|
||||||
"{},{},{},{}\n",
|
"{},{},{},{}\n",
|
||||||
@@ -480,5 +477,164 @@ 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;
|
||||||
|
|
||||||
|
let minimum = usize::from(self.settings.reflectors.num_reflectors).min(reflectors.len());
|
||||||
|
if reflectors.is_empty()
|
||||||
|
|| self.state_dl.deltas.len() < minimum
|
||||||
|
|| self.state_ul.deltas.len() < minimum
|
||||||
|
{
|
||||||
|
// trigger reselection
|
||||||
|
warn!(
|
||||||
|
"Not enough delta values (D: {}, U: {}, need {}), triggering reselection",
|
||||||
|
self.state_dl.deltas.len(),
|
||||||
|
self.state_ul.deltas.len(),
|
||||||
|
minimum
|
||||||
|
);
|
||||||
|
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<MeasurementStream>>,
|
||||||
|
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 crate::measurement::model::{MeasurementSource, MeasurementStreamId};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
fn stream(id: u64, peer: IpAddr) -> MeasurementStream {
|
||||||
|
MeasurementStream {
|
||||||
|
id: MeasurementStreamId(id),
|
||||||
|
peer,
|
||||||
|
source: MeasurementSource::IcmpTimestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = stream(0, "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[&stream(0, "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 = stream(0, "192.0.2.1".parse().unwrap());
|
||||||
|
let second = stream(1, "192.0.2.2".parse().unwrap());
|
||||||
|
let stale = stream(2, "192.0.2.3".parse().unwrap());
|
||||||
|
let inactive = stream(3, "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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+331
-37
@@ -1,21 +1,123 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
|
use crate::baseliner::ControlSnapshot;
|
||||||
|
use crate::measurement::model::MeasurementStream;
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::util::{MutexExt, RwLockExt};
|
use crate::settings::Settings;
|
||||||
use crate::{Config, ReflectorStats};
|
use crate::util::RwLockExt;
|
||||||
|
use flume::{Receiver, RecvError, RecvTimeoutError, Selector, Sender};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::collections::HashMap;
|
use std::net::SocketAddr;
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::{Arc, RwLock};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
enum SelectorEvent {
|
||||||
|
Trigger(Result<bool, RecvError>),
|
||||||
|
Snapshot(Result<ControlSnapshot, RecvError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
enum WaitOutcome {
|
||||||
|
Triggered,
|
||||||
|
DeadlineReached,
|
||||||
|
ChannelsClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_trigger_or_deadline(
|
||||||
|
trigger_rx: &Receiver<bool>,
|
||||||
|
snapshot_rx: &Receiver<ControlSnapshot>,
|
||||||
|
latest_snapshot: &mut Option<ControlSnapshot>,
|
||||||
|
deadline: Instant,
|
||||||
|
) -> WaitOutcome {
|
||||||
|
let mut trigger_connected = true;
|
||||||
|
let mut snapshot_connected = true;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
return WaitOutcome::DeadlineReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = match (trigger_connected, snapshot_connected) {
|
||||||
|
(true, true) => Selector::new()
|
||||||
|
.recv(trigger_rx, SelectorEvent::Trigger)
|
||||||
|
.recv(snapshot_rx, SelectorEvent::Snapshot)
|
||||||
|
.wait_deadline(deadline)
|
||||||
|
.ok(),
|
||||||
|
(true, false) => match trigger_rx.recv_deadline(deadline) {
|
||||||
|
Ok(trigger) => Some(SelectorEvent::Trigger(Ok(trigger))),
|
||||||
|
Err(RecvTimeoutError::Timeout) => None,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
Some(SelectorEvent::Trigger(Err(RecvError::Disconnected)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(false, true) => match snapshot_rx.recv_deadline(deadline) {
|
||||||
|
Ok(snapshot) => Some(SelectorEvent::Snapshot(Ok(snapshot))),
|
||||||
|
Err(RecvTimeoutError::Timeout) => None,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
Some(SelectorEvent::Snapshot(Err(RecvError::Disconnected)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(false, false) => return WaitOutcome::ChannelsClosed,
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Some(SelectorEvent::Trigger(Ok(_))) => return WaitOutcome::Triggered,
|
||||||
|
Some(SelectorEvent::Trigger(Err(_))) => trigger_connected = false,
|
||||||
|
Some(SelectorEvent::Snapshot(Ok(snapshot))) => *latest_snapshot = Some(snapshot),
|
||||||
|
Some(SelectorEvent::Snapshot(Err(_))) => snapshot_connected = false,
|
||||||
|
None => return WaitOutcome::DeadlineReached,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_snapshots_until(
|
||||||
|
snapshot_rx: &Receiver<ControlSnapshot>,
|
||||||
|
latest_snapshot: &mut Option<ControlSnapshot>,
|
||||||
|
deadline: Instant,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match snapshot_rx.recv_deadline(deadline) {
|
||||||
|
Ok(snapshot) => *latest_snapshot = Some(snapshot),
|
||||||
|
Err(RecvTimeoutError::Timeout) => return,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
sleep(deadline.saturating_duration_since(Instant::now()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recent_rtt_for_peer(
|
||||||
|
snapshot: Option<&ControlSnapshot>,
|
||||||
|
stream: Option<&MeasurementStream>,
|
||||||
|
now: Instant,
|
||||||
|
max_age: Duration,
|
||||||
|
) -> Option<u64> {
|
||||||
|
snapshot
|
||||||
|
.and_then(|snapshot| stream.and_then(|stream| snapshot.reflectors.get(stream)))
|
||||||
|
.filter(|state| now.saturating_duration_since(state.last_receive_at) < max_age)
|
||||||
|
.map(|state| (state.recent.down + state.recent.up) as u64)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ReflectorSelector {
|
pub struct ReflectorSelector {
|
||||||
pub config: Config,
|
pub settings: Settings,
|
||||||
pub owd_recent: Arc<Mutex<HashMap<IpAddr, ReflectorStats>>>,
|
pub snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
pub reflector_peers_lock: Arc<RwLock<Vec<IpAddr>>>,
|
pub reflector_peers_lock: Arc<RwLock<Vec<SocketAddr>>>,
|
||||||
pub reflector_pool: Vec<IpAddr>,
|
pub reflector_tx: Sender<Vec<SocketAddr>>,
|
||||||
|
pub reflector_pool: Vec<SocketAddr>,
|
||||||
|
pub endpoint_streams: Arc<RwLock<std::collections::HashMap<SocketAddr, MeasurementStream>>>,
|
||||||
pub trigger_channel: Receiver<bool>,
|
pub trigger_channel: Receiver<bool>,
|
||||||
pub metrics: MetricsSender,
|
pub metrics: MetricsSender,
|
||||||
}
|
}
|
||||||
@@ -24,25 +126,36 @@ impl ReflectorSelector {
|
|||||||
pub fn run(&self) -> anyhow::Result<()> {
|
pub fn run(&self) -> anyhow::Result<()> {
|
||||||
let mut selector_sleep_time = Duration::new(30, 0);
|
let mut selector_sleep_time = Duration::new(30, 0);
|
||||||
let mut reselection_count = 0;
|
let mut reselection_count = 0;
|
||||||
let baseline_sleep_time =
|
let mut latest_snapshot = None;
|
||||||
Duration::from_secs_f64(self.config.tick_interval * std::f64::consts::PI);
|
let baseline_sleep_time = Duration::from_secs_f64(
|
||||||
|
self.settings.advanced_settings.tick_interval * std::f64::consts::PI,
|
||||||
|
);
|
||||||
// Initial wait of several seconds to allow some OWD data to build up
|
// Initial wait of several seconds to allow some OWD data to build up
|
||||||
sleep(baseline_sleep_time);
|
receive_snapshots_until(
|
||||||
|
&self.snapshot_rx,
|
||||||
|
&mut latest_snapshot,
|
||||||
|
Instant::now() + baseline_sleep_time,
|
||||||
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if SHUTDOWN.load(Ordering::Relaxed) {
|
if SHUTDOWN.load(Ordering::Relaxed) {
|
||||||
info!("Reflector selector shutting down");
|
info!("Reflector selector shutting down");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
* Selection is triggered either by some other thread triggering it through the channel,
|
let triggered = match wait_for_trigger_or_deadline(
|
||||||
* or it passes the timeout. In any case we don't care about the result of this function,
|
&self.trigger_channel,
|
||||||
* so we ignore the result of it.
|
&self.snapshot_rx,
|
||||||
*/
|
&mut latest_snapshot,
|
||||||
let triggered = self
|
Instant::now() + selector_sleep_time,
|
||||||
.trigger_channel
|
) {
|
||||||
.recv_timeout(selector_sleep_time)
|
WaitOutcome::Triggered => true,
|
||||||
.is_ok();
|
WaitOutcome::DeadlineReached => false,
|
||||||
|
WaitOutcome::ChannelsClosed => {
|
||||||
|
info!("Reflector selector channels closed, shutting down");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
reselection_count += 1;
|
reselection_count += 1;
|
||||||
info!("Starting reselection [#{}]", reselection_count);
|
info!("Starting reselection [#{}]", reselection_count);
|
||||||
self.metrics.send(Metric::Event {
|
self.metrics.send(Metric::Event {
|
||||||
@@ -54,31 +167,37 @@ impl ReflectorSelector {
|
|||||||
|
|
||||||
// After 40 reselections, slow down to every 15 minutes
|
// After 40 reselections, slow down to every 15 minutes
|
||||||
if reselection_count > 40 {
|
if reselection_count > 40 {
|
||||||
selector_sleep_time = Duration::new(self.config.peer_reselection_time * 60, 0);
|
selector_sleep_time =
|
||||||
|
Duration::new(self.settings.reflectors.peer_reselection_time * 60, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut next_peers: Vec<IpAddr> = Vec::new();
|
let mut next_peers: Vec<SocketAddr> = Vec::new();
|
||||||
let mut reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
let mut reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
||||||
|
let current_peers = reflectors_peers.clone();
|
||||||
|
|
||||||
// Include all current peers
|
// Include all current peers
|
||||||
for reflector in reflectors_peers.iter() {
|
for reflector in reflectors_peers.iter() {
|
||||||
debug!("Current peer: {}", reflector.to_string());
|
debug!("Current peer: {}", reflector);
|
||||||
next_peers.push(*reflector);
|
next_peers.push(*reflector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.reflector_pool.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
let next_candidate =
|
let next_candidate =
|
||||||
&self.reflector_pool[fastrand::usize(..self.reflector_pool.len())];
|
&self.reflector_pool[fastrand::usize(..self.reflector_pool.len())];
|
||||||
if next_peers.contains(next_candidate) {
|
if next_peers.contains(next_candidate) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
debug!("Next candidate: {}", next_candidate.to_string());
|
debug!("Next candidate: {}", next_candidate);
|
||||||
next_peers.push(*next_candidate);
|
next_peers.push(*next_candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone next_peers because we need it again after the baseline sleep
|
// Clone next_peers because we need it again after the baseline sleep
|
||||||
// to iterate over candidates for RTT measurement.
|
// to iterate over candidates for RTT measurement.
|
||||||
*reflectors_peers = next_peers.clone();
|
*reflectors_peers = next_peers.clone();
|
||||||
|
self.reflector_tx.send(next_peers.clone())?;
|
||||||
|
|
||||||
// Drop the MutexGuard explicitly, as Rust won't unlock the mutex by default
|
// Drop the MutexGuard explicitly, as Rust won't unlock the mutex by default
|
||||||
// until the guard goes out of scope
|
// until the guard goes out of scope
|
||||||
@@ -86,23 +205,31 @@ impl ReflectorSelector {
|
|||||||
|
|
||||||
debug!("Waiting for candidates to be baselined");
|
debug!("Waiting for candidates to be baselined");
|
||||||
// Wait for several seconds to allow all reflectors to be re-baselined
|
// Wait for several seconds to allow all reflectors to be re-baselined
|
||||||
sleep(baseline_sleep_time);
|
receive_snapshots_until(
|
||||||
|
&self.snapshot_rx,
|
||||||
|
&mut latest_snapshot,
|
||||||
|
Instant::now() + baseline_sleep_time,
|
||||||
|
);
|
||||||
|
|
||||||
// Re-acquire the lock when we wake up again
|
// Re-acquire the lock when we wake up again
|
||||||
reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
||||||
|
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
let owd_recent = self.owd_recent.lock_anyhow()?;
|
|
||||||
|
|
||||||
for peer in next_peers {
|
for peer in next_peers {
|
||||||
if owd_recent.contains_key(&peer) {
|
let stream = self.endpoint_streams.read_anyhow()?.get(&peer).copied();
|
||||||
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(),
|
||||||
|
stream.as_ref(),
|
||||||
|
Instant::now(),
|
||||||
|
Duration::from_secs_f64(self.settings.advanced_settings.tick_interval * 2.0),
|
||||||
|
) {
|
||||||
candidates.push((peer, rtt));
|
candidates.push((peer, rtt));
|
||||||
info!("Candidate reflector: {} RTT: {}", peer.to_string(), rtt);
|
info!("Candidate reflector: {} RTT: {}", peer, rtt);
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
"No data found from candidate reflector: {} - skipping",
|
"No data found from candidate reflector: {} - skipping",
|
||||||
peer.to_string()
|
peer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +238,7 @@ impl ReflectorSelector {
|
|||||||
candidates.sort_by(|a, b| a.1.cmp(&b.1));
|
candidates.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
|
||||||
// Now we will just limit the candidates down to 2 * num_reflectors
|
// Now we will just limit the candidates down to 2 * num_reflectors
|
||||||
let mut num_reflectors = self.config.num_reflectors;
|
let num_reflectors = self.settings.reflectors.num_reflectors;
|
||||||
let candidate_pool_num = (2 * num_reflectors) as usize;
|
let candidate_pool_num = (2 * num_reflectors) as usize;
|
||||||
candidates.truncate(candidate_pool_num);
|
candidates.truncate(candidate_pool_num);
|
||||||
|
|
||||||
@@ -125,10 +252,21 @@ impl ReflectorSelector {
|
|||||||
candidates.swap(i, j);
|
candidates.swap(i, j);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidates.len() as u8) < num_reflectors {
|
if candidates.is_empty() {
|
||||||
num_reflectors = candidates.len() as u8;
|
info!("No usable reflectors, retaining current set");
|
||||||
|
self.reflector_tx.send(current_peers.clone())?;
|
||||||
|
*reflectors_peers = current_peers;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (candidates.len() as u8) < num_reflectors {
|
||||||
|
info!(
|
||||||
|
"Only {} usable reflectors, selecting available candidates",
|
||||||
|
candidates.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let num_reflectors = num_reflectors.min(candidates.len() as u8);
|
||||||
|
|
||||||
let mut new_peers = Vec::new();
|
let mut new_peers = Vec::new();
|
||||||
for i in 0..num_reflectors {
|
for i in 0..num_reflectors {
|
||||||
let peer = candidates[i as usize].0;
|
let peer = candidates[i as usize].0;
|
||||||
@@ -137,12 +275,168 @@ impl ReflectorSelector {
|
|||||||
self.metrics.send(Metric::Event {
|
self.metrics.send(Metric::Event {
|
||||||
name: "reflector_selected",
|
name: "reflector_selected",
|
||||||
reason: "",
|
reason: "",
|
||||||
reflector: Some(peer),
|
reflector: Some(peer.ip()),
|
||||||
tags: &[],
|
tags: &[],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.reflector_tx.send(new_peers.clone())?;
|
||||||
*reflectors_peers = new_peers;
|
*reflectors_peers = new_peers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::baseliner::{EwmaStats, ReflectorState};
|
||||||
|
use crate::measurement::model::MeasurementSource;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
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(
|
||||||
|
crate::measurement::model::MeasurementStream {
|
||||||
|
id: crate::measurement::model::MeasurementStreamId(0),
|
||||||
|
peer,
|
||||||
|
source: MeasurementSource::IcmpTimestamp,
|
||||||
|
},
|
||||||
|
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: IpAddr = "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(),
|
||||||
|
latest_snapshot.as_ref().unwrap().reflectors.keys().next(),
|
||||||
|
generated_at + Duration::from_secs(1),
|
||||||
|
Duration::from_secs(2)
|
||||||
|
),
|
||||||
|
Some(42)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use config::{Config, ConfigError, Environment};
|
||||||
|
use log::Level;
|
||||||
|
use serde::{Deserialize, Deserializer, de};
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use uci_derive::{UciConfig, UciSection};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "uci", unix))]
|
||||||
|
use crate::settings::uci::UciSource;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "uci", unix))]
|
||||||
|
pub mod uci;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "uci", unix))]
|
||||||
|
pub(crate) mod uci_schema;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
|
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, integer 0 or 1, 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_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
0 => Ok(false),
|
||||||
|
1 => Ok(true),
|
||||||
|
_ => Err(E::custom("boolean integer must be 0 or 1")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
0 => Ok(false),
|
||||||
|
1 => Ok(true),
|
||||||
|
_ => Err(E::custom("boolean integer must be 0 or 1")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum ReflectorProtocol {
|
||||||
|
IcmpEcho = 1,
|
||||||
|
IcmpTimestamps,
|
||||||
|
Irtt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ReflectorProtocol {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ReflectorProtocol::IcmpEcho => write!(f, "icmp-echo"),
|
||||||
|
ReflectorProtocol::IcmpTimestamps => write!(f, "icmp-timestamps"),
|
||||||
|
ReflectorProtocol::Irtt => write!(f, "irtt"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ReflectorProtocol {
|
||||||
|
type Err = ConfigError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"icmp-echo" => Ok(ReflectorProtocol::IcmpEcho),
|
||||||
|
"icmp-timestamps" => Ok(ReflectorProtocol::IcmpTimestamps),
|
||||||
|
"irtt" => Ok(ReflectorProtocol::Irtt),
|
||||||
|
_ => 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, Clone, Deserialize, UciSection)]
|
||||||
|
pub(crate) struct ReflectorDefinition {
|
||||||
|
pub(crate) host: String,
|
||||||
|
pub(crate) country: Option<String>,
|
||||||
|
pub(crate) protocols: Vec<ReflectorProtocol>,
|
||||||
|
pub(crate) irtt_port: Option<u16>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) irtt_hmac: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ReflectorRow {
|
||||||
|
host: String,
|
||||||
|
country: Option<String>,
|
||||||
|
protocols: String,
|
||||||
|
irtt_port: Option<u16>,
|
||||||
|
irtt_hmac: Option<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) reflectors: ReflectorSettings,
|
||||||
|
|
||||||
|
#[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>()
|
||||||
|
.with_repeated_schema(&[
|
||||||
|
uci_schema::UciRepeatedSectionMapping {
|
||||||
|
uci_section: "reflector",
|
||||||
|
config_path: "reflectors.entries",
|
||||||
|
options: <ReflectorDefinition as uci_schema::UciSectionSchema>::UCI_OPTIONS,
|
||||||
|
},
|
||||||
|
uci_schema::UciRepeatedSectionMapping {
|
||||||
|
uci_section: "bootstrap_reflector",
|
||||||
|
config_path: "reflectors.bootstrap",
|
||||||
|
options: <ReflectorDefinition as uci_schema::UciSectionSchema>::UCI_OPTIONS,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.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("__")
|
||||||
|
.try_parsing(true)
|
||||||
|
.list_separator(",")
|
||||||
|
.with_list_parse_key("reflectors.files")
|
||||||
|
.with_list_parse_key("reflectors.countries"),
|
||||||
|
);
|
||||||
|
let mut settings: Self = builder.build()?.try_deserialize()?;
|
||||||
|
|
||||||
|
settings.reflectors.load_files()?;
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) measurement_bind_device: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) measurement_mark: Option<u32>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) measurement_fib: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ReflectorSettings {
|
||||||
|
#[serde(deserialize_with = "deserialize_reflector_files")]
|
||||||
|
pub(crate) files: Vec<String>,
|
||||||
|
pub(crate) countries: Vec<String>,
|
||||||
|
|
||||||
|
#[uci(skip)]
|
||||||
|
pub(crate) entries: Vec<ReflectorDefinition>,
|
||||||
|
|
||||||
|
#[uci(skip)]
|
||||||
|
pub(crate) bootstrap: Vec<ReflectorDefinition>,
|
||||||
|
|
||||||
|
pub(crate) num_reflectors: u8,
|
||||||
|
pub(crate) peer_reselection_time: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReflectorSettings {
|
||||||
|
fn load_files(&mut self) -> Result<(), ConfigError> {
|
||||||
|
for path in &self.files {
|
||||||
|
let mut reader = csv::Reader::from_path(path)
|
||||||
|
.map_err(|error| ConfigError::Message(error.to_string()))?;
|
||||||
|
for row in reader.deserialize::<ReflectorRow>() {
|
||||||
|
let row = row.map_err(|error| ConfigError::Message(error.to_string()))?;
|
||||||
|
let protocols = row
|
||||||
|
.protocols
|
||||||
|
.split(['|', ';'])
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|protocol| !protocol.is_empty())
|
||||||
|
.map(ReflectorProtocol::from_str)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
if protocols.is_empty() {
|
||||||
|
return Err(ConfigError::Message(format!(
|
||||||
|
"reflector row in {path} has no protocols"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
self.entries.push(ReflectorDefinition {
|
||||||
|
host: row.host,
|
||||||
|
country: row.country,
|
||||||
|
protocols,
|
||||||
|
irtt_port: row.irtt_port,
|
||||||
|
irtt_hmac: row.irtt_hmac,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ReflectorSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
let icmp = |host: &str| ReflectorDefinition {
|
||||||
|
host: host.to_owned(),
|
||||||
|
country: None,
|
||||||
|
protocols: vec![
|
||||||
|
ReflectorProtocol::IcmpEcho,
|
||||||
|
ReflectorProtocol::IcmpTimestamps,
|
||||||
|
],
|
||||||
|
irtt_port: None,
|
||||||
|
irtt_hmac: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let irtt = |host: &str, country: Option<&str>| ReflectorDefinition {
|
||||||
|
host: host.to_owned(),
|
||||||
|
country: country.map(str::to_owned),
|
||||||
|
protocols: vec![ReflectorProtocol::Irtt],
|
||||||
|
irtt_port: Some(2112),
|
||||||
|
irtt_hmac: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
files: vec!["/etc/sqm-autorate-rust/reflectors.csv".into()],
|
||||||
|
countries: Vec::new(),
|
||||||
|
entries: Vec::new(),
|
||||||
|
bootstrap: vec![
|
||||||
|
icmp("9.9.9.9"),
|
||||||
|
icmp("8.238.120.14"),
|
||||||
|
icmp("74.82.42.42"),
|
||||||
|
icmp("194.242.2.2"),
|
||||||
|
icmp("208.67.222.222"),
|
||||||
|
icmp("94.140.14.14"),
|
||||||
|
irtt("global.irtt.lochnair.net", None),
|
||||||
|
irtt("uk-cov1.irtt.lochnair.net", Some("GB")),
|
||||||
|
irtt("uk-cov2.irtt.lochnair.net", Some("GB")),
|
||||||
|
irtt("de-fra.irtt.lochnair.net", Some("DE")),
|
||||||
|
irtt("nl-ams1.irtt.lochnair.net", Some("NL")),
|
||||||
|
],
|
||||||
|
num_reflectors: 5,
|
||||||
|
peer_reselection_time: 15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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: ReflectorProtocol,
|
||||||
|
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: ReflectorProtocol::IcmpTimestamps,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UCI drops empty string options; `option files 'none'` disables file loading.
|
||||||
|
fn deserialize_reflector_files<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum Files {
|
||||||
|
List(Vec<String>),
|
||||||
|
String(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
match Files::deserialize(deserializer)? {
|
||||||
|
Files::List(files) => Ok(files),
|
||||||
|
Files::String(value) if value.trim().is_empty() || value.trim() == "none" => Ok(Vec::new()),
|
||||||
|
Files::String(_) => Err(de::Error::custom(
|
||||||
|
"reflector files must be a list, an empty string, or none",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::settings::uci_schema::{UciConfigSchema, UciRepeatedSectionMapping, 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],
|
||||||
|
repeated_schema: &'static [UciRepeatedSectionMapping],
|
||||||
|
config_dir: Option<PathBuf>,
|
||||||
|
save_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UciSource {
|
||||||
|
fn with_selection(packages: PackageSelection) -> Self {
|
||||||
|
Self {
|
||||||
|
packages,
|
||||||
|
required: true,
|
||||||
|
schema: &[],
|
||||||
|
repeated_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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers section types collected as arrays of tables at their config paths.
|
||||||
|
/// Absent section types leave the destination unset for Serde/source defaults.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn with_repeated_schema(
|
||||||
|
mut self,
|
||||||
|
schema: &'static [UciRepeatedSectionMapping],
|
||||||
|
) -> Self {
|
||||||
|
self.repeated_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>,
|
||||||
|
repeated_values: &mut Map<String, Vec<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 repeated_mapping = self
|
||||||
|
.repeated_schema
|
||||||
|
.iter()
|
||||||
|
.find(|mapping| mapping.uci_section == section.type_());
|
||||||
|
let mut section_values = Map::new();
|
||||||
|
|
||||||
|
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 repeated_option = repeated_mapping.and_then(|mapping| {
|
||||||
|
mapping
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.find(|option| option.uci_option == option_name)
|
||||||
|
});
|
||||||
|
|
||||||
|
let destination_key = if let Some(mapping) = repeated_option {
|
||||||
|
mapping.config_field.to_owned()
|
||||||
|
} else if repeated_mapping.is_some() {
|
||||||
|
option_name.to_owned()
|
||||||
|
} else {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let destination = if repeated_mapping.is_some() {
|
||||||
|
&mut section_values
|
||||||
|
} else {
|
||||||
|
&mut *values
|
||||||
|
};
|
||||||
|
|
||||||
|
if destination.insert(destination_key.clone(), value).is_some() {
|
||||||
|
return Err(SourceError::Message(format!(
|
||||||
|
"duplicate UCI destination key \
|
||||||
|
{destination_key:?} while mapping {uci_key:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mapping) = repeated_mapping {
|
||||||
|
let origin = format!("UCI section {package_name}.{selector}");
|
||||||
|
repeated_values
|
||||||
|
.entry(mapping.config_path.to_owned())
|
||||||
|
.or_default()
|
||||||
|
.push(Value::new(Some(&origin), ValueKind::Table(section_values)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
let mut repeated_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, &mut repeated_values)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (config_path, sections) in repeated_values {
|
||||||
|
let origin = "UCI repeated sections".to_owned();
|
||||||
|
if values
|
||||||
|
.insert(
|
||||||
|
config_path.clone(),
|
||||||
|
Value::new(Some(&origin), ValueKind::Array(sections)),
|
||||||
|
)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(SourceError::Message(format!(
|
||||||
|
"duplicate UCI destination key {config_path:?} while mapping repeated sections"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,41 @@
|
|||||||
|
// 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],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "uci", unix))]
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) struct UciRepeatedSectionMapping {
|
||||||
|
/// UCI section type, for both named and anonymous sections.
|
||||||
|
pub(crate) uci_section: &'static str,
|
||||||
|
|
||||||
|
/// Config-rs / Serde path receiving an array of section tables.
|
||||||
|
pub(crate) config_path: &'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,465 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
//
|
||||||
|
|
||||||
|
use crate::settings::ReflectorSettings;
|
||||||
|
|
||||||
|
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.reflectors.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 ReflectorSettings {
|
||||||
|
fn validate(&self, report: &mut ValidationReport) {
|
||||||
|
if self.num_reflectors == 0 {
|
||||||
|
report.error("reflectors.num_reflectors", "must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.peer_reselection_time == 0 {
|
||||||
|
report.error("reflectors.peer_reselection_time", "must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.files.is_empty() && self.entries.is_empty() {
|
||||||
|
report.error(
|
||||||
|
"reflectors",
|
||||||
|
"at least one reflector file or inline entry is required",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, reflector) in self.entries.iter().enumerate() {
|
||||||
|
if reflector.host.trim().is_empty() {
|
||||||
|
report.error(
|
||||||
|
"reflectors.entries",
|
||||||
|
format!("entry {index} must have a nonempty host"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if reflector.protocols.is_empty() {
|
||||||
|
report.error(
|
||||||
|
"reflectors.entries",
|
||||||
|
format!("entry {index} must have at least one measurement protocol"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, file) in self.files.iter().enumerate() {
|
||||||
|
if file.trim().is_empty() {
|
||||||
|
report.error(
|
||||||
|
"reflectors.files",
|
||||||
|
format!("entry {index} must not be empty"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, country) in self.countries.iter().enumerate() {
|
||||||
|
if country.len() != 2 || !country.bytes().all(|c| c.is_ascii_alphabetic()) {
|
||||||
|
report.error(
|
||||||
|
"reflectors.countries",
|
||||||
|
format!(
|
||||||
|
"entry {index} ({country:?}) must be an ISO 3166-1 alpha-2 country code"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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,
|
||||||
|
measurement_bind_device: None,
|
||||||
|
measurement_mark: None,
|
||||||
|
measurement_fib: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
-15
@@ -1,25 +1,41 @@
|
|||||||
use rustix::fs::Timespec;
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
use rustix::thread::ClockId;
|
//
|
||||||
use rustix::time::clock_gettime;
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
pub struct Time {
|
use std::{
|
||||||
time_s: u64,
|
marker::PhantomData,
|
||||||
time_ns: u64,
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use rustix::time::{ClockId, clock_gettime};
|
||||||
|
|
||||||
|
pub trait Clock {
|
||||||
|
const ID: ClockId;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Time {
|
pub enum Realtime {}
|
||||||
pub fn new(id: ClockId) -> Self {
|
|
||||||
let time: Timespec = clock_gettime(id);
|
impl Clock for Realtime {
|
||||||
|
const ID: ClockId = ClockId::Realtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ClockSample<C> {
|
||||||
|
time_s: u64,
|
||||||
|
time_ns: u64,
|
||||||
|
_clock: PhantomData<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Clock> ClockSample<C> {
|
||||||
|
pub fn now() -> Self {
|
||||||
|
let time = clock_gettime(C::ID);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
time_s: time.tv_sec as u64,
|
time_s: time.tv_sec as u64,
|
||||||
time_ns: time.tv_nsec as u64,
|
time_ns: time.tv_nsec as u64,
|
||||||
|
_clock: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_time_since_midnight(&self) -> i64 {
|
|
||||||
(self.time_s as i64 % 86400 * 1000) + (self.time_ns as i64 / 1000000)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn secs(&self) -> u64 {
|
pub fn secs(&self) -> u64 {
|
||||||
self.time_s
|
self.time_s
|
||||||
}
|
}
|
||||||
@@ -35,8 +51,24 @@ impl Time {
|
|||||||
pub fn as_secs_f64(&self) -> f64 {
|
pub fn as_secs_f64(&self) -> f64 {
|
||||||
self.time_s as f64 + self.time_ns as f64 / 1_000_000_000.0
|
self.time_s as f64 + self.time_ns as f64 / 1_000_000_000.0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_milliseconds(&self) -> u64 {
|
impl ClockSample<Realtime> {
|
||||||
(self.time_s * 1000) + (self.time_ns / 1000000)
|
pub fn as_time_since_midnight(&self) -> i64 {
|
||||||
|
(self.time_s as i64 % 86_400 * 1000) + self.time_ns as i64 / 1_000_000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SystemTime> for ClockSample<Realtime> {
|
||||||
|
fn from(time: SystemTime) -> Self {
|
||||||
|
let duration = time
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("realtime timestamp predates Unix epoch");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
time_s: duration.as_secs(),
|
||||||
|
time_ns: u64::from(duration.subsec_nanos()),
|
||||||
|
_clock: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-10
@@ -1,15 +1,11 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
|
||||||
pub trait MutexExt<T> {
|
pub type ArcRwLock<T> = Arc<RwLock<T>>;
|
||||||
fn lock_anyhow(&self) -> anyhow::Result<MutexGuard<'_, T>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> MutexExt<T> for Mutex<T> {
|
|
||||||
fn lock_anyhow(&self) -> anyhow::Result<MutexGuard<'_, T>> {
|
|
||||||
self.lock().map_err(|e| anyhow!("mutex poisoned: {e}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RwLockExt<T> {
|
pub trait RwLockExt<T> {
|
||||||
fn read_anyhow(&self) -> anyhow::Result<RwLockReadGuard<'_, T>>;
|
fn read_anyhow(&self) -> anyhow::Result<RwLockReadGuard<'_, T>>;
|
||||||
|
|||||||
Reference in new issue
Block a user