174 Commits
Author SHA1 Message Date
Nils Andreas Svee 7fb744790d fix(reflectors): merge missing country metadata without startup conflicts
Build binaries / Build (aarch64-musl) (push) Successful in 1m54s
Build binaries / Build (arm-musleabi) (push) Successful in 1m54s
Build binaries / Build (arm-musleabihf) (push) Successful in 1m56s
Build binaries / Build (armv7-musleabi) (push) Successful in 1m56s
Build binaries / Build (armv7-musleabihf) (push) Successful in 1m56s
Build binaries / Build (i686-musl) (push) Successful in 1m55s
Build binaries / Build (mips64-muslabi64) (push) Successful in 2m0s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 2m13s
Build binaries / Build (riscv64gc-musl) (push) Successful in 1m53s
Build binaries / Build (x86_64-musl) (push) Successful in 1m53s
Build binaries / Build (mips-musl) (push) Successful in 2m3s
Build binaries / Build (mipsel-musl) (push) Successful in 2m9s
2026-10-13 22:58:15 +07:00
Nils Andreas Svee cb4ae4d251 chore: update reflectors list to use new format 2026-10-13 22:56:25 +07:00
Nils Andreas Svee 93bfee0624 fix(irtt): honor measurement socket routing settings
Build binaries / Build (arm-musleabi) (push) Successful in 1m37s
Build binaries / Build (aarch64-musl) (push) Successful in 1m39s
Build binaries / Build (armv7-musleabi) (push) Successful in 1m44s
Build binaries / Build (arm-musleabihf) (push) Successful in 1m45s
Build binaries / Build (armv7-musleabihf) (push) Successful in 1m52s
Build binaries / Build (i686-musl) (push) Successful in 1m53s
Build binaries / Build (mips64-muslabi64) (push) Successful in 2m19s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 2m12s
Build binaries / Build (x86_64-musl) (push) Successful in 1m48s
Build binaries / Build (riscv64gc-musl) (push) Successful in 1m50s
Build binaries / Build (mips-musl) (push) Successful in 2m20s
Build binaries / Build (mipsel-musl) (push) Successful in 2m13s
2026-10-13 22:03:09 +07:00
Nils Andreas Svee 80b39da9de fix(ratecontroller): use the configured reflector count for reselection 2026-10-13 22:02:58 +07:00
Nils Andreas Svee f8691c5189 fix(reflectors): reject conflicting resolved endpoints
Build binaries / Build (aarch64-musl) (push) Successful in 2m16s
Build binaries / Build (arm-musleabi) (push) Successful in 2m48s
Build binaries / Build (arm-musleabihf) (push) Successful in 2m6s
Build binaries / Build (armv7-musleabi) (push) Successful in 2m8s
Build binaries / Build (armv7-musleabihf) (push) Successful in 2m29s
Build binaries / Build (i686-musl) (push) Successful in 2m34s
Build binaries / Build (mips64-muslabi64) (push) Successful in 1m58s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 2m0s
Build binaries / Build (riscv64gc-musl) (push) Successful in 2m10s
Build binaries / Build (x86_64-musl) (push) Successful in 2m41s
Build binaries / Build (mips-musl) (push) Successful in 1m57s
Build binaries / Build (mipsel-musl) (push) Successful in 1m53s
Deduplicate compatible definitions and reject conflicting metadata or authentication within each list and across bootstrap and pool entries. Treat hostname aliases, protocol ordering, country-code case, and omitted default ports consistently.
2026-10-13 21:24:05 +07:00
Nils Andreas Svee a073a9d049 fix(reflectors): tolerate individual DNS resolution failures
Log and skip unresolved reflectors so startup can proceed with usable endpoints. Report an error when none remain for the selected protocol.
2026-10-13 21:24:05 +07:00
Nils Andreas Svee 78bcb7886a fix(controller): honor the configured ICMP probe interval
Use advanced_settings.tick_interval for Controller schedules so ICMP probe pacing follows the configured measurement cadence.
2026-10-13 21:24:05 +07:00
Nils Andreas Svee de77e5a281 feat(measurement): add configurable IRTT reflector measurements
Support optional IRTT measurements alongside ICMP with CSV and inline reflector definitions, repeated UCI registration, authentication, and endpoint-specific stream tracking.

Keep the managed driver running during target acknowledgements and stop and join it on event-loop exits. Carry stream identity through baselines, selection, and rate control so endpoints sharing an IP remain distinct.
2026-10-13 21:24:05 +07:00
Nils Andreas Svee 76a7e14835 feat(uci): support repeated sections and skipped fields
Allow structured lists to share the existing UCI configuration adapter while keeping collection fields out of scalar section mappings.
2026-10-13 21:24:04 +07:00
Nils Andreas Svee 8c923f373d chore(deps): update rust dependencies
Rust CI / cargo clippy (push) Failing after 27s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 11s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 7s
Supply chain / cargo-deny advisories (push) Failing after 2s
Supply chain / cargo-deny bans licenses sources (push) Failing after 2s
Supply chain / actionlint (push) Failing after 2s
Rust CI / cargo doc (push) Failing after 20s
Build binaries / Build (arm-musleabi) (push) Successful in 1m46s
Build binaries / Build (aarch64-musl) (push) Successful in 1m47s
Build binaries / Build (armv7-musleabi) (push) Successful in 1m44s
Build binaries / Build (arm-musleabihf) (push) Successful in 1m45s
Build binaries / Build (armv7-musleabihf) (push) Successful in 1m39s
Build binaries / Build (i686-musl) (push) Successful in 1m39s
Build binaries / Build (mips64-muslabi64) (push) Successful in 2m12s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 2m9s
Build binaries / Build (x86_64-musl) (push) Successful in 1m38s
Build binaries / Build (riscv64gc-musl) (push) Successful in 1m44s
Rust CI / cargo fmt (push) Successful in 16s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 23s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 28s
Build binaries / Build (mips-musl) (push) Successful in 2m12s
Build binaries / Build (mipsel-musl) (push) Successful in 2m7s
2026-10-13 00:41:49 +07:00
renovate Bot 8267089733 chore(deps): update rust crate syn to v3.0.9 2026-10-13 00:41:13 +07:00
renovate Bot 6ad41931a1 chore(deps): update google/osv-scanner-action action to v2.7.0 2026-10-13 00:41:13 +07:00
renovate Bot b94742c62a fix(deps): update rust-uci digest to 6835397 2026-10-13 00:41:13 +07:00
Nils Andreas Svee 7e8213556f chore(ci): add gitea action to build binaries 2026-10-10 14:50:32 +07:00
renovate Bot df7e9ffbab chore(deps): update github/codeql-action digest to 24c5418
Rust CI / cargo fmt (pull_request) Successful in 12s
Rust CI / cargo clippy (pull_request) Failing after 30s
Rust CI / cargo check (x86_64-unknown-linux-musl) (pull_request) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (pull_request) Failing after 9s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (pull_request) Failing after 58s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (pull_request) Failing after 1m4s
Supply chain / actionlint (pull_request) Successful in 7s
Rust CI / cargo doc (pull_request) Successful in 40s
Supply chain / cargo-deny bans licenses sources (pull_request) Failing after 1m55s
Rust CI / cargo clippy (push) Failing after 38s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo doc (push) Successful in 33s
Supply chain / cargo-deny advisories (push) Failing after 1m44s
Supply chain / actionlint (push) Successful in 7s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m37s
Supply chain / cargo-deny advisories (pull_request) Failing after 1m55s
Rust CI / cargo fmt (push) Successful in 16s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 37s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 42s
2026-10-08 13:03:35 +00:00
Nils Andreas Svee 68b34a587f refactor: move ucisource to the settings module
Rust CI / cargo clippy (push) Failing after 37s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 7s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo doc (push) Successful in 32s
Supply chain / cargo-deny advisories (push) Failing after 1m38s
Supply chain / actionlint (push) Successful in 6s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m40s
Rust CI / cargo fmt (push) Successful in 11s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 37s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 41s
2026-10-08 20:03:05 +07:00
Nils Andreas Svee b7bbed6e5e Merge branch 'refactor/measurement-icmp'
Rust CI / cargo clippy (push) Failing after 35s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo doc (push) Successful in 31s
Supply chain / cargo-deny advisories (push) Failing after 1m38s
Supply chain / actionlint (push) Successful in 8s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m36s
Rust CI / cargo fmt (push) Successful in 11s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 33s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 38s
2026-10-08 16:27:00 +07:00
Nils Andreas Svee 0e2e2f9d38 fix: start controller after the baseliner 2026-10-08 14:31:59 +07:00
Nils Andreas Svee e80940ba6d refactor(main): add spawn task helper 2026-10-08 14:28:12 +07:00
Nils Andreas Svee 480976b666 fix(metrics): move ping metrics sending to baseliner 2026-10-08 14:15:14 +07:00
Nils Andreas Svee ac349a1897 refactor(measurement): remove legacy pinger and migrate production to 2026-10-08 14:15:14 +07:00
Nils Andreas Svee a925e60f36 refactor(measurement): introduce Controller for dynamic ICMP probe configuration 2026-10-08 14:15:14 +07:00
Nils Andreas Svee c7b283d761 refactor(measurement): enhance ICMP engine's async integration and robustness
Ensure IcmpEngine initializes within a Tokio runtime, improve timestamp validation, and refine probe management.
2026-10-08 14:15:14 +07:00
Nils Andreas Svee 89c7d75947 refactor(measurement): migrate ICMP engine to Tokio async runtime 2026-10-08 14:15:14 +07:00
Nils Andreas Svee afedfbeb65 feat(measurement): implement ICMP measurement engine 2026-10-08 14:15:13 +07:00
Nils Andreas Svee f51ff19f04 feat(measurement): add core data models 2026-10-08 10:48:14 +07:00
Nils Andreas Svee 60fdea4dff refactor(time): implement ClockSample from SystemTime 2026-10-08 10:48:14 +07:00
Nils Andreas Svee 3016f4b887 feat(settings): add network settings for measurement sockets 2026-10-08 10:48:03 +07:00
renovate Bot 52a5ab2dfe chore(deps): update rust crate trybuild to v1.0.122
Rust CI / cargo fmt (pull_request) Successful in 14s
Rust CI / cargo clippy (pull_request) Failing after 36s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (pull_request) Failing after 35s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (pull_request) Failing after 44s
Rust CI / cargo check (x86_64-unknown-linux-musl) (pull_request) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (pull_request) Failing after 10s
Rust CI / cargo doc (pull_request) Successful in 34s
Supply chain / actionlint (pull_request) Successful in 10s
Supply chain / cargo-deny bans licenses sources (pull_request) Failing after 1m34s
Rust CI / cargo clippy (push) Failing after 36s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 7s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo doc (push) Successful in 33s
Supply chain / cargo-deny advisories (push) Failing after 1m43s
Supply chain / actionlint (push) Successful in 8s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m35s
Supply chain / cargo-deny advisories (pull_request) Failing after 1m41s
Rust CI / cargo fmt (push) Successful in 11s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 35s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 43s
2026-10-07 23:07:38 +00:00
Nils Andreas Svee fca0967563 refactor(time): consolidate ClockId usage via the public time::Clock trait
Rust CI / cargo clippy (push) Failing after 35s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 7s
Rust CI / cargo doc (push) Successful in 37s
Supply chain / actionlint (push) Successful in 8s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m47s
Supply chain / cargo-deny advisories (push) Failing after 1m49s
Rust CI / cargo fmt (push) Successful in 15s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 32s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 41s
Remove direct `rustix::time::ClockId` imports by centralizing its dependency within the `time` module.
2026-10-04 23:46:54 +07:00
renovate Bot b7b66fc1f4 chore(deps): update rust crate libc to v0.2.190
Rust CI / cargo fmt (pull_request) Successful in 16s
Rust CI / cargo clippy (pull_request) Failing after 38s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (pull_request) Failing after 38s
Rust CI / cargo check (x86_64-unknown-linux-musl) (pull_request) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (pull_request) Failing after 44s
Rust CI / cargo test (x86_64-unknown-linux-musl) (pull_request) Failing after 9s
Supply chain / actionlint (pull_request) Successful in 8s
Rust CI / cargo doc (pull_request) Successful in 42s
Supply chain / cargo-deny bans licenses sources (pull_request) Failing after 1m54s
Rust CI / cargo clippy (push) Failing after 41s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 9s
Supply chain / actionlint (push) Successful in 9s
Rust CI / cargo doc (push) Successful in 39s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m49s
Supply chain / cargo-deny advisories (push) Failing after 1m51s
Supply chain / cargo-deny advisories (pull_request) Failing after 1m57s
Rust CI / cargo fmt (push) Successful in 16s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 38s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 43s
2026-10-02 20:09:10 +00:00
renovate Bot 305a182001 chore(deps): update rust crate config to v0.15.27
Rust CI / cargo fmt (pull_request) Successful in 11s
Rust CI / cargo clippy (pull_request) Failing after 26s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (pull_request) Failing after 27s
Rust CI / cargo check (x86_64-unknown-linux-musl) (pull_request) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (pull_request) Failing after 33s
Rust CI / cargo test (x86_64-unknown-linux-musl) (pull_request) Failing after 7s
Supply chain / actionlint (pull_request) Successful in 17s
Rust CI / cargo doc (pull_request) Successful in 40s
Supply chain / cargo-deny bans licenses sources (pull_request) Failing after 1m49s
Rust CI / cargo clippy (push) Failing after 38s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Supply chain / actionlint (push) Successful in 8s
Rust CI / cargo doc (push) Successful in 40s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m52s
Supply chain / cargo-deny advisories (push) Failing after 1m54s
Supply chain / cargo-deny advisories (pull_request) Failing after 1m49s
Rust CI / cargo fmt (push) Successful in 18s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 38s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 42s
2026-09-30 08:33:02 +00:00
Nils Andreas Svee 64e002c0d0 chore(deps): update rust crate icmp-socket2 to v0.6.4
Rust CI / cargo clippy (push) Failing after 40s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 7s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo doc (push) Successful in 33s
Supply chain / actionlint (push) Successful in 11s
Supply chain / cargo-deny advisories (push) Failing after 1m51s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m44s
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 40s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 45s
2026-09-30 15:32:22 +07:00
Nils Andreas Svee 817cc304e6 refactor(time): Introduce typed clock sources for compile-time safety
This change replaces runtime `ClockId` parameters with distinct types to ensure correct clock usage at compile time.
2026-09-30 12:05:42 +07:00
Nils Andreas Svee e27bab1944 chore(deps): update rust crate icmp-socket2 to v0.6.2
Rust CI / cargo clippy (push) Failing after 40s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Supply chain / actionlint (push) Successful in 9s
Rust CI / cargo doc (push) Successful in 43s
Supply chain / cargo-deny advisories (push) Failing after 1m48s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m50s
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 40s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 47s
2026-09-28 19:52:26 +07:00
renovate[bot] fbb971b47d chore(deps): update rust crate icmp-socket2 to v0.6.1
Rust CI / cargo clippy (push) Failing after 38s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Supply chain / actionlint (push) Successful in 6s
Rust CI / cargo doc (push) Successful in 40s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m45s
Supply chain / cargo-deny advisories (push) Failing after 1m49s
Rust CI / cargo fmt (push) Successful in 16s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 37s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 46s
2026-09-28 07:49:49 +00:00
renovate Bot 3fbc96b15f chore(deps): update github/codeql-action digest to 2892aa5
Rust CI / cargo fmt (pull_request) Successful in 10s
Rust CI / cargo clippy (pull_request) Failing after 29s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (pull_request) Failing after 27s
Rust CI / cargo check (x86_64-unknown-linux-musl) (pull_request) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (pull_request) Failing after 38s
Rust CI / cargo test (x86_64-unknown-linux-musl) (pull_request) Failing after 9s
Rust CI / cargo doc (pull_request) Successful in 37s
Supply chain / actionlint (pull_request) Successful in 9s
Supply chain / cargo-deny bans licenses sources (pull_request) Failing after 1m42s
Rust CI / cargo clippy (push) Failing after 38s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 7s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Supply chain / actionlint (push) Successful in 8s
Rust CI / cargo doc (push) Successful in 44s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m48s
Supply chain / cargo-deny advisories (push) Failing after 1m51s
Supply chain / cargo-deny advisories (pull_request) Failing after 1m47s
Rust CI / cargo fmt (push) Successful in 16s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 37s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 46s
2026-09-25 17:01:54 +00:00
Nils Andreas Svee c199d63c93 ci: split Gitea and GitHub workflows
Rust CI / cargo clippy (push) Failing after 34s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo doc (push) Successful in 36s
Supply chain / actionlint (push) Successful in 11s
Supply chain / cargo-deny bans licenses sources (push) Failing after 1m54s
Supply chain / cargo-deny advisories (push) Failing after 2m1s
Rust CI / cargo fmt (push) Successful in 11s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Failing after 34s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 41s
2026-09-26 00:01:03 +07:00
renovate Bot 2afa1bba35 chore(deps): update rust crate thiserror to v2.0.21
Build / build (push) Failing after 3s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 1m41s
DevSkim / DevSkim (push) Failing after 1m21s
2026-09-25 17:26:03 +07:00
renovate Bot d03effd600 chore(deps): update rust crate config to v0.15.26 2026-09-25 17:26:03 +07:00
renovate Bot 6103d2dae4 chore(deps): update rust crate syn to v3.0.6 2026-09-25 17:26:03 +07:00
renovate Bot efe9eff036 chore(deps): update rust crate rustix to v1.1.5 2026-09-25 17:26:03 +07:00
renovate Bot bb94b42d24 chore(deps): update google/osv-scanner-action action to v2.6.0 2026-09-25 17:26:03 +07:00
renovate Bot de8d7e3aeb chore(deps): update netlink dependencies to v0.3.5 2026-09-25 17:26:02 +07:00
renovate Bot 746636b59a chore(deps): update rust crate trybuild to v1.0.121 2026-09-25 17:26:02 +07:00
renovate Bot 83e02c6e1c chore(deps): update actions-rust-lang/setup-rust-toolchain action to v2 2026-09-25 17:26:02 +07:00
Nils Andreas Svee 6d4849899e fix(settings): reject zero-floor minimum rates
Build / build (push) Failing after 4s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 1m35s
DevSkim / DevSkim (push) Failing after 1m22s
2026-09-25 17:20:08 +07:00
Nils Andreas Svee 7254aa5cf9 fix(uci): propagate malformed optional sources 2026-09-25 17:20:08 +07:00
Nils Andreas Svee 5b0854920d fix(settings): reject IPv6 reflectors 2026-09-25 17:20:07 +07:00
Nils Andreas Svee 4c9fe15201 fix(log): honor configured maximum level 2026-09-25 17:20:07 +07:00
Nils Andreas Svee 5872f40d3e fix(icmp): handle RFC 792 timestamp rollover 2026-09-25 17:20:06 +07:00
renovate Bot 8fb051d48f chore(deps): update rust crate log to v0.4.34
DevSkim / DevSkim (pull_request) Failing after 1m25s
Build / build (push) Successful in 1m5s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 2m7s
Build / build (pull_request) Successful in 1m7s
rust-clippy analyze / Run rust-clippy analyzing (pull_request) Failing after 2m2s
DevSkim / DevSkim (push) Failing after 1m18s
2026-08-29 14:29:43 +00:00
renovate Bot a0535357c7 chore(deps): update google/osv-scanner-action action to v2.5.1
DevSkim / DevSkim (pull_request) Failing after 1m26s
Build / build (push) Successful in 1m7s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 1m59s
Build / build (pull_request) Successful in 1m8s
rust-clippy analyze / Run rust-clippy analyzing (pull_request) Failing after 2m8s
DevSkim / DevSkim (push) Failing after 1m22s
2026-08-29 14:27:59 +00:00
renovate Bot 91d773c77e chore(deps): update rust crate syn to v3.0.4
DevSkim / DevSkim (pull_request) Failing after 1m26s
Build / build (push) Successful in 1m6s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 1m55s
Build / build (pull_request) Successful in 1m6s
rust-clippy analyze / Run rust-clippy analyzing (pull_request) Failing after 2m3s
DevSkim / DevSkim (push) Failing after 1m22s
2026-08-29 14:12:05 +00:00
Lochnair 083d464067 Merge pull request 'chore(deps): update netlink dependencies to v0.3.4' (#76) from renovate/netlink-dependencies into master
Build / build (push) Successful in 1m5s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 2m1s
DevSkim / DevSkim (push) Failing after 1m21s
Reviewed-on: #76
2026-08-29 14:11:36 +00:00
renovate Bot f0eae4b2ce chore(deps): update netlink dependencies to v0.3.4
DevSkim / DevSkim (pull_request) Failing after 1m32s
Build / build (pull_request) Successful in 1m4s
rust-clippy analyze / Run rust-clippy analyzing (pull_request) Failing after 2m5s
2026-08-29 14:10:14 +00:00
Lochnair a6067910ad chore: make renovate combine netlink deps
Build / build (push) Successful in 1m4s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 1m57s
DevSkim / DevSkim (push) Failing after 1m18s
2026-08-28 09:15:20 +00:00
Lochnair ffd7b0beb7 fix: remove unused import
Build / build (push) Successful in 57s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 2m10s
DevSkim / DevSkim (push) Failing after 4s
2026-08-23 17:23:10 +00:00
Lochnair 377fb3be2d chore(ci): install latest rust
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 1m42s
Build / build (push) Failing after 50s
DevSkim / DevSkim (push) Failing after 4s
2026-08-23 17:12:17 +00:00
Lochnair d765bef002 chore(ci): add workflow_dispatch to build workflow
Build / build (push) Failing after 2s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 5s
DevSkim / DevSkim (push) Failing after 4s
2026-08-23 16:50:26 +00:00
renovate[bot] 86b13f948e chore(deps): update rust crate thiserror to v2.0.20 2026-08-14 16:44:07 +00:00
Nils Andreas Svee 4abf4bfcb8 refactor(ratecontroller): own random generator 2026-08-13 22:16:43 +07:00
Nils Andreas Svee 1a49c9adce refactor(platform): move platform.rs to platform/mod.rs
Restructure the platform module into a directory to allow for future sub-module expansion.
2026-08-13 21:10:53 +07:00
Nils Andreas Svee 8c831e4287 refactor(ratecontroller): reorder methods to improve code organization 2026-08-13 14:20:15 +07:00
Nils Andreas Svee 6a90446b56 fix(ratecontroller): use egress interface for traffic stats
Read both RX and TX byte counters from the configured upload/egress
interface. Remove the obsolete per-direction stats selection and related
plumbing now that download load uses RX and upload load uses TX from the
same interface.
2026-08-13 14:19:53 +07:00
renovate[bot] 1ad47eaeb0 chore(deps): update rust crate syn to v3.0.3 2026-08-07 09:19:39 +00:00
renovate[bot] a27b95e9ca fix(deps): update rust crate syn to v3 2026-08-07 09:11:56 +00:00
Nils Andreas Svee be6546b443 Merge pull request #68 from Lochnair/feature/config
feat(config): migrate configuration loading to config-rs
2026-08-07 16:10:13 +07:00
Nils Andreas Svee 605ec6ef81 refactor(main): simplify interface stats direction logic
Remove the ifb/veth interface prefix detection from
interface_stats_directions. The function now unconditionally returns
RX for download and TX for upload, as the previous heuristic was
unreliable and the simplified behavior matches actual usage.
2026-08-07 16:05:00 +07:00
Nils Andreas Svee 7e0f434251 refactor: replace legacy Config with Settings
Remove legacy src/config.rs and migrate all application consumers to
the nested Settings model. Baseliner, metrics, pingers, ratecontroller,
reflector selector, and main now use Settings. Derived min rates become
methods on NetworkSettings rather than stored legacy fields.
MeasurementType and ObservabilityProtocol move into the settings module.
2026-08-07 16:04:59 +07:00
Nils Andreas Svee 3c688089a1 feat(settings): add validation and flexible boolean parsing
Add settings validation with Warning and Error severity levels.
Warnings print to stderr without preventing startup; errors are
aggregated and prevent startup. Percentage validation accepts 1..=100
rather than silently clamping. Add UCI-compatible flexible boolean
parsing through Serde, accepting 1/true/yes/y/on/enabled for true and
0/false/no/n/off/disabled for false, while native booleans continue to
deserialize correctly.
2026-08-07 16:04:59 +07:00
Nils Andreas Svee a6478c9bd4 feat(uci): derive UCI schema from settings
Add uci-derive proc-macro crate with UciConfig and UciSection derive
macros that generate UCI schema metadata from Settings structs. The
schema maps UCI section types and option names to config-rs field
paths. UciSource::from_schema() creates a source from the derived
schema, mapping known UCI options to their Settings fields while
preserving unknown options under raw UCI-derived paths.
2026-08-07 16:04:59 +07:00
Nils Andreas Svee f51d90bff6 feat(settings): parse reflector lists with CSV
Add CSV-based reflector list parsing with Serde reflector records.
The load_reflectors method reads a CSV file with reflector_ip,
ip_version, and description columns, validating IP version
consistency for each entry.
2026-08-07 16:04:59 +07:00
Nils Andreas Svee a653bcf279 ci: support static cross-compilation with vendored UCI
Add Dockerfile.ci with pkg-config for vendored UCI builds. Switch
Jenkinsfile from pre-built Docker images to Dockerfile-based agents
for full control over the build environment. Add static CRT linking
via RUSTFLAGS for standalone static binaries across the CI matrix.
2026-08-07 16:04:59 +07:00
Nils Andreas Svee af07915441 build(uci): enable vendored UCI builds and Unix development support
Switch rust-uci dependency to the Lochnair fork with vendored UCI
feature for self-contained builds without system libuci. Add
uci-vendored feature flag (enabled by default) that pulls in the
vendored C library. UCI support on non-Linux Unix is intended for
development and testing.
2026-08-07 16:04:59 +07:00
Nils Andreas Svee 39add2b2e0 feat(uci): add generalized UCI configuration source
Introduce the UciSource abstraction as a config-rs Source that reads
UCI packages and maps them into the configuration tree. Supports
package selection, required/optional behavior, and custom config/save
directories. Move rust-uci dependency to cfg(unix) to enable UCI
support on all Unix systems for development and testing, while
Linux/OpenWrt remains the production-supported platform.
2026-08-07 16:04:59 +07:00
Nils Andreas Svee c0a50c68f9 feat(config): add config-rs settings support
Introduce config-rs and Serde support with a nested Settings model.
Add config and serde dependencies with toml feature for file-based
configuration loading. The Settings struct provides typed configuration
with sensible defaults for network, output, observability, and advanced
settings sections. Environment variable override via SQMA__ prefix is
supported. The legacy Config remains in use; migration follows later.
2026-08-07 16:04:58 +07:00
renovate[bot] 8bacb102b7 chore(deps): update google/osv-scanner-action action to v2.5.0 2026-08-07 08:37:03 +00:00
renovate[bot] bc86a2cd53 fix(deps): update rust crate icmp-socket2 to 0.6.0 2026-07-29 10:04:32 +00:00
renovate[bot] c6338b596e chore(deps): update rust crate libc to v0.2.189 2026-07-24 00:05:41 +07:00
Nils Andreas Svee 7f557f26d4 style(reflector): remove redundant comment in reflector selector
Eliminate an outdated comment to improve code readability and maintain a clean codebase.
2026-07-24 00:05:41 +07:00
Nils Andreas Svee c1bcb22b23 refactor(baseliner): Replace shared Mutex state with thread-safe control snapshots
Eliminate lock contention between the baseliner, ratecontroller, and selector by moving to a message-passing architecture using immutable snapshots of reflector state. This improves data consistency and simplifies cross-thread communication.
2026-07-24 00:05:41 +07:00
Nils Andreas Svee 51377f947f refactor(baseliner): extract reflector statistics logic and add unit tests 2026-07-24 00:05:41 +07:00
Nils Andreas Svee dc4ce1159f chore(deps): update rust crate dependencies 2026-07-24 00:05:41 +07:00
Nils Andreas Svee 288d1afdb9 chore(deps): switch netlink dependencies from git to crates.io 2026-07-24 00:05:41 +07:00
renovate[bot] bf0027a0b3 chore(deps): update rust crate thiserror to v2.0.19 2026-07-24 00:05:40 +07:00
renovate[bot] 49b700d4b2 chore(deps): update rust crate libc to v0.2.187 2026-07-24 00:05:40 +07:00
renovate[bot] f6fa7b8814 fix(deps): update netlink-socket2 digest to 928e9ca 2026-07-24 00:05:40 +07:00
renovate[bot] cd160e67fe fix(deps): update netlink-bindings digest to 928e9ca 2026-07-24 00:05:40 +07:00
renovate[bot] bab8278bdd chore(deps): update rust crate fastrand to v2.5.0 2026-07-24 00:05:40 +07:00
renovate[bot] c9cecb6945 chore(deps): update rust crate anyhow to v1.0.104 2026-07-24 00:05:40 +07:00
Nils Andreas Svee b45ae4b6d6 build: update development and cross-compilation tools 2026-07-24 00:05:40 +07:00
Nils Andreas Svee b762901470 feat(platform): add support for FreeBSD interface statistics 2026-07-24 00:05:19 +07:00
Nils Andreas Svee cd6d631afa refactor(config): remove unimplemented measurement types
Remove NTP and TCP timestamp variants from the measurement type configuration since they are currently unimplemented and lead to runtime panics.
2026-07-20 23:06:23 +07:00
Nils Andreas Svee b07dc99bf5 fix(deps): update rust crate icmp-socket2 to 0.5.1 2026-07-17 13:07:30 +07:00
Nils Andreas Svee ae9193580c refactor(platform): restructure platform-specific abstractions and add synthetic backends 2026-07-17 13:02:17 +07:00
Nils Andreas Svee 50f0dcb4dd perf(baseliner): bound the reselection channel to prevent memory buildup
Change the reselection channel from unbounded to a size of 1 and use non-blocking sends to avoid unnecessary queuing of reselection triggers.
2026-07-17 11:39:07 +07:00
Nils Andreas Svee 9811592f13 feat(platform): introduce abstractions for cross-platform interface statistics and traffic control
Decouple the rate controller from Linux-specific Netlink calls to support other operating systems. This adds a macOS backend for interface statistics and an "observe-only" mode for platforms where traffic shaping is not yet implemented.
2026-07-17 11:35:43 +07:00
Nils Andreas Svee c5d673fcb8 build(uci): restrict UCI dependency and feature to Linux targets
Ensure the UCI integration is only compiled on Linux, as the underlying native dependency is platform-specific. Added a build script to warn users when the feature is enabled on unsupported platforms.
2026-07-17 01:43:10 +07:00
Nils Andreas Svee b56c8b72cc refactor(main): modularize main loop and improve resource cleanup logic 2026-07-14 22:53:01 +07:00
Nils Andreas Svee 8b0c37bc83 refactor: simplify control flow and error handling in metrics and netlink 2026-07-05 12:44:44 +07:00
Nils Andreas Svee 694cad1fac chore(deps): update wasm and web-related dependencies 2026-07-03 00:50:00 +07:00
Nils Andreas Svee 908fbfdf04 refactor(deps): cleanup netlink-bindings and shlex dependencies 2026-07-03 00:50:00 +07:00
Nils Andreas Svee 2dacde8a3f chore(deps): update cargo dependencies 2026-07-03 00:50:00 +07:00
renovate[bot] 371b3efcdc chore(deps): update rust crate anyhow to v1.0.103 2026-07-03 00:47:45 +07:00
renovate[bot] 81d6ebe3a8 chore(deps): update rust crate log to v0.4.33 2026-07-03 00:47:45 +07:00
renovate[bot] 8ccbd30582 chore(deps): update actions/checkout action to v7 2026-07-03 00:47:45 +07:00
renovate[bot] ae3cd96313 fix(deps): update netlink-socket2 digest to 0b609e9 2026-07-03 00:47:45 +07:00
renovate[bot] 3d1c14d312 fix(deps): update netlink-bindings digest to 0b609e9 2026-07-03 00:46:39 +07:00
renovate[bot] 7955046426 fix(deps): update rust-uci digest to 3e7b615 2026-07-03 00:46:39 +07:00
renovate[bot] 30e3281178 chore(deps): update google/osv-scanner-action action to v2.3.8 2026-07-03 00:46:39 +07:00
renovate[bot] f3f033cf0f chore(deps): update rust crate libc to v0.2.186 2026-07-03 00:46:39 +07:00
Nils Andreas Svee 4aaf8fa100 chore: add SPDX license identifiers and copyright headers to source files
Apply the MPL-2.0 license to all source files and include the full license text to ensure legal compliance and proper attribution.
2026-04-23 12:39:52 +07:00
Nils Andreas Svee dfa11be236 chore: add MPLv2.0 license
The original Lua sqm-autorate project is licensed under the MPLv2.0, and this project started life as a pure port of that. Thus our code should also be licensed under the MPL
2026-04-23 12:19:49 +07:00
Nils Andreas Svee 58d536bb22 refactor(pinger): replace hashlru with standard HashMap and manual TTL cleanup
Remove the hashlru dependency to reduce bloat, replacing it with a Mutex-wrapped HashMap and manual entry expiration logic.
2026-04-23 00:33:07 +07:00
Nils Andreas Svee 6f16f3a93c build(deps): disable default features for hashlru to reduce dependency bloat
Remove unused dependencies including ahash, hashbrown, and zerocopy by disabling default features for hashlru.
2026-04-23 00:19:00 +07:00
Nils Andreas Svee 1b959d52ed feat(main): Initialize and pass InFlightProbeCache to pinger threads 2026-04-23 00:16:19 +07:00
Nils Andreas Svee 8c29d7b251 feat(pinger): Integrate in-flight probe cache into sender and listener 2026-04-23 00:16:19 +07:00
Nils Andreas Svee 80adb368d4 feat(pinger): Implement new craft_packet signature for ICMP types 2026-04-23 00:16:19 +07:00
Nils Andreas Svee 6ae19b619a feat(pinger): Introduce InFlightProbeCache and update Ping traits 2026-04-23 00:16:19 +07:00
Nils Andreas Svee 7250050f63 feat(deps): Add hashlru for in-flight probe caching 2026-04-23 00:16:19 +07:00
Nils Andreas Svee 3322e82b6c refactor: replace std::sync::mpsc with flume for improved channel performance
Migrate internal message passing to flume to leverage better performance and additional features like eventual fairness and selection.
2026-04-22 22:39:06 +07:00
Nils Andreas Svee ea19c6d68b style: remove unused synchronization imports
Eliminate unused Arc, Mutex, and RwLock imports to clean up codebase and reduce compiler warnings.
2026-04-22 22:26:44 +07:00
Nils Andreas Svee a81d208c0d style: refine logging verbosity and improve diagnostic messages
Demote baseline telemetry to debug level, add context to rate controller warnings, and remove redundant string conversions in reflector selection logs.
2026-04-22 22:09:26 +07:00
Nils Andreas Svee e22c100e57 refactor(main): Relocate and enhance signal handler with improved logging 2026-04-22 21:45:27 +07:00
Nils Andreas Svee 72fe0230ee style: Apply general code formatting and import reordering 2026-04-22 21:45:25 +07:00
Nils Andreas Svee 6690402a34 chore(deps): update dependencies and switch rust-uci to git revision
Update bindgen, nom, and other core dependencies while switching rust-uci and libuci-sys to a specific git revision to ensure compatibility with recent project changes.
2026-04-22 16:55:46 +07:00
Nils Andreas Svee 3ec53a1224 refactor: Migrate configuration to new UCI path prefix and boolean helper 2026-04-22 16:55:05 +07:00
Nils Andreas Svee 7d5c2f0275 refactor: Add boolean configuration helper and streamline host tag retrieval 2026-04-22 16:55:05 +07:00
Nils Andreas Svee 45da0c030e feat: Add flexible boolean parsing for configuration 2026-04-22 16:55:05 +07:00
Nils Andreas Svee 33169182be chore(deps): update rust-uci repository source
Switch the rust-uci dependency to the namib-project fork while maintaining the same revision to ensure availability and consistency with project requirements.
2026-04-22 15:11:16 +07:00
Nils Andreas Svee 2aea1bea86 chore(deps): switch rust-uci to git dependency
Update rust-uci to use a specific git revision from the upstream repository to incorporate latest changes not yet available in the published version.
2026-04-22 15:06:45 +07:00
Nils Andreas Svee d0e3394f03 refactor: add ArcMutex/ArcRwLock type aliases
Replace repeated Arc> and Arc> with concise type aliases for readability.
2026-04-18 19:04:35 +02:00
Nils Andreas Svee 7c40990e93 chore(deps): switch netlink dependencies to upstream git and remove vendor patch
Replace the local vendored patch for netlink-socket2 with a direct reference to the upstream git repository to maintain the fix for musl targets.

This should be changed back to using a release once the fix has a published release
2026-04-18 15:41:02 +07:00
renovate[bot] a739d1a5e2 chore(deps): update google/osv-scanner-action action to v2.3.5 2026-04-16 23:37:02 +07:00
Nils Andreas Svee 11837195de chore(deps): update rust dependencies
Update several crates including cc, cmake, fastrand, and quote to their latest versions to maintain up-to-date build tools and libraries.
2026-04-14 16:04:02 +07:00
renovate[bot] 706d93f146 chore(deps): update rust crate libc to v0.2.185 2026-04-14 16:00:57 +07:00
Nils Andreas Svee 7b0eb942ea fix(deps): patch netlink-socket2 to support musl targets
Vendor netlink-socket2 to resolve compilation failures caused by direct struct-literal initialization of libc types on musl.
2026-04-14 15:00:49 +07:00
Nils Andreas Svee 5daccecc1d fix(deps): update rust crate netlink-bindings to 0.3
Adjust netlink and traffic control logic to accommodate breaking API changes, including shifts from method calls to direct field access.
2026-04-14 13:09:50 +07:00
renovate[bot] 2c336799b8 fix(deps): update rust crate netlink-socket2 to 0.3 2026-04-14 12:07:32 +07:00
Nils Andreas Svee 8739ed4419 fix(metrics): use AtomicU32 for drop counter to support MIPS targets
Also bumps version to 0.4.1.
2026-04-14 02:14:55 +07:00
Nils Andreas Svee 99e80c381d build: bump package version to 0.4.0 2026-04-14 02:09:20 +07:00
Nils Andreas Svee 3c689b1d1e feat(main): Orchestrate application shutdown and cleanup 2026-04-14 02:03:53 +07:00
Nils Andreas Svee bda1a44b6e feat(threads): Add graceful shutdown checks to worker loops 2026-04-14 02:03:53 +07:00
Nils Andreas Svee 6e4e6b85e9 feat(shutdown): Implement global shutdown signal handling 2026-04-14 02:03:53 +07:00
Nils Andreas Svee f8976a78a2 feat(metrics): support arbitrary event tags and enhance monitoring coverage
Enable key-value metadata for events to track dry-run status and introduce new monitoring events for rate controller failure states.
2026-04-14 01:45:07 +07:00
Nils Andreas Svee 60e7ffc9e7 refactor(main): integrate MetricsSender across application entry point 2026-04-13 18:57:51 +07:00
Nils Andreas Svee 2db63e6159 refactor(reflector_selector): migrate to MetricsSender for event dispatch 2026-04-13 18:57:50 +07:00
Nils Andreas Svee 3095e32d41 refactor(ratecontroller): migrate to MetricsSender for metric dispatch 2026-04-13 18:57:50 +07:00
Nils Andreas Svee 57382465bf refactor(pinger): migrate to MetricsSender for metric dispatch 2026-04-13 18:57:50 +07:00
Nils Andreas Svee e9e233d98a refactor(baseliner): migrate to MetricsSender for metric dispatch 2026-04-13 18:57:50 +07:00
Nils Andreas Svee 8036d27e6f feat(metrics): track and report dropped metric counts 2026-04-13 18:57:50 +07:00
Nils Andreas Svee 451d42640b refactor(metrics): encapsulate metric dispatch with MetricsSender 2026-04-13 18:57:50 +07:00
Nils Andreas Svee b930a01fd6 feat(observability): emit a startup event metric to signal application initialization
Send a telemetry event when the process starts to provide better visibility into application lifecycle transitions.
2026-04-13 17:49:19 +07:00
Nils Andreas Svee e7cbdbb265 docs(observability): document rationale for unbatched UDP metric flushing
Explain that individual metric delivery prevents data truncation caused by small UDP receive buffers on the remote host.
2026-04-13 17:36:30 +07:00
Nils Andreas Svee 1d7fd5800b refactor(observability): initialize metrics collection earlier in the startup sequence
Move the metrics thread and error channel initialization before the settling period and peer setup to ensure early telemetry is captured.
2026-04-13 17:34:17 +07:00
Nils Andreas Svee 699c8782d9 fix(observability): send individual metrics over UDP to avoid packet size limits
Differentiate flushing logic between transport types to prevent UDP payload truncation while maintaining batched delivery for TCP.
2026-04-13 17:21:14 +07:00
Nils Andreas Svee 0f902e27a3 perf(observability): optimize metric flushing to reduce memory allocations
Use a pre-allocated buffer for metric serialization to improve efficiency when processing large batches.
2026-04-13 16:54:33 +07:00
Nils Andreas Svee 530a715bdf fix(observability): refresh metric timestamps on each loop iteration
Move clock initialization inside processing loops to ensure exported metrics reflect the current time rather than a stale timestamp.
2026-04-13 16:32:30 +07:00
Nils Andreas Svee dd8735f46e chore(deps): update icmp-socket2 to 0.4.2
Update the icmp-socket2 dependency version in Cargo.toml and Cargo.lock to incorporate the latest upstream changes.
2026-04-13 16:23:20 +07:00
Nils Andreas Svee 72b4bc8dab fix(observability): ensure all metric outputs are newline-terminated
Append trailing newlines to metric string formats to ensure consistent parsing and record separation by monitoring backends.
2026-04-13 15:43:09 +07:00
Nils Andreas Svee 757a715a54 chore: cleanup unused imports 2026-04-13 15:29:04 +07:00
Nils Andreas Svee 5af5b6597b feat(observability): report reflector selection events and outcomes
Emit metrics detailing why a reflector reselection was triggered and which peers were selected to improve selection process visibility.
2026-04-13 15:28:56 +07:00
Nils Andreas Svee d1ea0d0238 feat(observability): export rate and load metrics from rate controller
Send rates, load statistics, and delay deltas via the metrics channel to enable real-time monitoring of network performance.
2026-04-13 15:22:24 +07:00
Nils Andreas Svee da3487e506 feat(observability): make metrics collection optional and granular
Wrap metrics senders in Option and conditionally initialize the monitoring thread to support granular configuration and reduced overhead.
2026-04-13 15:22:15 +07:00
Nils Andreas Svee 671e3f2671 refactor: standardize channel naming with tx and rx suffixes
Rename internal channel senders and receivers to follow standard Rust conventions for improved code consistency and readability.
2026-04-13 15:07:00 +07:00
Nils Andreas Svee e5ba021a96 feat(observability): Pass metrics sender to rate controller and reflector selector 2026-04-13 15:01:46 +07:00
Nils Andreas Svee 2c1e7c0483 feat(observability): Export baseline statistics and events 2026-04-13 15:01:46 +07:00
Nils Andreas Svee 645ac08daa feat(observability): Export ping related metrics 2026-04-13 15:01:46 +07:00
Nils Andreas Svee 1452409fe8 feat(observability): Integrate metrics exporter into main application flow 2026-04-13 15:01:46 +07:00
Nils Andreas Svee 44f551d38c feat(observability): Add InfluxDB Line Protocol metrics module and config 2026-04-13 15:01:45 +07:00
Nils Andreas Svee 853f807e2c feat: add metrics module for monitoring
Initialize the Metrics structure to receive and process ping statistics as part of the monitoring subsystem.
2026-04-13 12:11:05 +07:00
Nils Andreas Svee f0fb3c9640 feat(monitoring): add dry-run mode to simulate qdisc rate changes
Enable a monitoring-only mode that logs intended bandwidth adjustments without applying them to the system via netlink.
2026-04-13 12:11:05 +07:00
Nils Andreas Svee 6d44556d63 style: reformat thread builder calls in main.rs
Standardize the formatting of thread spawning for the receiver and sender to improve code consistency and readability.
2026-04-13 12:06:50 +07:00
53 changed files with 8105 additions and 1922 deletions

No files matched your search

+114
View File
@@ -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
+110
View File
@@ -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
+66
View File
@@ -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
+35
View File
@@ -0,0 +1,35 @@
name: GitHub Actions Security
on:
push:
branches: [master]
pull_request:
branches: [master]
schedule:
- cron: "11 3 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
zizmor:
name: zizmor
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
with:
persona: regular
inputs: |
.github/workflows
.gitea/workflows
-22
View File
@@ -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
+55
View File
@@ -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
+7 -11
View File
@@ -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:
+12 -33
View File
@@ -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/
+47 -41
View File
@@ -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:
permissions:
contents: read
security-events: write
actions: read
env:
CARGO_TERM_COLOR: always
jobs: jobs:
rust-clippy-analyze: clippy-sarif:
name: Run rust-clippy analyzing name: Clippy SARIF
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: timeout-minutes: 25
contents: read
security-events: write
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
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
View File
File diff suppressed because it is too large. Load diff
+27 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "sqm-autorate-rust" name = "sqm-autorate-rust"
version = "0.3.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.1" 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", "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]
uci = ["dep:rust-uci"] default = ["irtt", "toml", "uci-vendored"]
irtt = ["dep:irtt-client"]
toml = ["config/toml"]
uci = ["dep:rust-uci"]
uci-vendored = ["uci", "rust-uci/vendored"]
[workspace]
members = ["crates/uci-derive"]
resolver = "3"
+40
View File
@@ -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"]
+7
View File
@@ -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
+12 -9
View File
@@ -11,10 +11,10 @@ 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}"
} }
} }
@@ -25,7 +25,7 @@ pipeline {
# 1. Set CARGO_HOME to a writable directory in the workspace # 1. Set CARGO_HOME to a writable directory in the workspace
export CARGO_HOME="$(pwd)/.cargo" export CARGO_HOME="$(pwd)/.cargo"
mkdir -p "$CARGO_HOME" mkdir -p "$CARGO_HOME"
# 2. Add local bin to PATH (for tools you might install) # 2. Add local bin to PATH (for tools you might install)
export PATH="$(pwd)/.local/bin:${PATH}" export PATH="$(pwd)/.local/bin:${PATH}"
@@ -44,14 +44,17 @@ 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..."
# Ensure rust-src is installed (needed for build-std) # Ensure rust-src is installed (needed for build-std)
# We use '|| true' so it doesn't fail if already installed or network flakes # We use '|| true' so it doesn't fail if already installed or network flakes
rustup component add rust-src || true rustup component add rust-src || true
cargo build --release -Z build-std=std,panic_abort cargo build --release -Z build-std=std,panic_abort
else else
echo "Detected Stable toolchain. Building with standard pre-compiled library..." echo "Detected Stable toolchain. Building with standard pre-compiled library..."
@@ -71,7 +74,7 @@ pipeline {
# 2. Verify it exists # 2. Verify it exists
if [ -f "$BINARY_PATH" ]; then if [ -f "$BINARY_PATH" ]; then
echo "Found compiled binary at: $BINARY_PATH" echo "Found compiled binary at: $BINARY_PATH"
# 3. Rename with the matrix tag (e.g., sqm-autorate-rust-aarch64-musl) # 3. Rename with the matrix tag (e.g., sqm-autorate-rust-aarch64-musl)
# This ensures unique filenames in the shared workspace root # This ensures unique filenames in the shared workspace root
cp "$BINARY_PATH" "sqm-autorate-rust-${TARGET}" cp "$BINARY_PATH" "sqm-autorate-rust-${TARGET}"
@@ -82,7 +85,7 @@ pipeline {
exit 1 exit 1
fi fi
''' '''
// 4. Archive the specific file we just copied // 4. Archive the specific file we just copied
archiveArtifacts artifacts: "sqm-autorate-rust-${TARGET}", fingerprint: true, onlyIfSuccessful: true archiveArtifacts artifacts: "sqm-autorate-rust-${TARGET}", fingerprint: true, onlyIfSuccessful: true
} }
+373
View File
@@ -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.
+373
View File
@@ -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.
+26
View File
@@ -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");
}
Executable
+19
View File
@@ -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
+16
View File
@@ -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"
+277
View File
@@ -0,0 +1,277 @@
// 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! {
#[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! {
#[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 = \"...\")]`",
)
})
}
+50
View File
@@ -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 = []
-305
View File
@@ -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
1 reflector_ip ip_version description
2 94.140.14.14 4 AdGuard
3 94.140.15.15 4 AdGuard
4 94.140.14.15 4 AdGuard Family
5 94.140.15.16 4 AdGuard Family
6 94.140.14.140 4 AdGuard Unfiltered
7 94.140.14.141 4 AdGuard Unfiltered
8 205.171.3.65 4 CenturyLink/Level3
9 205.171.2.65 4 CenturyLink/Level3
10 185.228.168.9 4 CleanBrowsing
11 185.228.169.9 4 CleanBrowsing
12 185.228.168.168 4 CleanBrowsing
13 185.228.169.168 4 CleanBrowsing
14 185.228.168.10 4 CleanBrowsing
15 185.228.169.11 4 CleanBrowsing
16 8.26.56.26 4 Comodo
17 8.20.247.20 4 Comodo
18 84.200.69.80 4 DNS.WATCH
19 84.200.70.40 4 DNS.WATCH
20 46.227.200.54 4 FAELIX Anycast
21 46.227.200.55 4 FAELIX Anycast
22 45.33.97.5 4 FreeDNS
23 37.235.1.177 4 FreeDNS
24 74.82.42.42 4 Hurricane Electric
25 194.242.2.2 4 Mullvad
26 194.242.2.3 4 Mullvad
27 193.19.108.2 4 Mullvad
28 193.19.108.3 4 Mullvad
29 208.67.222.222 4 OpenDNS
30 208.67.220.220 4 OpenDNS
31 208.67.222.123 4 OpenDNS Family
32 208.67.220.123 4 OpenDNS Family
33 208.67.222.2 4 OpenDNS Sbx
34 208.67.220.2 4 OpenDNS Sbx
35 159.89.120.99 4 OpenNIC
36 134.195.4.2 4 OpenNIC
37 9.9.9.9 4 Quad9
38 9.9.9.10 4 Quad9
39 9.9.9.11 4 Quad9
40 149.112.112.10 4 Quad9
41 149.112.112.11 4 Quad9
42 149.112.112.112 4 Quad9
43 176.9.93.198 4 dnsforge.de
44 176.9.1.117 4 dnsforge.de
45 104.236.210.29 4 DNSReactor
46 37.235.1.174 4 FreeDNS
47 81.218.119.1 4 GreenTeamDNS
48 194.8.57.12 4 Nuernberg Internet Exchange (N-IX)
49 51.89.30.51 4 PublicDNS.eu
50 51.89.93.25 4 PublicDNS.eu
51 130.59.31.248 4 SWITCH
52 130.59.31.251 4 SWITCH
53 185.49.141.37 4 GetDNS
54 158.64.1.29 4 Restena Foundation
55 89.234.186.112 4 Neutopia
56 185.95.218.42 4 Digitale Gesellschaft Schweiz
57 185.95.218.43 4 Digitale Gesellschaft Schweiz
58 140.238.215.192 4 post-factum
59 199.58.81.218 4 CMRG DNS
60 146.255.56.98 4 Foundation for Applied Privacy
61 185.222.222.222 4 dns.sb
62 185.184.222.222 4 dns.sb
63 5.1.66.255 4 Freifunk München e.V.
64 80.80.80.80 4 Freenom World
65 80.80.81.81 4 Freenom World
66 80.67.169.12 4 French Data Network (FDN)
67 80.67.169.40 4 French Data Network (FDN)
68 217.24.255.134 4 Albtelecom Sh.a.
69 213.163.127.229 4 Telekomi i Kosoves SH.A.
70 85.94.178.198 4 ANDORRA TELECOM SAU
71 186.148.128.86 4 ETERNET S.R.L.
72 190.151.144.21 4 Lima Video Cable S.A. (Cabletel)
73 200.45.184.43 4 Telecom Argentina S.A.
74 179.60.232.10 4 WICORP SA
75 170.210.83.110 4 Red de Interconexion Universitaria
76 190.57.234.194 4 Gigared S.A.
77 186.153.180.148 4 Telecom Argentina S.A.
78 81.16.8.110 4 Ucom CJSC
79 45.133.105.123 4 GNC-Alfa CJSC
80 61.8.0.113 4 Telstra Corporation Ltd
81 103.224.162.40 4 Ezi-Web
82 139.130.4.4 4 Telstra Corporation Ltd
83 139.134.5.51 4 Telstra Corporation Ltd
84 139.134.2.190 4 Telstra Corporation Ltd
85 203.50.2.71 4 Telstra Corporation Ltd
86 83.137.41.8 4 nemox.net Informationstechnologie OG
87 83.137.41.9 4 nemox.net Informationstechnologie OG
88 194.36.144.87 4 netcup GmbH
89 188.118.227.21 4 Hutchison Drei Austria GmbH
90 83.218.176.140 4 myNet GmbH
91 85.132.85.85 4 Delta Telecom Ltd
92 45.250.253.222 4 New Generation Internet Services Limited
93 103.77.188.18 4 NETSCOPE
94 103.77.188.19 4 NETSCOPE
95 103.80.1.2 4 SS Online
96 103.153.154.2 4 Pabna Cable Vision Dot Net
97 114.130.5.5 4 Tire-1 IP Transit Provider of Bangladesh
98 114.130.5.6 4 Tire-1 IP Transit Provider of Bangladesh
99 103.146.221.20 4 Kloud Technologies Limited
100 103.146.221.21 4 Kloud Technologies Limited
101 115.127.19.129 4 BRACNet Limited
102 115.127.19.137 4 BRACNet Limited
103 103.113.200.10 4 National University
104 177.131.114.86 4 ACESSOLINE TELECOMUNICACOES LTDA
105 177.66.203.10 4 Downup Telecomunicacoes e servico LTDA
106 177.184.131.180 4 SOBRALNET SERVICOS E TELECOMUNICACOES LTDA - ME
107 186.194.224.82 4 R&R PROVEDOR DE INTERNET LTDA
108 186.225.194.29 4 Pannet Serviços On Line Ltda
109 189.42.239.34 4 CLARO S.A.
110 200.99.138.94 4 TELEFÔNICA BRASIL S.A
111 200.99.138.103 4 TELEFÔNICA BRASIL S.A
112 200.167.191.114 4 CLARO S.A.
113 200.169.8.1 4 Century Telecom Ltda
114 141.0.102.202 4 Altibox AS
115 62.97.223.62 4 BKK Digitek AS
116 217.18.206.12 4 EVRY NORGE AS
117 217.18.206.22 4 EVRY NORGE AS
118 195.204.130.85 4 GLOBALCONNECT AS
119 81.175.38.1 4 GLOBALCONNECT AS
120 46.182.19.48 4 Martin Prager trading as NbIServ
121 82.96.64.2 4 Probe Networks
122 94.247.43.254 4 OpenNIC
123 195.10.195.195 4 meerfarbig GmbH & Co. KG
124 194.172.160.4 4 Baumann Technologie GmbH
125 185.233.106.232 4 netcup GmbH
126 185.233.107.4 4 netcup GmbH
127 141.1.1.1 4 Vodafone Group PLC
128 141.1.27.249 4 Vodafone Group PLC
129 176.103.130.130 4 Serveroid LLC
130 176.103.130.131 4 Serveroid LLC
131 80.243.64.67 4 PJSC Vimpelcom
132 195.218.133.88 4 PJSC Vimpelcom
133 83.69.120.1 4 PJSC Vimpelcom
134 46.16.229.223 4 JSC Elektrosvyaz
135 193.183.98.66 4 Prometeus di Daniela Agro
136 212.19.96.2 4 Leonet4cloud srl
137 217.12.181.97 4 IRIDEOS S.P.A.
138 62.149.128.4 4 Aruba S.p.A.
139 62.149.132.2 4 Aruba S.p.A.
140 85.47.198.82 4 Telecom Italia S.p.A.
141 202.136.162.11 4 ASN - NTT SINGAPORE PTE LTD
142 51.79.144.245 4 OVH SAS
143 61.46.244.211 4 Jupiter Telecommunications Co. Ltd.
144 1.33.184.173 4 NTT PC Communications Inc.
145 1.33.184.116 4 NTT PC Communications Inc.
146 1.33.184.174 4 NTT PC Communications Inc.
147 1.33.184.108 4 NTT PC Communications Inc.
148 1.33.184.220 4 NTT PC Communications Inc.
149 1.33.184.230 4 NTT PC Communications Inc.
150 1.33.184.158 4 NTT PC Communications Inc.
151 62.212.113.125 4 Nerim SAS
152 185.30.211.154 4 ENTREPRISE DECIMA SARL
153 87.240.90.128 4 KERTEL SAS
154 45.77.61.165 4 The Constant Company LLC
155 178.62.57.141 4 DigitalOcean LLC
156 194.145.240.6 4 M247 Ltd
157 195.27.1.1 4 Vodafone Group PLC
158 81.17.66.13 4 South West Communications Group Ltd
159 87.117.196.200 4 IOMART CLOUD SERVICES LIMITED
160 213.123.251.87 4 British Telecommunications PLC
161 81.149.21.254 4 British Telecommunications PLC
162 196.21.247.1 4 TENET (The UNINET Project)
163 89.140.69.49 4 VODAFONE ONO S.A.
164 62.97.123.107 4 COLT Technology Services Group Limited
165 194.177.210.210 4 National Infrastructures for Research and Technology S.A.
166 168.126.63.1 4 Korea Telecom
167 121.254.134.99 4 LG DACOM Corporation
168 203.239.131.1 4 ELIMNET INC.
169 202.57.32.49 4 A Multihomed ISP Company
170 81.17.31.34 4 Private Layer INC
171 79.141.82.250 4 NTD SA
172 80.254.77.39 4 Monzoon Networks AG
173 85.93.212.2 4 Visual Online S.A.
174 196.223.158.58 4 MAX NET FOR INTERNET SERVICES
175 89.150.195.195 4 Media Network i Halmstad AB
176 130.244.126.100 4 TELE2
177 87.96.144.179 4 A3 Sverige AB
178 146.247.80.144 4 A3 Sverige AB
179 46.246.29.69 4 GleSYS AB
180 203.210.237.165 4 VNPT Corp
181 113.190.253.229 4 VNPT Corp
182 119.29.29.29 4 Shenzhen Tencent Computer Systems Company Limited
183 209.193.4.7 4 Alaska Communications
184 209.112.160.2 4 Alaska Communications
185 216.68.4.10 4 Cincinnati Bell ZoomTown
186 216.68.5.10 4 Cincinnati Bell ZoomTown
187 1.250.0.14 4 Verizon (Newark NJ)
188 1.252.0.14 4 Verizon (Reston VA)
189 1.252.0.12 4 Verizon (Reston VA)
190 8.238.120.14 4 Verizon (Atlanta GA)
191 8.238.120.12 4 Verizon (Atlanta GA)
192 78.143.192.10 4 Fast (UK)
193 201.220.136.186 4 CABLECOLOR S.A.
194 190.124.166.8 4 Redes y Telecomunicaciones
195 190.64.140.243 4 Administracion Nacional de Telecomunicaciones
196 200.40.230.36 4 Administracion Nacional de Telecomunicaciones
197 134.249.156.228 4 Kyivstar PJSC
198 195.60.70.5 4 VAK Ltd.
199 62.140.239.1 4 LLC TRC FIORD
200 94.158.153.236 4 Comfo LTD
201 109.254.13.190 4 Donbass Electronic Communications Ltd.
202 176.67.0.60 4 LLC McLaut-Invest
203 193.34.173.206 4 New Information Systems PP
204 94.153.241.134 4 Kyivstar PJSC
205 216.104.206.136 4 Africa Online Uganda
206 154.72.212.38 4 National Information Technology Authority Uganda
207 217.174.226.78 4 State Company of Electro Communications Turkmentelecom
208 46.45.187.181 4 Radore Veri Merkezi Hizmetleri A.S.
209 46.45.187.163 4 Radore Veri Merkezi Hizmetleri A.S.
210 46.45.187.184 4 Radore Veri Merkezi Hizmetleri A.S.
211 46.45.187.186 4 Radore Veri Merkezi Hizmetleri A.S.
212 46.20.159.27 4 DoraTelekom
213 93.113.60.84 4 Netinternet Bilisim Teknolojileri AS
214 93.113.60.82 4 Netinternet Bilisim Teknolojileri AS
215 196.3.132.153 4 Telecommunication Services of Trinidad and Tobago
216 196.3.132.154 4 Telecommunication Services of Trinidad and Tobago
217 80.248.72.1 4 CAFE Informatique et telecommunications
218 117.121.215.102 4 UIH
219 1.20.151.94 4 TOT Public Company Limited
220 117.121.215.101 4 UIH
221 122.154.230.146 4 The Communication Authoity of Thailand CAT
222 101.101.101.101 4 Taiwan Network Information Center
223 101.102.103.104 4 Taiwan Network Information Center
224 113.196.55.130 4 New Century InfoComm Tech Co. Ltd.
225 210.242.68.59 4 Data Communication Business Group
226 92.60.50.40 4 BONET Systems s.r.o.
227 217.73.17.110 4 VNET a.s.
228 84.245.69.193 4 SWAN a.s.
229 178.254.129.65 4 Drustvo za telekomunikacije Orion telekom doo Beograd-Zemun
230 93.86.63.54 4 TELEKOM SRBIJA a.d.
231 109.92.22.97 4 TELEKOM SRBIJA a.d.
232 197.159.166.81 4 Companhia Santomense de Telecomunicacoes
233 208.84.200.28 4 Columbus Communication St. Vincent and the Grenadines Ltd.
234 208.94.176.225 4 Karib Cable
235 199.116.57.181 4 IP SOLUTIONS INC.
236 74.120.24.129 4 IP SOLUTIONS INC.
237 74.120.24.81 4 IP SOLUTIONS INC.
238 74.120.24.97 4 IP SOLUTIONS INC.
239 78.130.38.222 4 NOS COMUNICACOES S.A.
240 195.22.25.130 4 Claranet Limited
241 213.241.88.98 4 Netia SA
242 87.204.28.12 4 Netia SA
243 212.160.85.232 4 Orange Polska Spolka Akcyjna
244 45.173.200.10 4 INVERSIONES EN TELECOMUNICACIONES DIGITALES S.A.C
245 200.10.231.110 4 Centro Nacional de Computacion
246 201.222.48.206 4 TECNOLOGIA EN ELECTRONICA E INFORMATICA SOCIEDAD ANONIMA (T.E.I.S.A)
247 201.222.50.30 4 TECNOLOGIA EN ELECTRONICA E INFORMATICA SOCIEDAD ANONIMA (T.E.I.S.A)
248 201.222.55.246 4 TECNOLOGIA EN ELECTRONICA E INFORMATICA SOCIEDAD ANONIMA (T.E.I.S.A)
249 103.209.52.250 4 The Professional Communications Pvt Ltd
250 119.63.132.26 4 Transworld Associates (Pvt.) Ltd.
251 83.143.8.249 4 MTN NIGERIA Communication limited
252 165.98.138.194 4 Amnet Telecomunicaciones S.A.
253 203.81.75.37 4 Myanma Posts and Telecommunications
254 65.18.114.254 4 Telecom International Myanmar Co. Ltd
255 65.18.117.174 4 Telecom International Myanmar Co. Ltd
256 69.160.0.86 4 OOREDOO MYANMAR
257 103.154.241.252 4 Myanmar Link Telecommunication Ltd
258 66.181.167.232 4 The first E-commerce and TriplePlay Service ISP in Mongolia.
259 202.21.99.42 4 Mobinet LLC. AS Mobinet Internet Service Provider
260 202.131.241.74 4 Mobinet LLC. AS Mobinet Internet Service Provider
261 124.158.93.2 4 STXCitinet. Leading Internet & VOIP Service Provider. Ulaanbaatar. Mongolia
262 77.89.232.86 4 ORANGE MOLDOVA S.A.
263 200.56.224.11 4 Marcatel Com S.A. de C.V.
264 201.144.135.211 4 Marcatel Com S.A. de C.V.
265 201.134.115.36 4 Uninet S.A. de C.V.
266 1.32.58.251 4 TM Net. Internet Service Provider
267 202.86.149.20 4 Companhia de Telecomunicacoes de Macau SARL
268 202.175.45.2 4 Companhia de Telecomunicacoes de Macau SARL
269 202.86.191.50 4 Companhia de Telecomunicacoes de Macau SARL
270 80.250.60.241 4 Sia Nano IT
271 101.78.12.50 4 Enterprise of Telecommunications Lao
272 168.187.161.250 4 Zajil International Telecom Company W.L.L.
273 62.12.114.25 4 Angani Limited
274 103.134.220.158 4 PT Global Media Pratama Solusindo
275 121.100.28.163 4 PT. Sakti Putra Mandiri
276 103.28.114.57 4 Lintas Data Prima PT
277 202.51.111.149 4 PT iForte Global Internet
278 103.23.20.23 4 PT Infinys System Indonesia
279 103.98.176.24 4 Universitas PGRI Semarang
280 103.98.176.143 4 Universitas PGRI Semarang
281 119.235.48.3 4 VAINAVI INDUSTIES LTD. INTERNET SERVICE PROVIDER. INDIA
282 103.194.120.33 4 GOLDY DIGINET PRIVATE LIMITED
283 103.194.120.3 4 GOLDY DIGINET PRIVATE LIMITED
284 103.194.120.129 4 GOLDY DIGINET PRIVATE LIMITED
285 223.31.121.171 4 Power Grid Corporation of India Limited
286 169.38.73.5 4 SoftLayer Technologies Inc.
287 202.86.251.201 4 Artech Infosystems Pvt Ltd
288 202.164.44.246 4 Quadrant Televentures Limited
289 87.229.99.1 4 SigmaNet Szolgaltato es Kereskedelm Ltd
290 91.137.135.75 4 OPC Networks Kft.
291 85.90.160.69 4 OPC Networks Kft.
292 195.228.219.226 4 Magyar Telekom plc.
293 103.6.168.114 4 NewMedia Express Pte Ltd
294 45.71.185.100 4 NEDETEL S.A.
295 186.149.243.52 4 ALTICE DOMINICANA S.A.
296 193.17.47.1 4 CZ.NIC z.s.p.o.
297 195.113.144.194 4 CZ.NIC z.s.p.o.
298 213.211.50.1 4 itself s.r.o.
299 213.211.50.2 4 itself s.r.o.
300 185.43.135.1 4 CZ.NIC z.s.p.o.
301 90.183.151.107 4 O2 Czech Republic. a.s.
302 27.50.20.21 4 PT. Mora Telematika Indonesia
303 116.193.190.164 4 PT Cloud Hosting Indonesia
304 103.56.148.179 4 PT Beon Intermedia
305 8.215.26.147 4 Alibaba.com Singapore E-Commerce Private Limited
+311
View File
@@ -0,0 +1,311 @@
host,country,protocols,irtt_port,irtt_hmac
94.140.14.14,CY,icmp-echo|icmp-timestamps,,
94.140.15.15,CY,icmp-echo|icmp-timestamps,,
94.140.14.15,CY,icmp-echo|icmp-timestamps,,
94.140.15.16,CY,icmp-echo|icmp-timestamps,,
94.140.14.140,CY,icmp-echo|icmp-timestamps,,
94.140.14.141,CY,icmp-echo|icmp-timestamps,,
205.171.3.65,US,icmp-echo|icmp-timestamps,,
205.171.2.65,US,icmp-echo|icmp-timestamps,,
185.228.168.9,US,icmp-echo|icmp-timestamps,,
185.228.169.9,US,icmp-echo|icmp-timestamps,,
185.228.168.168,US,icmp-echo|icmp-timestamps,,
185.228.169.168,US,icmp-echo|icmp-timestamps,,
185.228.168.10,US,icmp-echo|icmp-timestamps,,
185.228.169.11,US,icmp-echo|icmp-timestamps,,
8.26.56.26,US,icmp-echo|icmp-timestamps,,
8.20.247.20,US,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,GB,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,US,icmp-echo|icmp-timestamps,,
194.242.2.2,US,icmp-echo|icmp-timestamps,,
194.242.2.3,US,icmp-echo|icmp-timestamps,,
193.19.108.2,US,icmp-echo|icmp-timestamps,,
193.19.108.3,US,icmp-echo|icmp-timestamps,,
208.67.222.222,US,icmp-echo|icmp-timestamps,,
208.67.220.220,US,icmp-echo|icmp-timestamps,,
208.67.222.123,US,icmp-echo|icmp-timestamps,,
208.67.220.123,US,icmp-echo|icmp-timestamps,,
208.67.222.2,US,icmp-echo|icmp-timestamps,,
208.67.220.2,US,icmp-echo|icmp-timestamps,,
159.89.120.99,CA,icmp-echo|icmp-timestamps,,
134.195.4.2,US,icmp-echo|icmp-timestamps,,
9.9.9.9,US,icmp-echo|icmp-timestamps,,
9.9.9.10,US,icmp-echo|icmp-timestamps,,
9.9.9.11,US,icmp-echo|icmp-timestamps,,
149.112.112.10,US,icmp-echo|icmp-timestamps,,
149.112.112.11,US,icmp-echo|icmp-timestamps,,
149.112.112.112,US,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,DE,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,SM,icmp-echo|icmp-timestamps,,
80.80.81.81,SM,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,AU,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,AU,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,GB,icmp-echo|icmp-timestamps,,
141.1.27.249,GB,icmp-echo|icmp-timestamps,,
176.103.130.130,CY,icmp-echo|icmp-timestamps,,
176.103.130.131,CY,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,GB,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,CN,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,SI,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,
1 host country protocols irtt_port irtt_hmac
2 94.140.14.14 CY icmp-echo|icmp-timestamps
3 94.140.15.15 CY icmp-echo|icmp-timestamps
4 94.140.14.15 CY icmp-echo|icmp-timestamps
5 94.140.15.16 CY icmp-echo|icmp-timestamps
6 94.140.14.140 CY icmp-echo|icmp-timestamps
7 94.140.14.141 CY icmp-echo|icmp-timestamps
8 205.171.3.65 US icmp-echo|icmp-timestamps
9 205.171.2.65 US icmp-echo|icmp-timestamps
10 185.228.168.9 US icmp-echo|icmp-timestamps
11 185.228.169.9 US icmp-echo|icmp-timestamps
12 185.228.168.168 US icmp-echo|icmp-timestamps
13 185.228.169.168 US icmp-echo|icmp-timestamps
14 185.228.168.10 US icmp-echo|icmp-timestamps
15 185.228.169.11 US icmp-echo|icmp-timestamps
16 8.26.56.26 US icmp-echo|icmp-timestamps
17 8.20.247.20 US icmp-echo|icmp-timestamps
18 84.200.69.80 DE icmp-echo|icmp-timestamps
19 84.200.70.40 DE icmp-echo|icmp-timestamps
20 46.227.200.54 GB icmp-echo|icmp-timestamps
21 46.227.200.55 GB icmp-echo|icmp-timestamps
22 45.33.97.5 US icmp-echo|icmp-timestamps
23 37.235.1.177 AT icmp-echo|icmp-timestamps
24 74.82.42.42 US icmp-echo|icmp-timestamps
25 194.242.2.2 US icmp-echo|icmp-timestamps
26 194.242.2.3 US icmp-echo|icmp-timestamps
27 193.19.108.2 US icmp-echo|icmp-timestamps
28 193.19.108.3 US icmp-echo|icmp-timestamps
29 208.67.222.222 US icmp-echo|icmp-timestamps
30 208.67.220.220 US icmp-echo|icmp-timestamps
31 208.67.222.123 US icmp-echo|icmp-timestamps
32 208.67.220.123 US icmp-echo|icmp-timestamps
33 208.67.222.2 US icmp-echo|icmp-timestamps
34 208.67.220.2 US icmp-echo|icmp-timestamps
35 159.89.120.99 CA icmp-echo|icmp-timestamps
36 134.195.4.2 US icmp-echo|icmp-timestamps
37 9.9.9.9 US icmp-echo|icmp-timestamps
38 9.9.9.10 US icmp-echo|icmp-timestamps
39 9.9.9.11 US icmp-echo|icmp-timestamps
40 149.112.112.10 US icmp-echo|icmp-timestamps
41 149.112.112.11 US icmp-echo|icmp-timestamps
42 149.112.112.112 US icmp-echo|icmp-timestamps
43 176.9.93.198 DE icmp-echo|icmp-timestamps
44 176.9.1.117 DE icmp-echo|icmp-timestamps
45 104.236.210.29 US icmp-echo|icmp-timestamps
46 37.235.1.174 AT icmp-echo|icmp-timestamps
47 81.218.119.1 IL icmp-echo|icmp-timestamps
48 194.8.57.12 DE icmp-echo|icmp-timestamps
49 51.89.30.51 DE icmp-echo|icmp-timestamps
50 51.89.93.25 DE icmp-echo|icmp-timestamps
51 130.59.31.248 CH icmp-echo|icmp-timestamps
52 130.59.31.251 CH icmp-echo|icmp-timestamps
53 185.49.141.37 NL icmp-echo|icmp-timestamps
54 158.64.1.29 LU icmp-echo|icmp-timestamps
55 89.234.186.112 FR icmp-echo|icmp-timestamps
56 185.95.218.42 CH icmp-echo|icmp-timestamps
57 185.95.218.43 CH icmp-echo|icmp-timestamps
58 140.238.215.192 CH icmp-echo|icmp-timestamps
59 199.58.81.218 CA icmp-echo|icmp-timestamps
60 146.255.56.98 AT icmp-echo|icmp-timestamps
61 185.222.222.222 DE icmp-echo|icmp-timestamps
62 185.184.222.222 NL icmp-echo|icmp-timestamps
63 5.1.66.255 AT icmp-echo|icmp-timestamps
64 80.80.80.80 SM icmp-echo|icmp-timestamps
65 80.80.81.81 SM icmp-echo|icmp-timestamps
66 80.67.169.12 FR icmp-echo|icmp-timestamps
67 80.67.169.40 FR icmp-echo|icmp-timestamps
68 217.24.255.134 AL icmp-echo|icmp-timestamps
69 213.163.127.229 XK icmp-echo|icmp-timestamps
70 85.94.178.198 AD icmp-echo|icmp-timestamps
71 186.148.128.86 AR icmp-echo|icmp-timestamps
72 190.151.144.21 AR icmp-echo|icmp-timestamps
73 200.45.184.43 AR icmp-echo|icmp-timestamps
74 179.60.232.10 AR icmp-echo|icmp-timestamps
75 170.210.83.110 AR icmp-echo|icmp-timestamps
76 190.57.234.194 AR icmp-echo|icmp-timestamps
77 186.153.180.148 AR icmp-echo|icmp-timestamps
78 81.16.8.110 AM icmp-echo|icmp-timestamps
79 45.133.105.123 AM icmp-echo|icmp-timestamps
80 61.8.0.113 AU icmp-echo|icmp-timestamps
81 103.224.162.40 AU icmp-echo|icmp-timestamps
82 139.130.4.4 AU icmp-echo|icmp-timestamps
83 139.134.5.51 AU icmp-echo|icmp-timestamps
84 139.134.2.190 AU icmp-echo|icmp-timestamps
85 203.50.2.71 AU icmp-echo|icmp-timestamps
86 83.137.41.8 AT icmp-echo|icmp-timestamps
87 83.137.41.9 AT icmp-echo|icmp-timestamps
88 194.36.144.87 DE icmp-echo|icmp-timestamps
89 188.118.227.21 AT icmp-echo|icmp-timestamps
90 83.218.176.140 AT icmp-echo|icmp-timestamps
91 85.132.85.85 AZ icmp-echo|icmp-timestamps
92 45.250.253.222 US icmp-echo|icmp-timestamps
93 103.77.188.18 BD icmp-echo|icmp-timestamps
94 103.77.188.19 BD icmp-echo|icmp-timestamps
95 103.80.1.2 BD icmp-echo|icmp-timestamps
96 103.153.154.2 BD icmp-echo|icmp-timestamps
97 114.130.5.5 BD icmp-echo|icmp-timestamps
98 114.130.5.6 BD icmp-echo|icmp-timestamps
99 103.146.221.20 BD icmp-echo|icmp-timestamps
100 103.146.221.21 BD icmp-echo|icmp-timestamps
101 115.127.19.129 BD icmp-echo|icmp-timestamps
102 115.127.19.137 BD icmp-echo|icmp-timestamps
103 103.113.200.10 BD icmp-echo|icmp-timestamps
104 177.131.114.86 BR icmp-echo|icmp-timestamps
105 177.66.203.10 BR icmp-echo|icmp-timestamps
106 177.184.131.180 BR icmp-echo|icmp-timestamps
107 186.194.224.82 BR icmp-echo|icmp-timestamps
108 186.225.194.29 BR icmp-echo|icmp-timestamps
109 189.42.239.34 BR icmp-echo|icmp-timestamps
110 200.99.138.94 BR icmp-echo|icmp-timestamps
111 200.99.138.103 BR icmp-echo|icmp-timestamps
112 200.167.191.114 BR icmp-echo|icmp-timestamps
113 200.169.8.1 BR icmp-echo|icmp-timestamps
114 141.0.102.202 NO icmp-echo|icmp-timestamps
115 62.97.223.62 NO icmp-echo|icmp-timestamps
116 217.18.206.12 NO icmp-echo|icmp-timestamps
117 217.18.206.22 NO icmp-echo|icmp-timestamps
118 195.204.130.85 NO icmp-echo|icmp-timestamps
119 81.175.38.1 NO icmp-echo|icmp-timestamps
120 46.182.19.48 DE icmp-echo|icmp-timestamps
121 82.96.64.2 DE icmp-echo|icmp-timestamps
122 94.247.43.254 DE icmp-echo|icmp-timestamps
123 195.10.195.195 DE icmp-echo|icmp-timestamps
124 194.172.160.4 DE icmp-echo|icmp-timestamps
125 185.233.106.232 DE icmp-echo|icmp-timestamps
126 185.233.107.4 DE icmp-echo|icmp-timestamps
127 141.1.1.1 GB icmp-echo|icmp-timestamps
128 141.1.27.249 GB icmp-echo|icmp-timestamps
129 176.103.130.130 CY icmp-echo|icmp-timestamps
130 176.103.130.131 CY icmp-echo|icmp-timestamps
131 80.243.64.67 RU icmp-echo|icmp-timestamps
132 195.218.133.88 RU icmp-echo|icmp-timestamps
133 83.69.120.1 RU icmp-echo|icmp-timestamps
134 46.16.229.223 RU icmp-echo|icmp-timestamps
135 193.183.98.66 IT icmp-echo|icmp-timestamps
136 212.19.96.2 IT icmp-echo|icmp-timestamps
137 217.12.181.97 IT icmp-echo|icmp-timestamps
138 62.149.128.4 IT icmp-echo|icmp-timestamps
139 62.149.132.2 IT icmp-echo|icmp-timestamps
140 85.47.198.82 IT icmp-echo|icmp-timestamps
141 202.136.162.11 SG icmp-echo|icmp-timestamps
142 51.79.144.245 SG icmp-echo|icmp-timestamps
143 61.46.244.211 JP icmp-echo|icmp-timestamps
144 1.33.184.173 JP icmp-echo|icmp-timestamps
145 1.33.184.116 JP icmp-echo|icmp-timestamps
146 1.33.184.174 JP icmp-echo|icmp-timestamps
147 1.33.184.108 JP icmp-echo|icmp-timestamps
148 1.33.184.220 JP icmp-echo|icmp-timestamps
149 1.33.184.230 JP icmp-echo|icmp-timestamps
150 1.33.184.158 JP icmp-echo|icmp-timestamps
151 62.212.113.125 FR icmp-echo|icmp-timestamps
152 185.30.211.154 FR icmp-echo|icmp-timestamps
153 87.240.90.128 FR icmp-echo|icmp-timestamps
154 45.77.61.165 FR icmp-echo|icmp-timestamps
155 178.62.57.141 GB icmp-echo|icmp-timestamps
156 194.145.240.6 GB icmp-echo|icmp-timestamps
157 195.27.1.1 GB icmp-echo|icmp-timestamps
158 81.17.66.13 GB icmp-echo|icmp-timestamps
159 87.117.196.200 GB icmp-echo|icmp-timestamps
160 213.123.251.87 GB icmp-echo|icmp-timestamps
161 81.149.21.254 GB icmp-echo|icmp-timestamps
162 196.21.247.1 ZA icmp-echo|icmp-timestamps
163 89.140.69.49 ES icmp-echo|icmp-timestamps
164 62.97.123.107 ES icmp-echo|icmp-timestamps
165 194.177.210.210 GR icmp-echo|icmp-timestamps
166 168.126.63.1 KR icmp-echo|icmp-timestamps
167 121.254.134.99 KR icmp-echo|icmp-timestamps
168 203.239.131.1 KR icmp-echo|icmp-timestamps
169 202.57.32.49 PH icmp-echo|icmp-timestamps
170 81.17.31.34 CH icmp-echo|icmp-timestamps
171 79.141.82.250 CH icmp-echo|icmp-timestamps
172 80.254.77.39 CH icmp-echo|icmp-timestamps
173 85.93.212.2 LU icmp-echo|icmp-timestamps
174 196.223.158.58 SD icmp-echo|icmp-timestamps
175 89.150.195.195 HK icmp-echo|icmp-timestamps
176 130.244.126.100 SE icmp-echo|icmp-timestamps
177 87.96.144.179 SE icmp-echo|icmp-timestamps
178 146.247.80.144 SE icmp-echo|icmp-timestamps
179 46.246.29.69 SE icmp-echo|icmp-timestamps
180 203.210.237.165 VN icmp-echo|icmp-timestamps
181 113.190.253.229 VN icmp-echo|icmp-timestamps
182 119.29.29.29 CN icmp-echo|icmp-timestamps
183 209.193.4.7 US icmp-echo|icmp-timestamps
184 209.112.160.2 US icmp-echo|icmp-timestamps
185 216.68.4.10 US icmp-echo|icmp-timestamps
186 216.68.5.10 US icmp-echo|icmp-timestamps
187 1.250.0.14 KR icmp-echo|icmp-timestamps
188 1.252.0.14 KR icmp-echo|icmp-timestamps
189 1.252.0.12 KR icmp-echo|icmp-timestamps
190 8.238.120.14 US icmp-echo|icmp-timestamps
191 8.238.120.12 US icmp-echo|icmp-timestamps
192 78.143.192.10 GB icmp-echo|icmp-timestamps
193 201.220.136.186 HN icmp-echo|icmp-timestamps
194 190.124.166.8 HN icmp-echo|icmp-timestamps
195 190.64.140.243 UY icmp-echo|icmp-timestamps
196 200.40.230.36 UY icmp-echo|icmp-timestamps
197 134.249.156.228 UA icmp-echo|icmp-timestamps
198 195.60.70.5 UA icmp-echo|icmp-timestamps
199 62.140.239.1 RU icmp-echo|icmp-timestamps
200 94.158.153.236 UA icmp-echo|icmp-timestamps
201 109.254.13.190 UA icmp-echo|icmp-timestamps
202 176.67.0.60 UA icmp-echo|icmp-timestamps
203 193.34.173.206 UA icmp-echo|icmp-timestamps
204 94.153.241.134 UA icmp-echo|icmp-timestamps
205 216.104.206.136 UG icmp-echo|icmp-timestamps
206 154.72.212.38 UG icmp-echo|icmp-timestamps
207 217.174.226.78 TM icmp-echo|icmp-timestamps
208 46.45.187.181 TR icmp-echo|icmp-timestamps
209 46.45.187.163 TR icmp-echo|icmp-timestamps
210 46.45.187.184 TR icmp-echo|icmp-timestamps
211 46.45.187.186 TR icmp-echo|icmp-timestamps
212 46.20.159.27 TR icmp-echo|icmp-timestamps
213 93.113.60.84 TR icmp-echo|icmp-timestamps
214 93.113.60.82 TR icmp-echo|icmp-timestamps
215 196.3.132.153 TT icmp-echo|icmp-timestamps
216 196.3.132.154 TT icmp-echo|icmp-timestamps
217 80.248.72.1 TG icmp-echo|icmp-timestamps
218 117.121.215.102 TH icmp-echo|icmp-timestamps
219 1.20.151.94 TH icmp-echo|icmp-timestamps
220 117.121.215.101 TH icmp-echo|icmp-timestamps
221 122.154.230.146 TH icmp-echo|icmp-timestamps
222 101.101.101.101 TW icmp-echo|icmp-timestamps
223 101.102.103.104 TW icmp-echo|icmp-timestamps
224 113.196.55.130 TW icmp-echo|icmp-timestamps
225 210.242.68.59 TW icmp-echo|icmp-timestamps
226 92.60.50.40 SK icmp-echo|icmp-timestamps
227 217.73.17.110 SK icmp-echo|icmp-timestamps
228 84.245.69.193 SK icmp-echo|icmp-timestamps
229 178.254.129.65 RS icmp-echo|icmp-timestamps
230 93.86.63.54 RS icmp-echo|icmp-timestamps
231 109.92.22.97 RS icmp-echo|icmp-timestamps
232 197.159.166.81 ST icmp-echo|icmp-timestamps
233 208.84.200.28 VC icmp-echo|icmp-timestamps
234 208.94.176.225 LC icmp-echo|icmp-timestamps
235 199.116.57.181 PR icmp-echo|icmp-timestamps
236 74.120.24.129 PR icmp-echo|icmp-timestamps
237 74.120.24.81 PR icmp-echo|icmp-timestamps
238 74.120.24.97 PR icmp-echo|icmp-timestamps
239 78.130.38.222 PT icmp-echo|icmp-timestamps
240 195.22.25.130 PT icmp-echo|icmp-timestamps
241 213.241.88.98 PL icmp-echo|icmp-timestamps
242 87.204.28.12 PL icmp-echo|icmp-timestamps
243 212.160.85.232 PL icmp-echo|icmp-timestamps
244 45.173.200.10 PE icmp-echo|icmp-timestamps
245 200.10.231.110 PY icmp-echo|icmp-timestamps
246 201.222.48.206 PY icmp-echo|icmp-timestamps
247 201.222.50.30 PY icmp-echo|icmp-timestamps
248 201.222.55.246 PY icmp-echo|icmp-timestamps
249 103.209.52.250 PK icmp-echo|icmp-timestamps
250 119.63.132.26 PK icmp-echo|icmp-timestamps
251 83.143.8.249 NG icmp-echo|icmp-timestamps
252 165.98.138.194 NI icmp-echo|icmp-timestamps
253 203.81.75.37 MM icmp-echo|icmp-timestamps
254 65.18.114.254 MM icmp-echo|icmp-timestamps
255 65.18.117.174 MM icmp-echo|icmp-timestamps
256 69.160.0.86 MM icmp-echo|icmp-timestamps
257 103.154.241.252 MM icmp-echo|icmp-timestamps
258 66.181.167.232 MN icmp-echo|icmp-timestamps
259 202.21.99.42 MN icmp-echo|icmp-timestamps
260 202.131.241.74 MN icmp-echo|icmp-timestamps
261 124.158.93.2 SG icmp-echo|icmp-timestamps
262 77.89.232.86 MD icmp-echo|icmp-timestamps
263 200.56.224.11 MX icmp-echo|icmp-timestamps
264 201.144.135.211 MX icmp-echo|icmp-timestamps
265 201.134.115.36 MX icmp-echo|icmp-timestamps
266 1.32.58.251 MY icmp-echo|icmp-timestamps
267 202.86.149.20 MO icmp-echo|icmp-timestamps
268 202.175.45.2 MO icmp-echo|icmp-timestamps
269 202.86.191.50 MO icmp-echo|icmp-timestamps
270 80.250.60.241 LV icmp-echo|icmp-timestamps
271 101.78.12.50 LA icmp-echo|icmp-timestamps
272 168.187.161.250 KW icmp-echo|icmp-timestamps
273 62.12.114.25 KE icmp-echo|icmp-timestamps
274 103.134.220.158 ID icmp-echo|icmp-timestamps
275 121.100.28.163 ID icmp-echo|icmp-timestamps
276 103.28.114.57 ID icmp-echo|icmp-timestamps
277 202.51.111.149 ID icmp-echo|icmp-timestamps
278 103.23.20.23 ID icmp-echo|icmp-timestamps
279 103.98.176.24 ID icmp-echo|icmp-timestamps
280 103.98.176.143 ID icmp-echo|icmp-timestamps
281 119.235.48.3 IN icmp-echo|icmp-timestamps
282 103.194.120.33 IN icmp-echo|icmp-timestamps
283 103.194.120.3 IN icmp-echo|icmp-timestamps
284 103.194.120.129 IN icmp-echo|icmp-timestamps
285 223.31.121.171 IN icmp-echo|icmp-timestamps
286 169.38.73.5 IN icmp-echo|icmp-timestamps
287 202.86.251.201 IN icmp-echo|icmp-timestamps
288 202.164.44.246 IN icmp-echo|icmp-timestamps
289 87.229.99.1 HU icmp-echo|icmp-timestamps
290 91.137.135.75 HU icmp-echo|icmp-timestamps
291 85.90.160.69 HU icmp-echo|icmp-timestamps
292 195.228.219.226 HU icmp-echo|icmp-timestamps
293 103.6.168.114 SG icmp-echo|icmp-timestamps
294 45.71.185.100 EC icmp-echo|icmp-timestamps
295 186.149.243.52 DO icmp-echo|icmp-timestamps
296 193.17.47.1 CZ icmp-echo|icmp-timestamps
297 195.113.144.194 CZ icmp-echo|icmp-timestamps
298 213.211.50.1 CZ icmp-echo|icmp-timestamps
299 213.211.50.2 CZ icmp-echo|icmp-timestamps
300 185.43.135.1 CZ icmp-echo|icmp-timestamps
301 90.183.151.107 CZ icmp-echo|icmp-timestamps
302 27.50.20.21 ID icmp-echo|icmp-timestamps
303 116.193.190.164 ID icmp-echo|icmp-timestamps
304 103.56.148.179 ID icmp-echo|icmp-timestamps
305 8.215.26.147 ID icmp-echo|icmp-timestamps
306 global.irtt.lochnair.net SI irtt 2112
307 uk-cov1.irtt.lochnair.net GB irtt 2112
308 uk-cov2.irtt.lochnair.net GB irtt 2112
309 de-fra.irtt.lochnair.net DE irtt 2112
310 nl-ams1.irtt.lochnair.net NL irtt 2112
311 netperf-eu.bufferbloat.net SE irtt 2112
+14 -1
View File
@@ -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"
}
] ]
} }
+451 -103
View File
@@ -1,136 +1,484 @@
use crate::pinger::PingReply; // SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
use crate::util::MutexExt; // SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
use crate::Config; // SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
use log::info; // SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
//
// SPDX-License-Identifier: MPL-2.0
use crate::SHUTDOWN;
use crate::measurement::model::{MeasurementEvent, MeasurementObservation, MeasurementStream};
use crate::metrics::{Metric, MetricsSender};
use crate::settings::Settings;
use flume::{Receiver, Sender};
use log::{debug, info};
use std::collections::HashMap; use std::collections::HashMap;
use std::net::IpAddr; 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_receiver: Receiver<PingReply>, pub stats_rx: Receiver<MeasurementEvent>,
pub control_tx: Sender<ControlSnapshot>,
pub selection_tx: Option<Sender<ControlSnapshot>>,
pub baseline_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 {
let time_data = self.stats_receiver.recv()?; if SHUTDOWN.load(Ordering::Relaxed) {
info!("Baseliner shutting down");
let mut owd_baseline_map = self.owd_baseline.lock_anyhow()?; return Ok(());
let mut owd_recent_map = self.owd_recent.lock_anyhow()?;
let owd_baseline_new = ReflectorStats {
down_ewma: time_data.down_time,
up_ewma: time_data.up_time,
last_receive_time_s: time_data.last_receive_time_s,
};
let owd_recent_new = ReflectorStats {
down_ewma: time_data.down_time,
up_ewma: time_data.up_time,
last_receive_time_s: time_data.last_receive_time_s,
};
let owd_baseline = owd_baseline_map
.entry(time_data.reflector)
.or_insert(owd_baseline_new);
let owd_recent = owd_recent_map
.entry(time_data.reflector)
.or_insert(owd_recent_new);
if time_data
.last_receive_time_s
.duration_since(owd_baseline.last_receive_time_s)
.as_secs_f64()
> 30.0
|| time_data
.last_receive_time_s
.duration_since(owd_recent.last_receive_time_s)
.as_secs_f64()
> 30.0
{
owd_baseline.down_ewma = time_data.down_time;
owd_baseline.up_ewma = time_data.up_time;
owd_baseline.last_receive_time_s = time_data.last_receive_time_s;
owd_recent.down_ewma = time_data.down_time;
owd_recent.up_ewma = time_data.up_time;
owd_recent.last_receive_time_s = time_data.last_receive_time_s;
} }
owd_baseline.last_receive_time_s = time_data.last_receive_time_s; let event = self.stats_rx.recv()?;
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 let reply = match event {
if time_data.up_time > owd_baseline.up_ewma + 5000.0 MeasurementEvent::Observation(reply) => reply,
|| time_data.down_time > owd_baseline.down_ewma + 5000.0 MeasurementEvent::Loss(_) => continue,
{ };
// 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; let reflector = reply.stream.peer;
owd_recent.last_receive_time_s = self.start_time; let (state, anomaly) = process_reply(
&mut reflectors,
&reply,
self.start_time,
slow_factor,
fast_factor,
);
if anomaly {
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
); );
// If reselection is disabled this would trigger an error self.event_metrics.send(Metric::Event {
// so just ignore the result name: "reselection",
let _ = self.reselect_trigger.send(true); reason: "anomaly",
} else { reflector: Some(reflector),
owd_baseline.down_ewma = owd_baseline.down_ewma * slow_factor tags: &[],
+ (1.0 - slow_factor) * time_data.down_time; });
owd_baseline.up_ewma = let _ = self.reselect_trigger.try_send(true);
owd_baseline.up_ewma * slow_factor + (1.0 - slow_factor) * time_data.up_time;
owd_recent.down_ewma =
owd_recent.down_ewma * fast_factor + (1.0 - fast_factor) * time_data.down_time;
owd_recent.up_ewma =
owd_recent.up_ewma * fast_factor + (1.0 - fast_factor) * time_data.up_time;
if owd_baseline.down_ewma > owd_recent.down_ewma {
owd_baseline.down_ewma = owd_recent.down_ewma;
}
if owd_baseline.up_ewma > owd_recent.up_ewma {
owd_baseline.up_ewma = owd_recent.up_ewma;
}
} }
info!( self.ping_metrics.send(Metric::Ping {
reflector,
measurement_type: reply.stream.source,
rtt: reply.rtt_ms(),
up_time: reply
.one_way
.map(|o| o.uplink.as_nanos() as f64 / 1000000.0)
.unwrap_or(reply.rtt_ms() / 2.0),
down_time: reply
.one_way
.map(|o| o.downlink.as_nanos() as f64 / 1000000.0)
.unwrap_or(reply.rtt_ms() / 2.0),
});
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,
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);
}
}
-290
View File
@@ -1,290 +0,0 @@
use anyhow::Result;
use log::Level;
#[cfg(feature = "uci")]
use log::warn;
#[cfg(feature = "uci")]
use rust_uci::Uci;
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 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, 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,
// 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,
}
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),
)?,
// 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),
)?,
};
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_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)
}
}
+154
View File
@@ -0,0 +1,154 @@
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, schedule, 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
View File
@@ -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()))
} }
+517 -215
View File
@@ -1,240 +1,542 @@
// 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 netlink; mod measurement;
mod pinger; mod metrics;
mod pinger_icmp; mod platform;
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 ::log::{debug, info}; use crate::controller::Controller;
use std::collections::HashMap; use crate::measurement::icmp::IcmpEngine;
use std::net::IpAddr; #[cfg(feature = "irtt")]
use std::str::FromStr; use crate::measurement::irtt::IrttEngine;
use std::sync::mpsc::channel; use crate::measurement::model::{MeasurementSource, MeasurementStream};
use std::sync::{Arc, Mutex, RwLock}; use crate::metrics::{Metric, Metrics, MetricsSender};
use std::thread::sleep; use crate::platform::{TrafficControlBackend, traffic_control_backend, warn_platform_limitations};
use std::time::Duration; use crate::ratecontroller::Ratecontroller;
use std::time::Instant;
use std::{process, thread};
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; 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::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, RwLock};
use std::thread;
use std::thread::{JoinHandle, sleep};
use std::time::{Duration, Instant};
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);
extern "C" fn signal_handler(_: libc::c_int) {
SHUTDOWN.store(true, Ordering::Relaxed);
}
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))
}
let config = Config::new()?; fn initialize_shaper_with_settle_time<T: TrafficControlBackend>(
log::init(config.log_level)?; settings: &Settings,
let mut reflectors = config.load_reflectors()?; traffic_control: &mut T,
let start_t = Instant::now(); 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)?;
// The identifier field in ICMP is only 2 bytes
// so take the last 2 bytes of the PID as the ID
let id = (process::id() & 0xFFFF) as u16;
// Create data structures shared by different threads
let owd_baseline = Arc::new(Mutex::new(HashMap::<IpAddr, ReflectorStats>::new()));
let owd_recent = Arc::new(Mutex::new(HashMap::<IpAddr, ReflectorStats>::new()));
let reflector_peers_lock = Arc::new(RwLock::new(Vec::<IpAddr>::new()));
let mut reflector_pool = Vec::<IpAddr>::new();
let reflector_pool_size = reflectors.len();
let default_reflectors = [
IpAddr::from_str("9.9.9.9")?,
IpAddr::from_str("8.238.120.14")?,
IpAddr::from_str("74.82.42.42")?,
IpAddr::from_str("194.242.2.2")?,
IpAddr::from_str("208.67.222.222")?,
IpAddr::from_str("94.140.14.14")?,
];
match reflector_pool_size > config.num_reflectors as usize {
true => {
let mut peers = reflector_peers_lock.write().unwrap();
peers.extend_from_slice(&default_reflectors);
reflector_pool.append(reflectors.as_mut());
}
false => {
let mut peers = reflector_peers_lock.write().unwrap();
peers.extend_from_slice(&default_reflectors);
}
}
let (baseliner_stats_sender, baseliner_stats_receiver) = channel();
let (reselect_sender, reselect_receiver) = channel();
let (mut pinger_receiver, mut pinger_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 {
config: config.clone(),
owd_baseline: owd_baseline.clone(),
owd_recent: owd_recent.clone(),
reselect_trigger: reselect_sender.clone(),
start_time: start_t,
stats_receiver: baseliner_stats_receiver,
};
let down_qdisc = Netlink::qdisc_from_ifname(config.download_interface.as_str())?;
let up_qdisc = Netlink::qdisc_from_ifname(config.upload_interface.as_str())?;
/* Set initial TC values to minimum
* so there should be no initial bufferbloat to
* fool the baseliner
*/
info!( info!(
"Setting shaper rates to minimum (D/L): {} / {}", "Requesting minimum shaper rates (D/U): {} / {}",
config.download_min_kbits, config.upload_min_kbits settings.network.download_min_kbits(),
settings.network.upload_min_kbits(),
); );
Netlink::set_qdisc_rate(down_qdisc, config.download_min_kbits as u64)?;
Netlink::set_qdisc_rate(up_qdisc, config.upload_min_kbits as u64)?;
// Sleep for a few seconds to give the shaper a chance traffic_control.set_rate(
// to control the queue if load is heavy &down,
let settle_sleep_time = Duration::new(2, 0); 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!( info!(
"Sleeping for {} to give the shaper a chance to get in control if there's bloat", "Sleeping for {} seconds to give the shaper a chance to control existing bloat",
settle_sleep_time.as_secs_f64() settle_time.as_secs_f64(),
);
sleep(settle_sleep_time);
let (error_tx, error_rx) = channel::<anyhow::Error>();
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) = pinger_receiver.listen(
id,
config.measurement_type,
reflector_peers_lock_clone,
baseliner_stats_sender,
) {
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) = pinger_sender.send(
id,
config.measurement_type,
reflector_peers_lock_clone,
config.tick_interval,
) {
let _ = err_tx.send(e);
}
},
)?;
if reflector_pool_size > config.num_reflectors as usize {
let reflector_selector = ReflectorSelector {
config: config.clone(),
owd_recent: owd_recent.clone(),
reflector_peers_lock: reflector_peers_lock.clone(),
reflector_pool,
trigger_channel: reselect_receiver,
};
let err_tx = error_tx.clone();
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
sleep(Duration::new(10, 0));
let dl_direction = if config.download_interface.starts_with("ifb")
|| config.download_interface.starts_with("veth")
{
StatsDirection::TX
} else {
StatsDirection::RX
};
let ul_direction = if config.upload_interface.starts_with("ifb")
|| config.upload_interface.starts_with("veth")
{
StatsDirection::RX
} else {
StatsDirection::TX
};
let mut ratecontroller = Ratecontroller::new(
config.clone(),
owd_baseline,
owd_recent,
reflector_peers_lock,
reselect_sender,
dl_direction,
ul_direction,
)?;
debug!(
"Download direction: {}:{:?}",
config.download_interface, dl_direction
); );
debug!( sleep(settle_time);
"Upload direction: {}:{:?}",
config.upload_interface, ul_direction
);
let err_tx = error_tx.clone(); Ok((down, up))
thread::Builder::new() }
.name("ratecontroller".to_string())
.spawn(move || {
if let Err(e) = ratecontroller.run() {
let _ = err_tx.send(e);
}
})?;
// Drop original sender so recv() unblocks if all threads exit cleanly fn install_signal_handlers() {
drop(error_tx); unsafe {
libc::signal(
// Wait for first error libc::SIGINT,
match error_rx.recv() { signal_handler as *const () as libc::sighandler_t,
Ok(e) => Err(anyhow::anyhow!("thread exited with error: {e}")), );
Err(_) => Ok(()), // all senders dropped = all threads exited without error 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}");
}
if let Err(error) = traffic_control.set_rate(
up,
settings.network.upload_base_kbits as u64,
settings.advanced_settings.dry_run,
) {
warn!("Failed to restore upload shaper rate: {error}");
}
}
fn 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 ***");
}
let start_time = Instant::now();
let ReflectorSetup {
peers: reflector_peers,
active_streams,
endpoint_streams,
pool: reflector_pool,
reflectors,
reselection_enabled,
} = setup_reflectors(settings)?;
let (control_snapshot_tx, control_snapshot_rx) = flume::unbounded();
let (error_tx, error_rx) = flume::unbounded::<anyhow::Error>();
let (icmp_binding_tx, icmp_binding_rx) = flume::unbounded();
#[cfg(feature = "irtt")]
let (irtt_binding_tx, irtt_binding_rx) = flume::unbounded();
let (measurement_tx, measurement_rx) = flume::unbounded();
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)
};
let source = match settings.advanced_settings.measurement_type {
ReflectorProtocol::IcmpEcho => MeasurementSource::IcmpEcho,
ReflectorProtocol::IcmpTimestamps => MeasurementSource::IcmpTimestamp,
ReflectorProtocol::Irtt => MeasurementSource::Irtt,
};
let controller = Controller::new(
reflector_rx,
icmp_binding_tx,
#[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(
start_time,
Duration::from_secs_f64(settings.advanced_settings.tick_interval),
&settings.network,
measurement_tx,
irtt_binding_rx,
)?
};
spawn_task(&rt, &error_tx, icmp_engine.run());
#[cfg(feature = "irtt")]
spawn_task(&rt, &error_tx, irtt_engine.run(irtt_task));
let dropped = Arc::new(AtomicU32::new(0));
let (metrics_tx, metrics_thread_handle) = if settings.observability.enabled {
let (tx, rx) = flume::bounded(1000);
let metrics = Metrics {
settings: settings.clone(),
metrics_rx: rx,
metrics_dropped: Arc::clone(&dropped),
};
let handle = spawn_worker("metrics", &error_tx, move || metrics.run())?;
(Some(tx), Some(handle))
} else {
(None, None)
};
let make_sender = |enabled: bool| -> MetricsSender {
metrics_tx
.as_ref()
.filter(|_| enabled)
.map(|tx| MetricsSender::new(tx.clone(), Arc::clone(&dropped)))
.unwrap_or_else(MetricsSender::disabled)
};
let ping_metrics = make_sender(settings.observability.export_ping_metrics);
let baseline_metrics = make_sender(settings.observability.export_baseline_metrics);
let event_metrics = make_sender(settings.observability.export_events);
let rate_metrics = make_sender(settings.observability.export_rate_metrics);
event_metrics.send(Metric::Event {
name: "starting",
reason: "",
reflector: None,
tags: if settings.advanced_settings.dry_run {
&[("dry_run", "true")]
} else {
&[]
},
});
let baseliner = Baseliner {
settings: settings.clone(),
reselect_trigger: reselect_tx.clone(),
start_time,
stats_rx: measurement_rx,
control_tx: control_snapshot_tx,
selection_tx: selection_snapshot_tx,
baseline_metrics,
event_metrics: event_metrics.clone(),
ping_metrics,
};
let mut main_traffic_control = traffic_control_backend();
let (down_shaper, up_shaper) = initialize_shaper(settings, &mut main_traffic_control)?;
spawn_worker("baseliner", &error_tx, move || baseliner.run())?;
spawn_task(
&rt,
&error_tx,
controller.run(reflector_peers.read_anyhow()?.clone()),
);
let main_event_metrics = event_metrics.clone();
if reselection_enabled {
let reflector_selector = ReflectorSelector {
settings: settings.clone(),
snapshot_rx: selection_snapshot_rx
.expect("reselection snapshot receiver must exist when reselection is enabled"),
reflector_peers_lock: Arc::clone(&reflector_peers),
reflector_pool,
endpoint_streams,
trigger_channel: reselect_rx,
metrics: event_metrics,
reflector_tx,
};
spawn_worker("reselection", &error_tx, move || reflector_selector.run())?;
}
// Give the baseliner time to collect initial samples before adjusting rates.
sleep(Duration::from_secs(10));
let mut ratecontroller = Ratecontroller::new(
settings.clone(),
control_snapshot_rx,
active_streams,
reselect_tx,
rate_metrics,
)?;
spawn_worker("ratecontroller", &error_tx, move || ratecontroller.run())?;
// Drop the original sender so the channel disconnects if all workers exit cleanly.
drop(error_tx);
let result = wait_for_exit(&error_rx);
// Make the error path request shutdown too, rather than leaving the other workers running.
SHUTDOWN.store(true, Ordering::Relaxed);
let stopping_reason = if result.is_err() { "error" } else { "signal" };
main_event_metrics.send(Metric::Event {
name: "stopping",
reason: stopping_reason,
reflector: None,
tags: &[],
});
// Drop all MetricsSender instances and the raw tx so the metrics channel
// disconnects once all worker threads also drop their copies.
drop(main_event_metrics);
drop(metrics_tx);
if let Some(handle) = metrics_thread_handle {
let _ = handle.join();
}
restore_shaper(
settings,
&mut main_traffic_control,
&down_shaper,
&up_shaper,
);
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)
}
+505
View File
@@ -0,0 +1,505 @@
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,
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,
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
}
}
+414
View File
@@ -0,0 +1,414 @@
use std::{collections::HashMap, net::SocketAddr, time::Instant};
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, 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, ProbeSchedule, 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,
/// Retained for future hybrid-mode scheduling. IRTT currently uses the
/// global interval configured on its managed client.
#[allow(dead_code)]
schedule: ProbeSchedule,
port: u16,
hmac: Option<String>,
}
impl IrttBinding {
pub fn new(
stream: MeasurementStream,
schedule: ProbeSchedule,
port: u16,
hmac: Option<String>,
) -> Self {
Self {
stream,
schedule,
port,
hmac,
}
}
}
pub struct IrttEngine {
client: ManagedClientHandle,
bindings: HashMap<MeasurementStreamId, IrttBinding>,
origin: Instant,
binding_rx: Receiver<Vec<IrttBinding>>,
observation_tx: Sender<MeasurementEvent>,
}
impl IrttEngine {
pub fn new(
origin: Instant,
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(),
origin,
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 {
target.auth = TargetAuth::Override(irtt_client::Authentication::Hmac(
irtt_client::HmacKey::new(key.as_bytes().to_vec().into_boxed_slice()),
));
}
target
})
.collect();
self.client.update_targets(targets)?.await?;
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()?;
loop {
tokio::select! {
bindings = self.binding_rx.recv_async() => {
let bindings = bindings
.map_err(|_| IrttError::BindingChannelClosed)?;
self.set_bindings(bindings).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,
..
} => {
let Some(stream) = stream else {
continue;
};
let observation = MeasurementObservation {
stream,
started_at: sent_at.mono,
observed_at: received_at.mono,
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,
})
}
+4
View File
@@ -0,0 +1,4 @@
pub(crate) mod icmp;
#[cfg(feature = "irtt")]
pub(crate) mod irtt;
pub(crate) mod model;
+149
View File
@@ -0,0 +1,149 @@
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,
/// 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.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),
}
+333
View File
@@ -0,0 +1,333 @@
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
//
// 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 std::fmt::Write;
use std::net::{IpAddr, TcpStream, UdpSocket};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
const MAX_RECONNECT_BACKOFF: u64 = 60;
enum Transport {
Udp(UdpSocket),
Tcp {
stream: Option<TcpStream>,
host: String,
port: u16,
reconnect_backoff: u64,
},
}
impl Transport {
fn new_udp(host: &str, port: u16) -> anyhow::Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:0")?;
socket.set_write_timeout(Some(Duration::from_millis(100)))?;
socket.connect((host, port))?;
Ok(Transport::Udp(socket))
}
fn new_tcp(host: &str, port: u16) -> Self {
Transport::Tcp {
stream: None,
host: host.to_string(),
port,
reconnect_backoff: 1,
}
}
fn send(&mut self, data: &str) {
match self {
Transport::Udp(socket) => {
if let Err(e) = socket.send(data.as_bytes()) {
warn!("UDP send failed: {}", e);
}
}
Transport::Tcp {
stream,
host,
port,
reconnect_backoff,
} => {
if stream.is_none() {
match TcpStream::connect((host.as_str(), *port)) {
Ok(s) => {
s.set_write_timeout(Some(Duration::from_millis(500))).ok();
info!("Connected to metrics collector at {}:{}", host, port);
*stream = Some(s);
*reconnect_backoff = 1;
}
Err(e) => {
warn!(
"Failed to connect to {}:{} - {}, backoff {}s",
host, port, e, reconnect_backoff
);
*reconnect_backoff =
(*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
return;
}
}
}
let full_data = format!("{}\n", data);
// take the stream out, write, put it back or set None on failure
let write_error = stream
.as_mut()
.and_then(|s| std::io::Write::write_all(s, full_data.as_bytes()).err());
if let Some(e) = write_error {
warn!("TCP send failed: {}", e);
*stream = None;
*reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
}
}
}
}
}
pub enum Metric {
Ping {
reflector: IpAddr,
measurement_type: MeasurementSource,
rtt: f64,
up_time: f64,
down_time: f64,
},
Rate {
dl_rate: f64,
ul_rate: f64,
rx_load: f64,
tx_load: f64,
delta_delay_down: f64,
delta_delay_up: f64,
},
Baseline {
reflector: IpAddr,
baseline_up_ewma: f64,
baseline_down_ewma: f64,
recent_up_ewma: f64,
recent_down_ewma: f64,
},
Event {
name: &'static str,
reason: &'static str,
reflector: Option<IpAddr>,
tags: &'static [(&'static str, &'static str)],
},
Dropped {
count: u32,
},
}
#[derive(Clone)]
pub struct MetricsSender {
tx: Option<Sender<(Metric, u64)>>,
dropped: Arc<AtomicU32>,
}
impl MetricsSender {
pub fn new(tx: Sender<(Metric, u64)>, dropped: Arc<AtomicU32>) -> Self {
Self {
tx: Some(tx),
dropped,
}
}
pub fn disabled() -> Self {
Self {
tx: None,
dropped: Arc::new(AtomicU32::new(0)),
}
}
pub fn send(&self, metric: Metric) {
if let Some(ref tx) = self.tx {
let ts = ClockSample::<Realtime>::now().as_nanos();
if tx.try_send((metric, ts)).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
}
}
pub struct Metrics {
pub settings: Settings,
pub metrics_dropped: Arc<AtomicU32>,
pub metrics_rx: Receiver<(Metric, u64)>,
}
impl Metrics {
pub fn run(self) -> anyhow::Result<()> {
let host = match &self.settings.observability.host {
Some(h) => h.clone(),
None => {
error!("Observability host not configured - metrics exporter disabled");
return Ok(());
}
};
let port = self.settings.observability.port;
let batch_size = self.settings.observability.batch_size;
let timeout = Duration::from_millis(self.settings.observability.batch_timeout_ms);
let host_tag = &self.settings.observability.host_tag.clone();
let mut transport = match self.settings.observability.protocol {
ObservabilityProtocol::Udp => {
info!("Metrics exporter configured for UDP to {}:{}", host, port);
Transport::new_udp(&host, port)?
}
ObservabilityProtocol::Tcp => {
info!("Metrics exporter configured for TCP to {}:{}", host, port);
Transport::new_tcp(&host, port)
}
};
let mut batch = Vec::with_capacity(batch_size);
loop {
// block until first metric or timeout
match self.metrics_rx.recv_timeout(timeout) {
Ok(metric) => batch.push(metric),
Err(RecvTimeoutError::Timeout) => {
let dropped = self.metrics_dropped.swap(0, Ordering::Relaxed);
if dropped > 0 {
let ts = ClockSample::<Realtime>::now().as_nanos();
batch.push((Metric::Dropped { count: dropped }, ts));
}
if !batch.is_empty() {
self.flush(&mut transport, &batch, host_tag);
batch.clear();
}
continue;
}
Err(RecvTimeoutError::Disconnected) => break,
}
// greedily drain without blocking
while let Ok(metric) = self.metrics_rx.try_recv() {
batch.push(metric);
if batch.len() >= batch_size {
break;
}
}
if batch.len() >= batch_size {
self.flush(&mut transport, &batch, host_tag);
batch.clear();
}
}
Ok(())
}
fn flush(&self, transport: &mut Transport, batch: &[(Metric, u64)], host_tag: &str) {
match transport {
/*
* the UDP receive buffer might be too small on the receiver to accept batched data,
* and it'll get cutoff in the middle of a record. so send metrics directly on UDP
*/
Transport::Udp(_) => {
let mut data = String::with_capacity(300);
for (metric, timestamp_ns) in batch {
data.clear();
self.write_lines(metric, *timestamp_ns, host_tag, &mut data);
transport.send(&data);
}
}
Transport::Tcp { .. } => {
let mut data = String::with_capacity(batch.len() * 300);
for (metric, timestamp_ns) in batch {
self.write_lines(metric, *timestamp_ns, host_tag, &mut data);
}
transport.send(&data);
}
}
}
fn write_lines(&self, metric: &Metric, timestamp_ns: u64, host_tag: &str, out: &mut String) {
match metric {
Metric::Ping {
reflector,
measurement_type,
rtt,
up_time,
down_time,
} => {
writeln!(
out,
"sqm_ping,host={host_tag},reflector={reflector},type={measurement_type} \
rtt={rtt:.3},up_time={up_time:.3},down_time={down_time:.3} {timestamp_ns}i"
)
.unwrap_or(());
}
Metric::Rate {
dl_rate,
ul_rate,
rx_load,
tx_load,
delta_delay_down,
delta_delay_up,
} => {
writeln!(out,
"sqm_rate,host={host_tag},direction=download \
rate_kbps={dl_rate:.0},load={rx_load:.4},delta_delay={delta_delay_down:.3} {timestamp_ns}i"
).unwrap_or(());
writeln!(out,
"sqm_rate,host={host_tag},direction=upload \
rate_kbps={ul_rate:.0},load={tx_load:.4},delta_delay={delta_delay_up:.3} {timestamp_ns}i"
).unwrap_or(());
}
Metric::Baseline {
reflector,
baseline_up_ewma,
baseline_down_ewma,
recent_up_ewma,
recent_down_ewma,
} => {
writeln!(out,
"sqm_baseline,host={host_tag},reflector={reflector},direction=up \
baseline_ewma={baseline_up_ewma:.3},recent_ewma={recent_up_ewma:.3} {timestamp_ns}i"
).unwrap_or(());
writeln!(out,
"sqm_baseline,host={host_tag},reflector={reflector},direction=down \
baseline_ewma={baseline_down_ewma:.3},recent_ewma={recent_down_ewma:.3} {timestamp_ns}i"
).unwrap_or(());
}
Metric::Event {
name,
reason,
reflector,
tags,
} => {
let mut tag_str = format!("host={host_tag},type={name}");
if let Some(reflector) = reflector {
write!(tag_str, ",reflector={reflector}").unwrap_or(());
}
if !reason.is_empty() {
write!(tag_str, ",reason={reason}").unwrap_or(());
}
for (k, v) in *tags {
write!(tag_str, ",{k}={v}").unwrap_or(());
}
writeln!(out, "sqm_event,{tag_str} count=1i {timestamp_ns}").unwrap_or(());
}
Metric::Dropped { count } => {
writeln!(
out,
"sqm_metrics_dropped,host={host_tag} count={count}i {timestamp_ns}"
)
.unwrap_or(());
}
}
}
}
-136
View File
@@ -1,136 +0,0 @@
use std::io;
use std::str::Utf8Error;
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) -> Result<(), NetlinkError> {
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(())
}
}
-139
View File
@@ -1,139 +0,0 @@
use crate::MeasurementType;
use icmp_socket2::socket::IcmpSocket;
use icmp_socket2::{IcmpSocket4, Icmpv4Packet};
use crate::util::RwLockExt;
use log::{debug, warn};
use std::net::{IpAddr, Ipv4Addr};
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 seq: u16,
pub rtt: i64,
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_sender: Sender<PingReply>,
) -> anyhow::Result<()> {
let socket = &mut open_socket(type_)?;
loop {
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, pkt) {
Ok(val) => val,
Err(_) => {
// parse_packet will throw an error if it's an unknown protocol etc.
// so just quietly move on
continue;
}
};
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_sender.send(reply) {
warn!("Stats channel closed, stopping listener: {}", e);
break Ok(());
}
}
}
fn parse_packet(&self, id: u16, reflector: IpAddr, 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 {
// 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;
}
-75
View File
@@ -1,75 +0,0 @@
use std::net::IpAddr;
use std::time::Instant;
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, 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: i64 = time_ms - time_sent;
Ok(PingReply {
reflector,
seq: sequence,
rtt,
current_time: time_ms,
down_time: (rtt / 2) as f64,
up_time: (rtt / 2) 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()
}
}
-70
View File
@@ -1,70 +0,0 @@
use crate::pinger::{PingError, PingListener, PingReply, PingSender};
use crate::time::Time;
use icmp_socket2::packet::WithTimestampRequest;
use icmp_socket2::Icmpv4Message;
use icmp_socket2::Icmpv4Packet;
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, 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: i64 = time_since_midnight - originate as i64;
let dl_time: i64 = time_since_midnight - transmit as i64;
let ul_time: i64 = receive as i64 - originate as i64;
Ok(PingReply {
reflector,
seq: sequence,
rtt,
current_time: time_since_midnight,
down_time: dl_time as f64,
up_time: ul_time as f64,
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()
}
}
+125
View File
@@ -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."
);
}
+18
View File
@@ -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() {}
+172
View File
@@ -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)
}
}
+218
View File
@@ -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."
);
}
+63
View File
@@ -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,
};
+39
View File
@@ -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(())
}
}
+100
View File
@@ -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."
);
}
+406 -210
View File
@@ -1,18 +1,28 @@
use crate::netlink::{Netlink, NetlinkError, Qdisc}; // SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
use crate::time::Time; // SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
use crate::util::{MutexExt, RwLockExt}; // SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
use crate::{Config, ReflectorStats}; // SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
//
// SPDX-License-Identifier: MPL-2.0
use crate::SHUTDOWN;
use crate::baseliner::ControlSnapshot;
use crate::measurement::model::MeasurementStream;
use crate::metrics::{Metric, MetricsSender};
use crate::platform::{
InterfaceStatsProvider, PlatformInterfaceStats, PlatformTrafficControl, TrafficControlBackend,
interface_stats_provider, traffic_control_backend,
};
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::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 {
@@ -20,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,
@@ -79,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,
@@ -89,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,
@@ -98,32 +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
reselect_trigger: Sender<bool>, .as_ref()
state_dl: State, .is_none_or(|current| snapshot.generated_at >= current.generated_at)
state_ul: State, {
up_direction: StatsDirection, *latest = Some(snapshot);
}
}
} }
impl Ratecontroller { 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>,
state_dl: State<T::Handle>,
state_ul: State<T::Handle>,
rng: fastrand::Rng,
stats_provider: S,
traffic_control: T,
metrics: MetricsSender,
}
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,
) )
}; };
@@ -152,7 +252,7 @@ impl Ratecontroller {
state.load = state.utilisation / state.current_rate; state.load = state.utilisation / state.current_rate;
if state.delta_stat < delay_ms if state.delta_stat < delay_ms
&& state.load > self.config.high_load_level && 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
@@ -164,14 +264,16 @@ 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.safe_rates.get(fastrand::usize(..state.safe_rates.len())) { match state
.safe_rates
.get(self.rng.usize(..state.safe_rates.len()))
{
Some(rnd_rate) => { Some(rnd_rate) => {
state.next_rate = state.next_rate = rnd_rate.min(0.9 * state.current_rate * state.load);
rnd_rate.min(0.9 * state.current_rate * state.load);
} }
None => { None => {
state.next_rate = 0.9 * state.current_rate * state.load; state.next_rate = 0.9 * state.current_rate * state.load;
@@ -188,117 +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,
) -> 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,
})
}
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,
)?;
Netlink::set_qdisc_rate(
self.state_ul.qdisc,
self.state_ul.current_rate.round() as u64,
)?;
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()?;
@@ -309,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(),
@@ -320,36 +331,45 @@ impl Ratecontroller {
} }
loop { loop {
if SHUTDOWN.load(Ordering::Relaxed) {
info!("Rate controller shutting down");
return Ok(());
}
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"
);
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() {
warn!("No reflector data available, dropping to minimum rates"); warn!("No reflector data available, dropping to minimum rates");
self.state_dl.next_rate = self.config.download_min_kbits; self.metrics.send(Metric::Event {
self.state_ul.next_rate = self.config.upload_min_kbits; name: "reflector_unavailable",
reason: "",
reflector: None,
tags: &[],
});
self.state_dl.next_rate = self.settings.network.download_min_kbits();
self.state_ul.next_rate = self.settings.network.upload_min_kbits();
Netlink::set_qdisc_rate( self.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.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.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;
@@ -364,23 +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.state_dl.qdisc, self.state_dl.next_rate as u64)?; self.traffic_control.set_rate(
&self.state_dl.shaper,
self.state_dl.next_rate as u64,
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.state_ul.qdisc, self.state_ul.next_rate as u64)?; self.traffic_control.set_rate(
&self.state_ul.shaper,
self.state_ul.next_rate as u64,
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(),
@@ -393,8 +421,17 @@ impl Ratecontroller {
self.state_ul.current_rate as u64 self.state_ul.current_rate as u64
); );
if let Some(ref mut fd) = stats_fd { self.metrics.send(Metric::Rate {
if let Err(e) = fd.write_all( dl_rate: self.state_dl.current_rate,
ul_rate: self.state_ul.current_rate,
rx_load: self.state_dl.load,
tx_load: self.state_ul.load,
delta_delay_down: self.state_dl.delta_stat,
delta_delay_up: self.state_ul.delta_stat,
});
let stats_write_error = stats_fd.as_mut().and_then(|fd| {
fd.write_all(
format!( format!(
"{},{},{},{},{},{},{},{}\n", "{},{},{},{},{},{},{},{}\n",
stats_time.secs(), stats_time.secs(),
@@ -407,35 +444,194 @@ impl Ratecontroller {
self.state_ul.current_rate as u64 self.state_ul.current_rate as u64
) )
.as_bytes(), .as_bytes(),
) { )
warn!("Failed to write statistics: {}", e); .err()
} });
if let Some(e) = stats_write_error {
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 {
if let Err(e) = fd.write_all( let hist_time = ClockSample::<Realtime>::now();
format!( if let Err(e) = fd.write_all(
"{},{},{},{}\n", format!(
hist_time.as_secs_f64(), "{},{},{},{}\n",
i, hist_time.as_secs_f64(),
self.state_ul.safe_rates[i], i,
self.state_dl.safe_rates[i] self.state_ul.safe_rates[i],
) self.state_dl.safe_rates[i]
.as_bytes(), )
) { .as_bytes(),
warn!("Failed to write speed history file: {}", e); ) {
} warn!("Failed to write speed history file: {}", e);
} }
lastdump_t = now_t;
} }
lastdump_t = now_t;
} }
} }
} }
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);
if 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]);
}
} }
+350 -42
View File
@@ -1,70 +1,203 @@
use crate::util::{MutexExt, RwLockExt}; // SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
use crate::{Config, ReflectorStats}; // SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
//
// SPDX-License-Identifier: MPL-2.0
use crate::SHUTDOWN;
use crate::baseliner::ControlSnapshot;
use crate::measurement::model::MeasurementStream;
use crate::metrics::{Metric, MetricsSender};
use crate::settings::Settings;
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::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,
} }
impl ReflectorSelector { 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) {
* Selection is triggered either by some other thread triggering it through the channel, info!("Reflector selector shutting down");
* or it passes the timeout. In any case we don't care about the result of this function, return Ok(());
* so we ignore the result of it. }
*/
let _ = self let triggered = match wait_for_trigger_or_deadline(
.trigger_channel &self.trigger_channel,
.recv_timeout(selector_sleep_time) &self.snapshot_rx,
.unwrap_or(true); &mut latest_snapshot,
Instant::now() + selector_sleep_time,
) {
WaitOutcome::Triggered => true,
WaitOutcome::DeadlineReached => false,
WaitOutcome::ChannelsClosed => {
info!("Reflector selector channels closed, shutting down");
return Ok(());
}
};
reselection_count += 1; reselection_count += 1;
info!("Starting reselection [#{}]", reselection_count); info!("Starting reselection [#{}]", reselection_count);
self.metrics.send(Metric::Event {
name: "reflector_selection",
reason: if triggered { "triggered" } else { "timeout" },
reflector: None,
tags: &[],
});
// 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 = &self.reflector_pool[fastrand::usize(..self.reflector_pool.len())]; let next_candidate =
&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
@@ -72,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
); );
} }
} }
@@ -97,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);
@@ -112,19 +253,186 @@ impl ReflectorSelector {
} }
if (candidates.len() as u8) < num_reflectors { if (candidates.len() as u8) < num_reflectors {
num_reflectors = candidates.len() as u8; info!(
"Only {} usable reflectors, retaining current set of {}",
candidates.len(),
num_reflectors
);
self.reflector_tx.send(current_peers.clone())?;
*reflectors_peers = current_peers;
continue;
} }
let mut new_peers = Vec::new(); let mut new_peers = Vec::new();
for i in 0..num_reflectors { for i in 0..num_reflectors {
new_peers.push(candidates[i as usize].0); let peer = candidates[i as usize].0;
info!( new_peers.push(peer);
"New selected peer: {}", info!("New selected peer: {}", peer);
candidates[i as usize].0.to_string() self.metrics.send(Metric::Event {
); name: "reflector_selected",
reason: "",
reflector: Some(peer.ip()),
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)
);
}
}
+445
View File
@@ -0,0 +1,445 @@
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
//
// SPDX-License-Identifier: MPL-2.0
use anyhow::{Context, bail};
use config::{Config, ConfigError, Environment};
use log::Level;
use serde::{Deserialize, Deserializer, de};
use std::fmt::{self, Display};
use std::net::IpAddr;
use std::str::FromStr;
use uci_derive::{UciConfig, UciSection};
#[cfg(all(feature = "uci", unix))]
use crate::settings::uci::UciSource;
#[cfg(all(feature = "uci", unix))]
pub mod uci;
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 or a UCI-compatible boolean string")
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value
.parse::<FlexiBool>()
.map(|value| value.0)
.map_err(E::custom)
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(&value)
}
}
deserializer.deserialize_any(FlexiBoolVisitor)
}
#[derive(Clone, Copy, Debug, 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("__"));
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 {
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)
}
+375
View File
@@ -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());
}
}
+41
View File
@@ -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];
}
+450
View File
@@ -0,0 +1,450 @@
// 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, 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());
}
}
+58 -13
View File
@@ -1,25 +1,46 @@
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 { pub enum Monotonic {}
let time: Timespec = clock_gettime(id);
impl Clock for Realtime {
const ID: ClockId = ClockId::Realtime;
}
impl Clock for Monotonic {
const ID: ClockId = ClockId::Monotonic;
}
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
} }
@@ -28,6 +49,10 @@ impl Time {
self.time_ns self.time_ns
} }
pub fn as_nanos(&self) -> u64 {
self.time_s * 1_000_000_000 + self.time_ns
}
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
} }
@@ -36,3 +61,23 @@ impl Time {
(self.time_s * 1000) + (self.time_ns / 1000000) (self.time_s * 1000) + (self.time_ns / 1000000)
} }
} }
impl ClockSample<Realtime> {
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,
}
}
}
+7 -1
View File
@@ -1,5 +1,11 @@
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
//
// SPDX-License-Identifier: MPL-2.0
use anyhow::anyhow; use anyhow::anyhow;
use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
pub type ArcRwLock<T> = Arc<RwLock<T>>;
pub trait MutexExt<T> { pub trait MutexExt<T> {
fn lock_anyhow(&self) -> anyhow::Result<MutexGuard<'_, T>>; fn lock_anyhow(&self) -> anyhow::Result<MutexGuard<'_, T>>;