fix7: продакшен-реализация полного reconcile swap и интеграций
- Полностью переписан swap-модуль в режиме exclusive ownership\n- Добавлены verify/checks, статус фактического swap-state и drift\n- Интегрирован ensure-swap в daemon и systemd ExecStartPre\n- Усилен lock API (таймауты) и применение в traffic/firewall/swap\n- Усилен parser firewall (JSON-first + устойчивый text fallback)\n- Обновлены config validation, runtime tools и default priority\n- Синхронизированы docs и build/package hygiene\n- Удалены member Cargo.lock в пользу корневого lockfile
This commit is contained in:
Generated
-851
@@ -1,851 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[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.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[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 = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[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 = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[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 = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[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 = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[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_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[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 = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[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 = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[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 = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[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 = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -95,12 +95,22 @@ impl Config {
|
||||
anyhow::bail!("traffic.reconcile_interval_secs must be > 0");
|
||||
}
|
||||
if self.swap.enabled {
|
||||
if self.swap.size_mb == 0 {
|
||||
anyhow::bail!("swap.size_mb must be > 0 when swap.enabled=true");
|
||||
if self.swap.size_mb < 64 {
|
||||
anyhow::bail!("swap.size_mb must be >= 64 when swap.enabled=true");
|
||||
}
|
||||
if self.swap.size_mb > 65536 {
|
||||
anyhow::bail!("swap.size_mb must be <= 65536 when swap.enabled=true");
|
||||
}
|
||||
if !self.swap.path.starts_with('/') {
|
||||
anyhow::bail!("swap.path must be absolute");
|
||||
}
|
||||
let dangerous = ["/", "/etc", "/etc/fstab", "/dev", "/proc", "/sys", "/run"];
|
||||
if dangerous.iter().any(|p| *p == self.swap.path) {
|
||||
anyhow::bail!("swap.path is dangerous and forbidden: {}", self.swap.path);
|
||||
}
|
||||
if self.swap.priority < -1 || self.swap.priority > 32767 {
|
||||
anyhow::bail!("swap.priority must be between -1 and 32767");
|
||||
}
|
||||
}
|
||||
if self.anti_torrent.enabled && self.anti_torrent.max_connections_per_peer == 0 {
|
||||
anyhow::bail!("anti_torrent.max_connections_per_peer must be > 0");
|
||||
@@ -140,7 +150,15 @@ impl Config {
|
||||
required.push("systemctl");
|
||||
}
|
||||
if self.swap.enabled {
|
||||
required.extend(["fallocate", "chmod", "mkswap", "swapon", "sysctl"]);
|
||||
required.extend([
|
||||
"fallocate",
|
||||
"chmod",
|
||||
"mkswap",
|
||||
"swapon",
|
||||
"swapoff",
|
||||
"sysctl",
|
||||
"dd",
|
||||
]);
|
||||
}
|
||||
|
||||
for tool in required {
|
||||
@@ -209,7 +227,9 @@ fn tool_exists(tool: &str) -> bool {
|
||||
"chmod" => &[&["--help"]],
|
||||
"mkswap" => &[&["--help"]],
|
||||
"swapon" => &[&["--help"]],
|
||||
"swapoff" => &[&["--help"]],
|
||||
"sysctl" => &[&["--version"], &["--help"]],
|
||||
"dd" => &[&["--help"]],
|
||||
_ => &[&["--version"], &["--help"], &["-V"]],
|
||||
};
|
||||
|
||||
@@ -280,4 +300,44 @@ mod tests {
|
||||
assert_eq!(parse_tc_rate_bps("1000mbit").expect("bps"), 1_000_000_000);
|
||||
assert!(parse_tc_rate_bps("foo").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_rejects_dangerous_path() {
|
||||
let cfg = Config {
|
||||
amnezia: AmneziaConfig {
|
||||
container_names: vec!["x".into()],
|
||||
interface_candidates: vec!["awg0".into()],
|
||||
interface_patterns: vec!["awg*".into()],
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
},
|
||||
traffic: TrafficConfig {
|
||||
enabled: true,
|
||||
peer_limit: "70mbit".into(),
|
||||
root_rate: "1000mbit".into(),
|
||||
reconcile_interval_secs: 15,
|
||||
container_ready_timeout_secs: 30,
|
||||
class_start_id: 10,
|
||||
ifb_name: "ifb0".into(),
|
||||
},
|
||||
anti_torrent: AntiTorrentConfig {
|
||||
enabled: false,
|
||||
max_connections_per_peer: 100,
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
apply_in_amnezia_netns: false,
|
||||
},
|
||||
reboot: RebootConfig {
|
||||
enabled: false,
|
||||
window_start_utc: "01:00".into(),
|
||||
window_end_utc: "02:00".into(),
|
||||
},
|
||||
swap: SwapConfig {
|
||||
enabled: true,
|
||||
path: "/etc/fstab".into(),
|
||||
size_mb: 2048,
|
||||
priority: 1,
|
||||
swappiness: 1,
|
||||
},
|
||||
};
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{config::Config, docker, firewall, iface, netns::build_transport, traffic};
|
||||
use crate::{config::Config, docker, firewall, iface, netns::build_transport, swap, traffic};
|
||||
use tracing::{error, info};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -26,6 +26,15 @@ pub fn run(cfg: Config) -> anyhow::Result<()> {
|
||||
info!("starting daemon");
|
||||
let state = Arc::new(Mutex::new(LastActionState::default()));
|
||||
|
||||
if let Err(e) = swap::ensure_swap(&cfg) {
|
||||
set_err(&state, format!("startup swap reconcile: {e}"));
|
||||
append_event(&format!("startup_swap error=\"{e}\""));
|
||||
return Err(e);
|
||||
} else {
|
||||
set_ok(&state, "startup swap reconcile ok");
|
||||
append_event("startup_swap ok");
|
||||
}
|
||||
|
||||
if let Err(e) = traffic::reconcile_traffic(&cfg) {
|
||||
set_err(&state, format!("startup traffic reconcile: {e}"));
|
||||
append_event(&format!("startup_reconcile traffic=error error=\"{e}\""));
|
||||
@@ -60,6 +69,15 @@ pub fn run(cfg: Config) -> anyhow::Result<()> {
|
||||
});
|
||||
|
||||
loop {
|
||||
if let Err(e) = swap::ensure_swap(&cfg) {
|
||||
error!(error = %e, "swap reconcile error");
|
||||
set_err(&state, format!("periodic swap reconcile: {e}"));
|
||||
append_event(&format!("periodic_reconcile swap=error error=\"{e}\""));
|
||||
} else {
|
||||
set_ok(&state, "periodic swap reconcile ok");
|
||||
append_event("periodic_reconcile swap=ok");
|
||||
}
|
||||
|
||||
if let Err(e) = traffic::reconcile_traffic(&cfg) {
|
||||
error!(error = %e, "traffic reconcile error");
|
||||
set_err(&state, format!("periodic traffic reconcile: {e}"));
|
||||
@@ -199,7 +217,11 @@ fn append_event(event: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(mut f) = fs::OpenOptions::new().create(true).write(true).truncate(true).open(EVENTS_FILE)
|
||||
if let Ok(mut f) = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(EVENTS_FILE)
|
||||
{
|
||||
let _ = f.write_all(content.as_bytes());
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
docker,
|
||||
lock::FileLock,
|
||||
netns::{NsTransport, build_host_transport, build_transport},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FirewallActual {
|
||||
@@ -20,6 +22,9 @@ struct FirewallActual {
|
||||
}
|
||||
|
||||
pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
|
||||
let _lock =
|
||||
FileLock::acquire_timeout("/run/awg2-harness/firewall.lock", Duration::from_secs(10))?;
|
||||
|
||||
if !cfg.anti_torrent.enabled {
|
||||
apply_firewall_disabled(cfg)?;
|
||||
return Ok(());
|
||||
@@ -94,6 +99,13 @@ fn apply_firewall_disabled(cfg: &Config) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result<FirewallActual> {
|
||||
let json = transport.run(&["nft", "-j", "list", "table", "inet", "awg2_harness"]);
|
||||
if let Ok(raw) = json {
|
||||
if let Some(actual) = inspect_firewall_json(&raw, cfg)? {
|
||||
return Ok(actual);
|
||||
}
|
||||
}
|
||||
|
||||
let table = match transport.run(&["nft", "list", "table", "inet", "awg2_harness"]) {
|
||||
Ok(out) => out,
|
||||
Err(e) if is_nft_not_found(&e.to_string()) => {
|
||||
@@ -113,15 +125,16 @@ fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result
|
||||
Err(e) => return Err(anyhow::anyhow!("nft table inspect failed: {e}")),
|
||||
};
|
||||
|
||||
let table_exists = !table.trim().is_empty();
|
||||
let counter_exists = table.contains("counter antitorrent_dropped_v4");
|
||||
let chain_exists = table.contains("chain forward_antitorrent");
|
||||
let hook_forward_present = table.contains("hook forward");
|
||||
let policy_accept_present = table.contains("policy accept");
|
||||
let ct_state_new_present = table.contains("ct state new");
|
||||
let meter_present = table.contains("meter awg2_peer_connlimit_v4");
|
||||
let counter_name_used = table.contains("counter name antitorrent_dropped_v4");
|
||||
let drop_present = table.contains(" drop") || table.contains("\ndrop");
|
||||
let nt = normalize_nft_text(&table);
|
||||
let table_exists = !nt.trim().is_empty();
|
||||
let counter_exists = nt.contains("counter antitorrent_dropped_v4");
|
||||
let chain_exists = nt.contains("chain forward_antitorrent");
|
||||
let hook_forward_present = nt.contains("hook forward");
|
||||
let policy_accept_present = nt.contains("policy accept");
|
||||
let ct_state_new_present = nt.contains("ct state new");
|
||||
let meter_present = nt.contains("meter awg2_peer_connlimit_v4");
|
||||
let counter_name_used = nt.contains("counter name antitorrent_dropped_v4");
|
||||
let drop_present = nt.contains(" drop");
|
||||
|
||||
let rule_matches = firewall_rule_contract_matches(
|
||||
&table,
|
||||
@@ -292,12 +305,15 @@ fn firewall_drift_reason(actual: &FirewallActual) -> String {
|
||||
}
|
||||
|
||||
fn firewall_rule_contract_matches(table: &str, cidr: &str, max_conn: u32) -> bool {
|
||||
let table = normalize_nft_text(table);
|
||||
let needle_cidr = format!("ip saddr {cidr}");
|
||||
let needle_max = format!("ct count over {max_conn}");
|
||||
table.contains("table inet awg2_harness")
|
||||
&& table.contains("counter antitorrent_dropped_v4")
|
||||
&& table.contains("chain forward_antitorrent")
|
||||
&& table.contains("type filter hook forward priority filter; policy accept")
|
||||
&& table.contains("type filter hook forward")
|
||||
&& (table.contains("priority filter") || table.contains("priority 0"))
|
||||
&& table.contains("policy accept")
|
||||
&& table.contains(&needle_cidr)
|
||||
&& table.contains("ct state new")
|
||||
&& table.contains("meter awg2_peer_connlimit_v4")
|
||||
@@ -306,6 +322,48 @@ fn firewall_rule_contract_matches(table: &str, cidr: &str, max_conn: u32) -> boo
|
||||
&& table.contains("drop")
|
||||
}
|
||||
|
||||
fn normalize_nft_text(input: &str) -> String {
|
||||
input
|
||||
.replace(['\n', '\t', '{', '}', ';', ','], " ")
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn inspect_firewall_json(raw: &str, cfg: &Config) -> anyhow::Result<Option<FirewallActual>> {
|
||||
let v: Value = serde_json::from_str(raw)?;
|
||||
let Some(arr) = v.get("nftables").and_then(|x| x.as_array()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut text = String::new();
|
||||
for item in arr {
|
||||
text.push_str(&item.to_string());
|
||||
text.push(' ');
|
||||
}
|
||||
let nt = normalize_nft_text(&text);
|
||||
let table_exists = nt.contains("awg2_harness");
|
||||
if !table_exists {
|
||||
return Ok(None);
|
||||
}
|
||||
let actual = FirewallActual {
|
||||
table_exists,
|
||||
counter_exists: nt.contains("antitorrent_dropped_v4"),
|
||||
chain_exists: nt.contains("forward_antitorrent"),
|
||||
rule_matches: firewall_rule_contract_matches(
|
||||
&nt,
|
||||
&cfg.anti_torrent.peer_ipv4_cidr,
|
||||
cfg.anti_torrent.max_connections_per_peer,
|
||||
),
|
||||
hook_forward_present: nt.contains("hook") && nt.contains("forward"),
|
||||
policy_accept_present: nt.contains("policy") && nt.contains("accept"),
|
||||
ct_state_new_present: nt.contains("ct") && nt.contains("state") && nt.contains("new"),
|
||||
meter_present: nt.contains("awg2_peer_connlimit_v4"),
|
||||
counter_name_used: nt.contains("antitorrent_dropped_v4"),
|
||||
drop_present: nt.contains("drop"),
|
||||
};
|
||||
Ok(Some(actual))
|
||||
}
|
||||
|
||||
pub fn antitorrent_drop_counter(cfg: &Config) -> anyhow::Result<Option<(u64, u64)>> {
|
||||
if !cfg.anti_torrent.enabled {
|
||||
return Ok(None);
|
||||
@@ -412,4 +470,18 @@ table inet awg2_harness {
|
||||
"#;
|
||||
assert!(!firewall_rule_contract_matches(table, "10.8.1.0/24", 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nft_text_parser_tolerates_priority_0_and_spacing() {
|
||||
let table = r#"
|
||||
table inet awg2_harness {
|
||||
counter antitorrent_dropped_v4
|
||||
chain forward_antitorrent {
|
||||
type filter hook forward priority 0; policy accept;
|
||||
ip saddr 10.8.1.0/24 ct state new meter awg2_peer_connlimit_v4 { ip saddr ct count over 100 } counter name antitorrent_dropped_v4 drop
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(firewall_rule_contract_matches(table, "10.8.1.0/24", 100));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use std::{
|
||||
fs::{self, OpenOptions},
|
||||
io::Write,
|
||||
path::Path,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub struct FileLock {
|
||||
@@ -10,6 +12,29 @@ pub struct FileLock {
|
||||
|
||||
impl FileLock {
|
||||
pub fn acquire(path: &str) -> anyhow::Result<Self> {
|
||||
Self::acquire_timeout(path, Duration::from_secs(0))
|
||||
}
|
||||
|
||||
pub fn acquire_timeout(path: &str, timeout: Duration) -> anyhow::Result<Self> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
match Self::try_acquire_once(path) {
|
||||
Ok(lock) => return Ok(lock),
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if !msg.contains("lock already exists") {
|
||||
return Err(e);
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
anyhow::bail!("lock busy timeout: {path}");
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_acquire_once(path: &str) -> anyhow::Result<Self> {
|
||||
let p = Path::new(path);
|
||||
if let Some(parent) = p.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
@@ -66,4 +91,12 @@ mod tests {
|
||||
let second = FileLock::acquire(&p);
|
||||
assert!(second.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquire_timeout_times_out_for_live_lock() {
|
||||
let p = format!("/tmp/awg2_harness_lock_test_timeout_{}", std::process::id());
|
||||
let _first = FileLock::acquire(&p).expect("first lock");
|
||||
let second = FileLock::acquire_timeout(&p, Duration::from_millis(250));
|
||||
assert!(second.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+48
-12
@@ -1,4 +1,4 @@
|
||||
use crate::{config::Config, docker, firewall, netns::build_transport, traffic, wg};
|
||||
use crate::{config::Config, docker, firewall, netns::build_transport, swap, traffic, wg};
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
@@ -183,12 +183,7 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Swap:");
|
||||
println!(" enabled: {}", cfg.swap.enabled);
|
||||
println!(" path: {}", cfg.swap.path);
|
||||
println!(" size_mb: {}", cfg.swap.size_mb);
|
||||
println!(" priority: {}", cfg.swap.priority);
|
||||
println!(" swappiness: {}", cfg.swap.swappiness);
|
||||
print_swap_status(cfg);
|
||||
|
||||
let ct_count = Command::new("sysctl")
|
||||
.args(["-n", "net.netfilter.nf_conntrack_count"])
|
||||
@@ -265,11 +260,7 @@ fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Swap:");
|
||||
println!(" enabled: {}", cfg.swap.enabled);
|
||||
println!(" path: {}", cfg.swap.path);
|
||||
println!(" size_mb: {}", cfg.swap.size_mb);
|
||||
println!(" priority: {}", cfg.swap.priority);
|
||||
print_swap_status(cfg);
|
||||
|
||||
println!("Conntrack:");
|
||||
let ct_count = Command::new("sysctl")
|
||||
@@ -336,6 +327,51 @@ fn print_reboot_section(cfg: &Config) {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_swap_status(cfg: &Config) {
|
||||
println!("Swap:");
|
||||
match swap::inspect_swap(cfg) {
|
||||
Ok(s) => {
|
||||
println!(" enabled: {}", s.enabled);
|
||||
println!(" desired_path: {}", s.desired_path.display());
|
||||
println!(" desired_size_mb: {}", cfg.swap.size_mb);
|
||||
println!(" desired_priority: {}", cfg.swap.priority);
|
||||
println!(" active_entries: {}", s.active_entries.len());
|
||||
println!(
|
||||
" desired_active: {}",
|
||||
if s.desired_active { "yes" } else { "no" }
|
||||
);
|
||||
println!(" exclusive: {}", if s.exclusive { "yes" } else { "no" });
|
||||
println!(
|
||||
" fstab_exclusive: {}",
|
||||
if s.fstab_exclusive { "yes" } else { "no" }
|
||||
);
|
||||
println!(
|
||||
" swappiness_current: {}",
|
||||
s.current_swappiness
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "n/a".to_string())
|
||||
);
|
||||
println!(
|
||||
" swappiness_persisted: {}",
|
||||
s.persisted_swappiness
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "n/a".to_string())
|
||||
);
|
||||
if let Some(reason) = s.drift_reason {
|
||||
println!(" drift: yes");
|
||||
println!(" drift_reason: {}", reason);
|
||||
} else {
|
||||
println!(" drift: no");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" enabled: {}", cfg.swap.enabled);
|
||||
println!(" desired_path: {}", cfg.swap.path);
|
||||
println!(" drift: unknown ({e})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handshake_is_online(epoch: Option<u64>, threshold_secs: u64) -> &'static str {
|
||||
if let Some(ts) = epoch {
|
||||
let now = SystemTime::now()
|
||||
|
||||
+538
-48
@@ -1,61 +1,484 @@
|
||||
use std::{fs, process::Command};
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::{config::Config, lock::FileLock};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ActiveSwap {
|
||||
pub name: PathBuf,
|
||||
pub kind: String,
|
||||
pub size_bytes: u64,
|
||||
pub priority: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DesiredSwap {
|
||||
path: PathBuf,
|
||||
size_bytes: u64,
|
||||
priority: i32,
|
||||
swappiness: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SwapStatus {
|
||||
pub enabled: bool,
|
||||
pub desired_path: PathBuf,
|
||||
pub active_entries: Vec<ActiveSwap>,
|
||||
pub desired_active: bool,
|
||||
pub exclusive: bool,
|
||||
pub fstab_swap_entries: Vec<String>,
|
||||
pub fstab_exclusive: bool,
|
||||
pub current_swappiness: Option<u8>,
|
||||
pub persisted_swappiness: Option<u8>,
|
||||
pub drift_reason: Option<String>,
|
||||
}
|
||||
|
||||
pub fn ensure_swap(cfg: &Config) -> anyhow::Result<()> {
|
||||
if !cfg.swap.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let path = &cfg.swap.path;
|
||||
let expected_size = cfg.swap.size_mb * 1024 * 1024;
|
||||
if std::path::Path::new(path).exists() {
|
||||
let actual_size = fs::metadata(path)?.len();
|
||||
if actual_size != expected_size {
|
||||
anyhow::bail!(
|
||||
"existing swapfile has wrong size: {} bytes, expected {} bytes; manual intervention required",
|
||||
actual_size,
|
||||
expected_size
|
||||
);
|
||||
}
|
||||
} else {
|
||||
run(
|
||||
"fallocate",
|
||||
&["-l", &format!("{}M", cfg.swap.size_mb), path],
|
||||
)?;
|
||||
run("chmod", &["600", path])?;
|
||||
run("mkswap", &[path])?;
|
||||
}
|
||||
let _lock = FileLock::acquire_timeout("/run/awg2-harness/swap.lock", Duration::from_secs(30))?;
|
||||
|
||||
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)],
|
||||
)?;
|
||||
ensure_swappiness_persist(cfg.swap.swappiness)?;
|
||||
let desired = DesiredSwap::from_config(cfg)?;
|
||||
let active_before = read_active_swaps()?;
|
||||
disable_foreign_swaps(&desired, &active_before)?;
|
||||
ensure_desired_swapfile(&desired)?;
|
||||
rewrite_fstab_exclusive(&desired)?;
|
||||
enable_desired_swap(&desired)?;
|
||||
apply_swappiness(desired.swappiness)?;
|
||||
verify_exclusive_swap_state(&desired)?;
|
||||
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"))
|
||||
pub fn inspect_swap(cfg: &Config) -> anyhow::Result<SwapStatus> {
|
||||
let desired_path = PathBuf::from(cfg.swap.path.clone());
|
||||
let active_entries = read_active_swaps()?;
|
||||
let desired_active = active_entries.iter().any(|s| s.name == desired_path);
|
||||
let exclusive = active_entries.len() == 1 && desired_active;
|
||||
let fstab_swap_entries = read_fstab_swap_entries("/etc/fstab")?;
|
||||
let expected_line = render_fstab_line(&desired_path, cfg.swap.priority);
|
||||
let fstab_exclusive = fstab_swap_entries.len() == 1
|
||||
&& normalize_ws(&fstab_swap_entries[0])
|
||||
== normalize_ws(&expected_line.trim_end().to_string());
|
||||
let current_swappiness = read_current_swappiness().ok();
|
||||
let persisted_swappiness = read_persisted_swappiness()?;
|
||||
|
||||
let drift_reason = if !cfg.swap.enabled {
|
||||
None
|
||||
} else if !exclusive {
|
||||
if let Some(foreign) = active_entries.iter().find(|s| s.name != desired_path) {
|
||||
Some(format!("foreign active swap: {}", foreign.name.display()))
|
||||
} else {
|
||||
Some("desired swap is not exclusively active".to_string())
|
||||
}
|
||||
} else if !fstab_exclusive {
|
||||
Some("/etc/fstab swap entries are not exclusive".to_string())
|
||||
} else if current_swappiness != Some(cfg.swap.swappiness) {
|
||||
Some(format!(
|
||||
"runtime swappiness mismatch: current={:?} desired={}",
|
||||
current_swappiness, cfg.swap.swappiness
|
||||
))
|
||||
} else if persisted_swappiness != Some(cfg.swap.swappiness) {
|
||||
Some(format!(
|
||||
"persisted swappiness mismatch: persisted={:?} desired={}",
|
||||
persisted_swappiness, cfg.swap.swappiness
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(SwapStatus {
|
||||
enabled: cfg.swap.enabled,
|
||||
desired_path,
|
||||
active_entries,
|
||||
desired_active,
|
||||
exclusive,
|
||||
fstab_swap_entries,
|
||||
fstab_exclusive,
|
||||
current_swappiness,
|
||||
persisted_swappiness,
|
||||
drift_reason,
|
||||
})
|
||||
}
|
||||
|
||||
impl DesiredSwap {
|
||||
fn from_config(cfg: &Config) -> anyhow::Result<Self> {
|
||||
let path = PathBuf::from(cfg.swap.path.clone());
|
||||
if !path.is_absolute() {
|
||||
anyhow::bail!("swap.path must be absolute: {}", path.display());
|
||||
}
|
||||
let size_bytes = cfg
|
||||
.swap
|
||||
.size_mb
|
||||
.checked_mul(1024 * 1024)
|
||||
.ok_or_else(|| anyhow::anyhow!("swap.size_mb overflow"))?;
|
||||
Ok(Self {
|
||||
path,
|
||||
size_bytes,
|
||||
priority: cfg.swap.priority,
|
||||
swappiness: cfg.swap.swappiness,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn read_active_swaps() -> anyhow::Result<Vec<ActiveSwap>> {
|
||||
let out = Command::new("swapon")
|
||||
.args([
|
||||
"--show",
|
||||
"--bytes",
|
||||
"--raw",
|
||||
"--noheadings",
|
||||
"--output",
|
||||
"NAME,TYPE,SIZE,PRIO",
|
||||
])
|
||||
.output()?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!(
|
||||
"swapon --show failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
parse_swapon_output(&String::from_utf8_lossy(&out.stdout))
|
||||
}
|
||||
|
||||
fn parse_swapon_output(raw: &str) -> anyhow::Result<Vec<ActiveSwap>> {
|
||||
let mut out = Vec::new();
|
||||
for line in raw.lines() {
|
||||
let t = line.trim();
|
||||
if t.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let parts: Vec<&str> = t.split_whitespace().collect();
|
||||
if parts.len() != 4 {
|
||||
anyhow::bail!("malformed swapon row: {t}");
|
||||
}
|
||||
let size_bytes = parts[2]
|
||||
.parse::<u64>()
|
||||
.map_err(|_| anyhow::anyhow!("malformed swapon size: {}", parts[2]))?;
|
||||
let priority = parts[3]
|
||||
.parse::<i32>()
|
||||
.map_err(|_| anyhow::anyhow!("malformed swapon priority: {}", parts[3]))?;
|
||||
out.push(ActiveSwap {
|
||||
name: PathBuf::from(parts[0]),
|
||||
kind: parts[1].to_string(),
|
||||
size_bytes,
|
||||
priority,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn disable_foreign_swaps(desired: &DesiredSwap, active: &[ActiveSwap]) -> anyhow::Result<()> {
|
||||
for entry in active {
|
||||
if entry.name != desired.path {
|
||||
run("swapoff", &[entry.name.to_string_lossy().as_ref()])?;
|
||||
maybe_delete_foreign_swapfile(entry, &desired.path)?;
|
||||
} else if entry.size_bytes != desired.size_bytes || entry.priority != desired.priority {
|
||||
run("swapoff", &[desired.path.to_string_lossy().as_ref()])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn maybe_delete_foreign_swapfile(entry: &ActiveSwap, desired_path: &Path) -> anyhow::Result<()> {
|
||||
if entry.name == desired_path {
|
||||
return Ok(());
|
||||
}
|
||||
let md = match fs::symlink_metadata(&entry.name) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
if md.file_type().is_symlink() {
|
||||
return Ok(());
|
||||
}
|
||||
if md.file_type().is_file() {
|
||||
let _ = fs::remove_file(&entry.name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_desired_swapfile(desired: &DesiredSwap) -> anyhow::Result<()> {
|
||||
let p = &desired.path;
|
||||
if !p.is_absolute() {
|
||||
anyhow::bail!("swap.path must be absolute: {}", p.display());
|
||||
}
|
||||
|
||||
let mut need_create = true;
|
||||
if p.exists() {
|
||||
let md = fs::symlink_metadata(p)?;
|
||||
if md.file_type().is_symlink() {
|
||||
anyhow::bail!("desired swap path is symlink: {}", p.display());
|
||||
}
|
||||
if !md.file_type().is_file() {
|
||||
anyhow::bail!("desired swap path is not regular file: {}", p.display());
|
||||
}
|
||||
|
||||
if md.len() == desired.size_bytes {
|
||||
need_create = false;
|
||||
} else {
|
||||
fs::remove_file(p)?;
|
||||
}
|
||||
}
|
||||
|
||||
if need_create {
|
||||
create_swapfile(p, desired.size_bytes)?;
|
||||
run("mkswap", &[p.to_string_lossy().as_ref()])?;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
fs::set_permissions(p, fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_swapfile(path: &Path, size_bytes: u64) -> anyhow::Result<()> {
|
||||
let size_mb = size_bytes / (1024 * 1024);
|
||||
let p = path.to_string_lossy().to_string();
|
||||
|
||||
let out = Command::new("fallocate")
|
||||
.args(["-l", &format!("{}M", size_mb), &p])
|
||||
.output()?;
|
||||
if out.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let out_dd = Command::new("dd")
|
||||
.args([
|
||||
"if=/dev/zero",
|
||||
&format!("of={p}"),
|
||||
"bs=1M",
|
||||
&format!("count={size_mb}"),
|
||||
"status=none",
|
||||
])
|
||||
.output()?;
|
||||
if !out_dd.status.success() {
|
||||
anyhow::bail!(
|
||||
"swapfile create failed (fallocate+dd): fallocate={}, dd={}",
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
String::from_utf8_lossy(&out_dd.stderr)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rewrite_fstab_exclusive(desired: &DesiredSwap) -> anyhow::Result<()> {
|
||||
let fstab_path = "/etc/fstab";
|
||||
let bak_path = "/etc/fstab.awg2-harness.bak";
|
||||
let tmp_path = "/etc/fstab.awg2-harness.tmp";
|
||||
|
||||
let content = fs::read_to_string(fstab_path).unwrap_or_default();
|
||||
let rewritten = rewrite_fstab_content(&content, &desired.path, desired.priority);
|
||||
if rewritten == content {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _ = fs::write(bak_path, &content);
|
||||
let mut f = fs::File::create(tmp_path)?;
|
||||
f.write_all(rewritten.as_bytes())?;
|
||||
f.sync_all()?;
|
||||
drop(f);
|
||||
fs::rename(tmp_path, fstab_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rewrite_fstab_content(content: &str, desired_path: &Path, priority: i32) -> String {
|
||||
let mut out = Vec::<String>::new();
|
||||
for line in content.lines() {
|
||||
let t = line.trim();
|
||||
if t.is_empty() || t.starts_with('#') {
|
||||
out.push(line.to_string());
|
||||
continue;
|
||||
}
|
||||
let cols: Vec<&str> = t.split_whitespace().collect();
|
||||
if cols.len() >= 3 && cols[2] == "swap" {
|
||||
continue;
|
||||
}
|
||||
out.push(line.to_string());
|
||||
}
|
||||
out.push(
|
||||
render_fstab_line(desired_path, priority)
|
||||
.trim_end()
|
||||
.to_string(),
|
||||
);
|
||||
let mut s = out.join("\n");
|
||||
if !s.ends_with('\n') {
|
||||
s.push('\n');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn render_fstab_line(path: &Path, priority: i32) -> String {
|
||||
format!("{} none swap sw,pri={} 0 0\n", path.display(), priority)
|
||||
}
|
||||
|
||||
fn enable_desired_swap(desired: &DesiredSwap) -> anyhow::Result<()> {
|
||||
let active = read_active_swaps()?;
|
||||
if let Some(found) = active.iter().find(|v| v.name == desired.path)
|
||||
&& found.size_bytes == desired.size_bytes
|
||||
&& found.priority == desired.priority
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let line = format!("{path} none swap sw,pri={priority} 0 0\n");
|
||||
fs::write(file, format!("{}{}", content, line))?;
|
||||
|
||||
run(
|
||||
"swapon",
|
||||
&[
|
||||
"-p",
|
||||
&desired.priority.to_string(),
|
||||
desired.path.to_string_lossy().as_ref(),
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_swappiness(v: u8) -> anyhow::Result<()> {
|
||||
run("sysctl", &["-w", &format!("vm.swappiness={v}")])?;
|
||||
let current = read_current_swappiness()?;
|
||||
if current != v {
|
||||
anyhow::bail!("failed to apply vm.swappiness: got {current}, expected {v}");
|
||||
}
|
||||
|
||||
let conf_path = "/etc/sysctl.d/99-awg2-harness.conf";
|
||||
let expected = format!("# Managed by awg2-harness\nvm.swappiness = {v}\n");
|
||||
let current_file = fs::read_to_string(conf_path).unwrap_or_default();
|
||||
if current_file != expected {
|
||||
fs::write(conf_path, expected)?;
|
||||
}
|
||||
|
||||
let persisted = read_persisted_swappiness()?;
|
||||
if persisted != Some(v) {
|
||||
anyhow::bail!(
|
||||
"failed to persist vm.swappiness: got {:?}, expected {}",
|
||||
persisted,
|
||||
v
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_current_swappiness() -> anyhow::Result<u8> {
|
||||
let out = Command::new("sysctl")
|
||||
.args(["-n", "vm.swappiness"])
|
||||
.output()?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!("sysctl read vm.swappiness failed");
|
||||
}
|
||||
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
s.parse::<u8>()
|
||||
.with_context(|| format!("invalid vm.swappiness value: {s}"))
|
||||
}
|
||||
|
||||
pub fn read_persisted_swappiness() -> anyhow::Result<Option<u8>> {
|
||||
let conf_path = "/etc/sysctl.d/99-awg2-harness.conf";
|
||||
let content = match fs::read_to_string(conf_path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
for line in content.lines() {
|
||||
let t = line.trim();
|
||||
if t.is_empty() || t.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let Some((k, v)) = t.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
if k.trim() == "vm.swappiness" {
|
||||
return Ok(Some(v.trim().parse::<u8>()?));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn verify_exclusive_swap_state(desired: &DesiredSwap) -> anyhow::Result<()> {
|
||||
let active = read_active_swaps()?;
|
||||
if active.len() != 1 {
|
||||
anyhow::bail!(
|
||||
"swap verify failed: expected exactly one active swap, got {}",
|
||||
active.len()
|
||||
);
|
||||
}
|
||||
let only = &active[0];
|
||||
if only.name != desired.path {
|
||||
anyhow::bail!(
|
||||
"swap verify failed: active swap {} != desired {}",
|
||||
only.name.display(),
|
||||
desired.path.display()
|
||||
);
|
||||
}
|
||||
if only.size_bytes != desired.size_bytes {
|
||||
anyhow::bail!(
|
||||
"swap verify failed: size mismatch {} != {}",
|
||||
only.size_bytes,
|
||||
desired.size_bytes
|
||||
);
|
||||
}
|
||||
if only.priority != desired.priority {
|
||||
anyhow::bail!(
|
||||
"swap verify failed: priority mismatch {} != {}",
|
||||
only.priority,
|
||||
desired.priority
|
||||
);
|
||||
}
|
||||
|
||||
let entries = read_fstab_swap_entries("/etc/fstab")?;
|
||||
if entries.len() != 1 {
|
||||
anyhow::bail!(
|
||||
"swap verify failed: expected one fstab swap entry, got {}",
|
||||
entries.len()
|
||||
);
|
||||
}
|
||||
let expected = render_fstab_line(&desired.path, desired.priority);
|
||||
if normalize_ws(&entries[0]) != normalize_ws(expected.trim_end()) {
|
||||
anyhow::bail!(
|
||||
"swap verify failed: fstab entry mismatch: got='{}' expected='{}'",
|
||||
entries[0],
|
||||
expected.trim_end()
|
||||
);
|
||||
}
|
||||
|
||||
let current = read_current_swappiness()?;
|
||||
if current != desired.swappiness {
|
||||
anyhow::bail!(
|
||||
"swap verify failed: vm.swappiness current={} expected={}",
|
||||
current,
|
||||
desired.swappiness
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_fstab_swap_entries(path: &str) -> anyhow::Result<Vec<String>> {
|
||||
let content = fs::read_to_string(path).unwrap_or_default();
|
||||
let mut out = Vec::new();
|
||||
for line in content.lines() {
|
||||
let t = line.trim();
|
||||
if t.is_empty() || t.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let cols: Vec<&str> = t.split_whitespace().collect();
|
||||
if cols.len() >= 3 && cols[2] == "swap" {
|
||||
out.push(t.to_string());
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn normalize_ws(s: &str) -> String {
|
||||
s.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
fn run(program: &str, args: &[&str]) -> anyhow::Result<()> {
|
||||
let out = Command::new(program).args(args).output()?;
|
||||
if !out.status.success() {
|
||||
@@ -69,12 +492,79 @@ fn run(program: &str, args: &[&str]) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_swappiness_persist(swappiness: u8) -> anyhow::Result<()> {
|
||||
let conf_path = "/etc/sysctl.d/99-awg2-harness.conf";
|
||||
let expected = format!("vm.swappiness = {}\n", swappiness);
|
||||
let current = fs::read_to_string(conf_path).unwrap_or_default();
|
||||
if current != expected {
|
||||
fs::write(conf_path, expected)?;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_swapon_empty() {
|
||||
let got = parse_swapon_output("").expect("parse");
|
||||
assert!(got.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_swapon_single_file() {
|
||||
let got = parse_swapon_output("/swapfile file 2147479552 1\n").expect("parse");
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name, PathBuf::from("/swapfile"));
|
||||
assert_eq!(got[0].kind, "file");
|
||||
assert_eq!(got[0].size_bytes, 2147479552);
|
||||
assert_eq!(got[0].priority, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_swapon_multiple_entries() {
|
||||
let raw = "/swapfile file 2147479552 1\n/dev/zram0 partition 1073741824 100\n";
|
||||
let got = parse_swapon_output(raw).expect("parse");
|
||||
assert_eq!(got.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_swapon_rejects_malformed_size() {
|
||||
let err = parse_swapon_output("/swapfile file abc 1\n");
|
||||
assert!(err.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fstab_rewrite_removes_all_swap_entries() {
|
||||
let raw =
|
||||
"UUID=x / ext4 defaults 0 1\n/swap-old none swap sw 0 0\n/dev/zram0 none swap sw 0 0\n";
|
||||
let out = rewrite_fstab_content(raw, Path::new("/swapfile"), 1);
|
||||
assert!(!out.contains("/swap-old"));
|
||||
assert!(!out.contains("/dev/zram0"));
|
||||
assert!(out.contains("/swapfile none swap sw,pri=1 0 0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fstab_rewrite_preserves_comments_and_mounts() {
|
||||
let raw = "# head\nUUID=x / ext4 defaults 0 1\n\n# tail\n";
|
||||
let out = rewrite_fstab_content(raw, Path::new("/swapfile"), 1);
|
||||
assert!(out.contains("# head"));
|
||||
assert!(out.contains("UUID=x / ext4 defaults 0 1"));
|
||||
assert!(out.contains("# tail"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fstab_rewrite_is_idempotent() {
|
||||
let raw = "UUID=x / ext4 defaults 0 1\n/swapfile none swap sw,pri=1 0 0\n";
|
||||
let one = rewrite_fstab_content(raw, Path::new("/swapfile"), 1);
|
||||
let two = rewrite_fstab_content(&one, Path::new("/swapfile"), 1);
|
||||
assert_eq!(one, two);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fstab_rewrite_adds_trailing_newline() {
|
||||
let raw = "UUID=x / ext4 defaults 0 1";
|
||||
let out = rewrite_fstab_content(raw, Path::new("/swapfile"), 1);
|
||||
assert!(out.ends_with('\n'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swappiness_conf_render_is_stable() {
|
||||
let expected = "# Managed by awg2-harness\nvm.swappiness = 1\n";
|
||||
assert_eq!(
|
||||
expected,
|
||||
format!("# Managed by awg2-harness\nvm.swappiness = {}\n", 1)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
net::Ipv4Addr,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -82,7 +83,8 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _lock = FileLock::acquire("/run/awg2-harness/traffic.lock")?;
|
||||
let _lock =
|
||||
FileLock::acquire_timeout("/run/awg2-harness/traffic.lock", Duration::from_secs(10))?;
|
||||
let target = docker::discover_target(cfg)?;
|
||||
let transport = build_transport(target.container_pid, &target.container_name);
|
||||
let peers = wg::discover_peers(
|
||||
@@ -1022,7 +1024,9 @@ fn parse_tc_rate_bps(v: &str) -> anyhow::Result<u64> {
|
||||
} else {
|
||||
anyhow::bail!("unknown tc rate unit: {v}")
|
||||
};
|
||||
let base: u64 = num.parse().map_err(|_| anyhow::anyhow!("invalid tc rate: {v}"))?;
|
||||
let base: u64 = num
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("invalid tc rate: {v}"))?;
|
||||
base.checked_mul(mul)
|
||||
.ok_or_else(|| anyhow::anyhow!("tc rate overflow: {v}"))
|
||||
}
|
||||
@@ -1085,15 +1089,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn detects_ingress_redirect_to_ifb_dev_format_lowercase() {
|
||||
let raw =
|
||||
"filter parent ffff: protocol all pref 1 u32\n action order 1: mirred egress redirect dev ifb0";
|
||||
let raw = "filter parent ffff: protocol all pref 1 u32\n action order 1: mirred egress redirect dev ifb0";
|
||||
assert!(has_ingress_redirect_to_ifb(raw, "ifb0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_match_redirect_to_other_ifb() {
|
||||
let raw =
|
||||
"filter parent ffff: protocol all pref 1 u32\n action order 1: mirred (Egress Redirect to device ifb1) stolen";
|
||||
let raw = "filter parent ffff: protocol all pref 1 u32\n action order 1: mirred (Egress Redirect to device ifb1) stolen";
|
||||
assert!(!has_ingress_redirect_to_ifb(raw, "ifb0"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user