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:
2026-05-11 17:30:15 +05:00
parent 819cc9c308
commit 7a4e9764b1
17 changed files with 842 additions and 957 deletions
-851
View File
@@ -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"
+63 -3
View File
@@ -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());
}
}
+24 -2
View File
@@ -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());
}
+82 -10
View File
@@ -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));
}
}
+33
View File
@@ -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
View File
@@ -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
View File
@@ -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(())
}
+8 -6
View File
@@ -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"));
}