feat: bootstrap awg2 harness rust workspace and docs

This commit is contained in:
2026-05-09 02:17:25 +05:00
commit d937f816bf
47 changed files with 4195 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
+682
View File
@@ -0,0 +1,682 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "awg2-harness"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"rand",
"serde",
"thiserror",
"toml",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "cc"
version = "1.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "js-sys"
version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "awg2-harness"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0"
chrono = { version = "0.4", features = ["clock"] }
clap = { version = "4.5", features = ["derive"] }
rand = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0"
toml = "0.8"
+24
View File
@@ -0,0 +1,24 @@
use std::path::PathBuf;
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(name = "awg2-harness")]
pub struct Cli {
#[arg(long)]
pub config: Option<PathBuf>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Daemon,
ApplyLimits,
ApplyFirewall,
Status,
EnsureSwap,
ValidateConfig,
}
+108
View File
@@ -0,0 +1,108 @@
use std::{fs, net::Ipv4Addr, path::Path};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub amnezia: AmneziaConfig,
pub traffic: TrafficConfig,
pub anti_torrent: AntiTorrentConfig,
pub reboot: RebootConfig,
pub swap: SwapConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AmneziaConfig {
pub container_names: Vec<String>,
pub interface_candidates: Vec<String>,
pub interface_patterns: Vec<String>,
pub peer_ipv4_cidr: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TrafficConfig {
pub enabled: bool,
pub peer_limit: String,
pub root_rate: String,
pub reconcile_interval_secs: u64,
pub container_ready_timeout_secs: u64,
pub class_start_id: u16,
pub ifb_name: String,
pub full_rebuild_on_drift: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AntiTorrentConfig {
pub enabled: bool,
pub max_connections_per_peer: u32,
pub peer_ipv4_cidr: String,
pub apply_in_amnezia_netns: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RebootConfig {
pub enabled: bool,
pub window_start_utc: String,
pub window_end_utc: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SwapConfig {
pub enabled: bool,
pub path: String,
pub size_mb: u64,
pub priority: i32,
pub swappiness: u8,
}
impl Config {
pub fn load(path: &Path) -> anyhow::Result<Self> {
let raw = fs::read_to_string(path)?;
let cfg: Self = toml::from_str(&raw)?;
Ok(cfg)
}
pub fn validate(&self) -> anyhow::Result<()> {
if self.amnezia.container_names.is_empty() {
anyhow::bail!("amnezia.container_names must not be empty");
}
if self.traffic.class_start_id < 10 {
anyhow::bail!("traffic.class_start_id must be >= 10");
}
if self.swap.swappiness > 100 {
anyhow::bail!("swap.swappiness must be <= 100");
}
parse_cidr_v4(&self.amnezia.peer_ipv4_cidr)?;
parse_cidr_v4(&self.anti_torrent.peer_ipv4_cidr)?;
Ok(())
}
}
pub fn parse_cidr_v4(cidr: &str) -> anyhow::Result<(Ipv4Addr, u8)> {
let mut parts = cidr.split('/');
let ip = parts
.next()
.ok_or_else(|| anyhow::anyhow!("invalid cidr: {cidr}"))?
.parse::<Ipv4Addr>()?;
let prefix = parts
.next()
.ok_or_else(|| anyhow::anyhow!("invalid cidr: {cidr}"))?
.parse::<u8>()?;
if prefix > 32 {
anyhow::bail!("invalid prefix in cidr: {cidr}");
}
Ok((ip, prefix))
}
pub fn ip_in_cidr(ip: Ipv4Addr, cidr: &str) -> anyhow::Result<bool> {
let (base, prefix) = parse_cidr_v4(cidr)?;
let mask = if prefix == 0 {
0u32
} else {
u32::MAX << (32 - prefix)
};
let ip_u = u32::from(ip);
let base_u = u32::from(base);
Ok((ip_u & mask) == (base_u & mask))
}
+52
View File
@@ -0,0 +1,52 @@
use std::{thread, time::Duration};
use crate::{config::Config, firewall, reboot, traffic};
pub fn run(cfg: Config) -> anyhow::Result<()> {
println!("starting daemon");
let _ = traffic::reconcile_traffic(&cfg);
let _ = firewall::apply_firewall(&cfg);
let _ = reboot::maybe_schedule_reboot(&cfg);
let events_cfg = cfg.clone();
thread::spawn(move || {
let _ = docker_events_loop(events_cfg);
});
loop {
if let Err(e) = traffic::reconcile_traffic(&cfg) {
eprintln!("traffic reconcile error: {e}");
}
if let Err(e) = firewall::apply_firewall(&cfg) {
eprintln!("firewall reconcile error: {e}");
}
thread::sleep(Duration::from_secs(cfg.traffic.reconcile_interval_secs.max(5)));
}
}
fn docker_events_loop(cfg: Config) -> anyhow::Result<()> {
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
let mut child = Command::new("docker")
.args(["events", "--filter", "event=start", "--format", "{{.Actor.Attributes.name}}"])
.stdout(Stdio::piped())
.spawn()?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("failed to capture docker events stdout"))?;
let reader = BufReader::new(stdout);
for line in reader.lines() {
let name = line?.trim().to_string();
if cfg.amnezia.container_names.iter().any(|n| n == &name) {
let _ = traffic::reconcile_traffic(&cfg);
let _ = firewall::apply_firewall(&cfg);
}
}
Ok(())
}
+65
View File
@@ -0,0 +1,65 @@
use std::process::Command;
use crate::{config::Config, error::AppError};
#[derive(Debug, Clone)]
pub struct RuntimeTarget {
pub container_name: String,
pub container_id: String,
pub container_pid: u32,
pub netns_path: String,
pub interface_name: String,
}
pub fn discover_target(cfg: &Config) -> anyhow::Result<RuntimeTarget> {
let container_name = find_running_container(&cfg.amnezia.container_names)?;
let container_id = run_capture(&["docker", "inspect", "--format", "{{.Id}}", &container_name])?
.trim()
.to_string();
let pid_str = run_capture(&[
"docker",
"inspect",
"--format",
"{{.State.Pid}}",
&container_name,
])?;
let container_pid = pid_str.trim().parse::<u32>()?;
let netns_path = format!("/proc/{container_pid}/ns/net");
let interface_name = crate::iface::detect_interface(cfg, &container_name)?;
Ok(RuntimeTarget {
container_name,
container_id,
container_pid,
netns_path,
interface_name,
})
}
fn find_running_container(names: &[String]) -> anyhow::Result<String> {
let ps = run_capture(&["docker", "ps", "--format", "{{.Names}}"])?;
for name in names {
if ps.lines().any(|x| x.trim() == name) {
return Ok(name.clone());
}
}
Err(AppError::NotFound("amnezia container not running".to_string()).into())
}
pub fn run_capture(args: &[&str]) -> anyhow::Result<String> {
let (program, rest) = args
.split_first()
.ok_or_else(|| anyhow::anyhow!("empty command"))?;
let out = Command::new(program).args(rest).output()?;
if !out.status.success() {
return Err(AppError::Command(format!(
"{} {:?}: {}",
program,
rest,
String::from_utf8_lossy(&out.stderr)
))
.into());
}
Ok(String::from_utf8_lossy(&out.stdout).to_string())
}
+10
View File
@@ -0,0 +1,10 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("command failed: {0}")]
Command(String),
#[error("not found: {0}")]
NotFound(String),
}
+29
View File
@@ -0,0 +1,29 @@
use crate::{config::Config, docker};
pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
let target = docker::discover_target(cfg)?;
if !cfg.anti_torrent.enabled {
let _ = docker::run_capture(&[
"docker",
"exec",
&target.container_name,
"sh",
"-c",
"nft delete table inet awg2_harness 2>/dev/null || true",
])?;
return Ok(());
}
let rule = format!(
"nft 'add table inet awg2_harness' 2>/dev/null || true; \
nft 'add chain inet awg2_harness forward_antitorrent {{ type filter hook forward priority filter; policy accept; }}' 2>/dev/null || true; \
nft 'flush chain inet awg2_harness forward_antitorrent'; \
nft 'add rule inet awg2_harness forward_antitorrent ip saddr {cidr} ct state new meter awg2_peer_connlimit_v4 {{ ip saddr ct count over {max} }} counter name antitorrent_dropped_v4 drop'",
cidr = cfg.anti_torrent.peer_ipv4_cidr,
max = cfg.anti_torrent.max_connections_per_peer
);
let _ = docker::run_capture(&["docker", "exec", &target.container_name, "sh", "-c", &rule])?;
Ok(())
}
+41
View File
@@ -0,0 +1,41 @@
use crate::{config::Config, docker::run_capture};
pub fn detect_interface(cfg: &Config, container: &str) -> anyhow::Result<String> {
for ifname in &cfg.amnezia.interface_candidates {
let ok = std::process::Command::new("docker")
.args(["exec", container, "ip", "link", "show", ifname])
.output()?;
if ok.status.success() {
return Ok(ifname.clone());
}
}
let links = run_capture(&[
"docker",
"exec",
container,
"sh",
"-c",
"ip -o link show | awk -F': ' '{print $2}'",
])?;
for raw in links.lines() {
let name = raw.trim();
for p in &cfg.amnezia.interface_patterns {
if glob_like_match(p, name) {
return Ok(name.to_string());
}
}
}
anyhow::bail!("unable to detect amnezia interface")
}
fn glob_like_match(pattern: &str, value: &str) -> bool {
if let Some(prefix) = pattern.strip_suffix('*') {
value.starts_with(prefix)
} else {
pattern == value
}
}
+28
View File
@@ -0,0 +1,28 @@
use std::{fs, path::Path};
pub struct FileLock {
path: String,
}
impl FileLock {
pub fn acquire(path: &str) -> anyhow::Result<Self> {
let p = Path::new(path);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent)?;
}
if p.exists() {
anyhow::bail!("lock already exists: {path}");
}
fs::write(p, std::process::id().to_string())?;
Ok(Self {
path: path.to_string(),
})
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
+66
View File
@@ -0,0 +1,66 @@
mod cli;
mod config;
mod daemon;
mod docker;
mod error;
mod firewall;
mod iface;
mod lock;
mod netns;
mod reboot;
mod status;
mod swap;
mod traffic;
mod wg;
use std::path::PathBuf;
use clap::Parser;
use crate::cli::{Cli, Command};
use crate::config::Config;
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let config_path = cli
.config
.unwrap_or_else(|| PathBuf::from("/etc/awg2-harness/config.toml"));
match cli.command {
Command::ValidateConfig => {
let cfg = Config::load(&config_path)?;
cfg.validate()?;
println!("config valid: {}", config_path.display());
}
Command::ApplyLimits => {
let cfg = Config::load(&config_path)?;
cfg.validate()?;
traffic::reconcile_traffic(&cfg)?;
println!("traffic limits reconciled");
}
Command::ApplyFirewall => {
let cfg = Config::load(&config_path)?;
cfg.validate()?;
firewall::apply_firewall(&cfg)?;
println!("firewall reconciled");
}
Command::Status => {
let cfg = Config::load(&config_path)?;
cfg.validate()?;
status::print_status(&cfg)?;
}
Command::EnsureSwap => {
let cfg = Config::load(&config_path)?;
cfg.validate()?;
swap::ensure_swap(&cfg)?;
println!("swap ensured");
}
Command::Daemon => {
let cfg = Config::load(&config_path)?;
cfg.validate()?;
daemon::run(cfg)?;
}
}
Ok(())
}
+16
View File
@@ -0,0 +1,16 @@
use std::process::Command;
pub fn nsenter_run_capture(pid: u32, args: &[&str]) -> anyhow::Result<String> {
let mut cmd = Command::new("nsenter");
cmd.args(["-t", &pid.to_string(), "-n"]);
cmd.args(args);
let out = cmd.output()?;
if !out.status.success() {
anyhow::bail!(
"nsenter failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
Ok(String::from_utf8_lossy(&out.stdout).to_string())
}
+44
View File
@@ -0,0 +1,44 @@
use chrono::{Duration, Timelike, Utc};
use rand::Rng;
use crate::config::Config;
pub fn maybe_schedule_reboot(cfg: &Config) -> anyhow::Result<()> {
if !cfg.reboot.enabled {
return Ok(());
}
let start = parse_hhmm(&cfg.reboot.window_start_utc)?;
let end = parse_hhmm(&cfg.reboot.window_end_utc)?;
let now = Utc::now();
let date = now.date_naive();
let start_dt = date
.and_hms_opt(start.0, start.1, 0)
.ok_or_else(|| anyhow::anyhow!("invalid reboot start time"))?;
let end_dt = date
.and_hms_opt(end.0, end.1, 0)
.ok_or_else(|| anyhow::anyhow!("invalid reboot end time"))?;
let span = if end_dt > start_dt {
end_dt - start_dt
} else {
Duration::hours(1)
};
let secs = rand::thread_rng().gen_range(0..span.num_seconds().max(1));
let planned = start_dt + Duration::seconds(secs);
println!("reboot planned at UTC {}", planned);
Ok(())
}
fn parse_hhmm(v: &str) -> anyhow::Result<(u32, u32)> {
let mut s = v.split(':');
let h = s.next().ok_or_else(|| anyhow::anyhow!("bad time"))?.parse()?;
let m = s.next().ok_or_else(|| anyhow::anyhow!("bad time"))?.parse()?;
if h > 23 || m > 59 {
anyhow::bail!("bad time")
}
Ok((h, m))
}
+40
View File
@@ -0,0 +1,40 @@
use crate::{config::Config, docker, wg};
pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
println!("System:");
let target = match docker::discover_target(cfg) {
Ok(t) => t,
Err(e) => {
println!(" container: not available ({e})");
return Ok(());
}
};
println!(" container: {}", target.container_name);
println!(" container_id: {}", target.container_id);
println!(" pid: {}", target.container_pid);
println!(" netns: {}", target.netns_path);
println!(" interface: {}", target.interface_name);
let peers = wg::discover_peers(&target.container_name, &cfg.amnezia.peer_ipv4_cidr)?;
println!("Peers:");
println!(" total: {}", peers.len());
for p in peers {
println!(" - {}", p.ipv4);
}
println!("Traffic desired:");
println!(" peer_limit: {}", cfg.traffic.peer_limit);
println!(" root_rate: {}", cfg.traffic.root_rate);
println!(" ifb_name: {}", cfg.traffic.ifb_name);
println!(" class_start_id: {}", cfg.traffic.class_start_id);
println!("Anti-torrent:");
println!(" enabled: {}", cfg.anti_torrent.enabled);
println!(
" max_connections_per_peer: {}",
cfg.anti_torrent.max_connections_per_peer
);
Ok(())
}
+51
View File
@@ -0,0 +1,51 @@
use std::{fs, process::Command};
use crate::config::Config;
pub fn ensure_swap(cfg: &Config) -> anyhow::Result<()> {
if !cfg.swap.enabled {
return Ok(());
}
let path = &cfg.swap.path;
if !std::path::Path::new(path).exists() {
run("fallocate", &["-l", &format!("{}M", cfg.swap.size_mb), path])?;
run("chmod", &["600", path])?;
run("mkswap", &[path])?;
}
let swapon = Command::new("swapon").args(["--show"]).output()?;
let active = String::from_utf8_lossy(&swapon.stdout);
if !active.contains(path) {
run("swapon", &["-p", &cfg.swap.priority.to_string(), path])?;
}
ensure_fstab(path, cfg.swap.priority)?;
run("sysctl", &["-w", &format!("vm.swappiness={}", cfg.swap.swappiness)])?;
Ok(())
}
fn ensure_fstab(path: &str, priority: i32) -> anyhow::Result<()> {
let file = "/etc/fstab";
let content = fs::read_to_string(file).unwrap_or_default();
if content.lines().any(|l| l.contains(path) && l.contains("swap")) {
return Ok(());
}
let line = format!("{path} none swap sw,pri={priority} 0 0\n");
fs::write(file, format!("{}{}", content, line))?;
Ok(())
}
fn run(program: &str, args: &[&str]) -> anyhow::Result<()> {
let out = Command::new(program).args(args).output()?;
if !out.status.success() {
anyhow::bail!(
"{} {:?} failed: {}",
program,
args,
String::from_utf8_lossy(&out.stderr)
);
}
Ok(())
}
+73
View File
@@ -0,0 +1,73 @@
use crate::{config::Config, docker, lock::FileLock, wg};
pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
if !cfg.traffic.enabled {
return Ok(());
}
let _lock = FileLock::acquire("/run/awg2-harness/traffic.lock")?;
let target = docker::discover_target(cfg)?;
let peers = wg::discover_peers(&target.container_name, &cfg.amnezia.peer_ipv4_cidr)?;
if peers.is_empty() {
println!("no peers found, skip tc apply");
return Ok(());
}
if cfg.traffic.full_rebuild_on_drift {
teardown_owned_topology(&target.container_name, &target.interface_name, &cfg.traffic.ifb_name)?;
apply_full_topology(cfg, &target.container_name, &target.interface_name, &cfg.traffic.ifb_name, &peers)?;
}
Ok(())
}
fn teardown_owned_topology(container: &str, iface: &str, ifb: &str) -> anyhow::Result<()> {
let cmd = format!(
"tc qdisc del dev {iface} root 2>/dev/null || true; \
tc qdisc del dev {iface} ingress 2>/dev/null || true; \
ip link del {ifb} 2>/dev/null || true"
);
let _ = docker::run_capture(&["docker", "exec", container, "sh", "-c", &cmd])?;
Ok(())
}
fn apply_full_topology(
cfg: &Config,
container: &str,
iface: &str,
ifb: &str,
peers: &[wg::Peer],
) -> anyhow::Result<()> {
let setup_cmd = format!(
"tc qdisc add dev {iface} root handle 1: htb; \
tc class add dev {iface} parent 1: classid 1:1 htb rate {root} ceil {root}; \
modprobe ifb 2>/dev/null || true; \
ip link add name {ifb} type ifb 2>/dev/null || true; \
ip link set dev {ifb} up; \
tc qdisc add dev {iface} handle ffff: ingress; \
tc filter add dev {iface} parent ffff: protocol all u32 match u32 0 0 action mirred egress redirect dev {ifb}; \
tc qdisc add dev {ifb} root handle 2: htb; \
tc class add dev {ifb} parent 2: classid 2:1 htb rate {root} ceil {root}",
root = cfg.traffic.root_rate
);
let _ = docker::run_capture(&["docker", "exec", container, "sh", "-c", &setup_cmd])?;
for (idx, p) in peers.iter().enumerate() {
let class_id = cfg.traffic.class_start_id + idx as u16;
let ip = p.ipv4;
let per_peer = format!(
"tc class add dev {iface} parent 1:1 classid 1:{class_id} htb rate {limit} ceil {limit}; \
tc qdisc add dev {iface} parent 1:{class_id} fq_codel; \
tc filter add dev {iface} parent 1: protocol ip u32 match ip dst {ip} flowid 1:{class_id}; \
tc class add dev {ifb} parent 2:1 classid 2:{class_id} htb rate {limit} ceil {limit}; \
tc qdisc add dev {ifb} parent 2:{class_id} fq_codel; \
tc filter add dev {ifb} parent 2: protocol ip u32 match ip src {ip} flowid 2:{class_id}",
limit = cfg.traffic.peer_limit
);
let _ = docker::run_capture(&["docker", "exec", container, "sh", "-c", &per_peer])?;
}
Ok(())
}
+93
View File
@@ -0,0 +1,93 @@
use std::net::Ipv4Addr;
use crate::{config, docker::run_capture};
#[derive(Debug, Clone)]
pub struct Peer {
pub ipv4: Ipv4Addr,
}
pub fn discover_peers(container: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer>> {
let primary = run_capture(&[
"docker",
"exec",
container,
"sh",
"-c",
"wg show all allowed-ips 2>/dev/null",
])
.unwrap_or_default();
let mut peers = parse_allowed_ips(&primary, peer_cidr)?;
if peers.is_empty() {
let fallback = run_capture(&["docker", "exec", container, "sh", "-c", "wg show 2>/dev/null"])
.unwrap_or_default();
peers = parse_fallback_wg_show(&fallback, peer_cidr)?;
}
peers.sort_by_key(|p| p.ipv4.to_string());
peers.dedup_by_key(|p| p.ipv4);
Ok(peers)
}
fn parse_allowed_ips(raw: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer>> {
let mut out = Vec::new();
for line in raw.lines() {
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.len() < 3 {
continue;
}
for tok in cols[2].split(',') {
let tok = tok.trim();
if let Some((ip, pfx)) = tok.split_once('/') {
if pfx != "32" {
continue;
}
if let Ok(v4) = ip.parse::<Ipv4Addr>() {
if config::ip_in_cidr(v4, peer_cidr)? {
out.push(Peer { ipv4: v4 });
}
}
}
}
}
Ok(out)
}
fn parse_fallback_wg_show(raw: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer>> {
let mut out = Vec::new();
for line in raw.lines() {
let t = line.trim();
if !t.starts_with("allowed ips:") {
continue;
}
let rest = t.trim_start_matches("allowed ips:").trim();
for tok in rest.split(',') {
let tok = tok.trim();
if let Some((ip, pfx)) = tok.split_once('/') {
if pfx != "32" {
continue;
}
if let Ok(v4) = ip.parse::<Ipv4Addr>() {
if config::ip_in_cidr(v4, peer_cidr)? {
out.push(Peer { ipv4: v4 });
}
}
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_primary() {
let raw = "wg0 key1 10.8.1.10/32\nwg0 key2 10.8.1.2/32";
let peers = parse_allowed_ips(raw, "10.8.1.0/24").expect("parse");
assert_eq!(peers.len(), 2);
}
}