working base
This commit is contained in:
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[target.aarch64-unknown-linux-gnu]
|
||||||
|
linker = "aarch64-linux-gnu-gcc"
|
||||||
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
target/
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
scripts/
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
ARCHITECTURE.md
|
||||||
|
TESTING.md
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
12
AGENTS.md
Normal file
12
AGENTS.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Use code comments only when absolutely needed. Less comments the better.
|
||||||
|
|
||||||
|
Documentation is contain in doc/ folder.
|
||||||
|
|
||||||
|
For building the code, use docker/podman. Never build directly on the machine.
|
||||||
|
|
||||||
|
Since the setup requires network manipulation, we need to execute docker-compose with sudo
|
||||||
|
|
||||||
|
When testing code to compile and if it works in basic way, use
|
||||||
|
`sudo podman-compose exec route-switcher-dev -- cargo build` and
|
||||||
|
`sudo podman-compose exec route-switcher-dev -- cargo run`
|
||||||
|
Do not add extra vars to those commands, RUST_LOG=debug is already configured.
|
||||||
910
Cargo.lock
generated
Normal file
910
Cargo.lock
generated
Normal file
@@ -0,0 +1,910 @@
|
|||||||
|
# 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 = "anstream"
|
||||||
|
version = "0.6.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||||
|
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 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"env_filter",
|
||||||
|
"jiff",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnetwork"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff"
|
||||||
|
version = "0.2.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-static",
|
||||||
|
"log",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-static"
|
||||||
|
version = "0.2.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.182"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "netlink-packet-core"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"byteorder",
|
||||||
|
"netlink-packet-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "netlink-packet-route"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0800eae8638a299eaa67476e1c6b6692922273e0f7939fd188fc861c837b9cd2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags",
|
||||||
|
"byteorder",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"netlink-packet-core",
|
||||||
|
"netlink-packet-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "netlink-packet-utils"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"byteorder",
|
||||||
|
"paste",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "netlink-sys"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "no-std-net"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pnet"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d"
|
||||||
|
dependencies = [
|
||||||
|
"ipnetwork",
|
||||||
|
"pnet_base",
|
||||||
|
"pnet_datalink",
|
||||||
|
"pnet_packet",
|
||||||
|
"pnet_sys",
|
||||||
|
"pnet_transport",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pnet_base"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7"
|
||||||
|
dependencies = [
|
||||||
|
"no-std-net",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pnet_datalink"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7"
|
||||||
|
dependencies = [
|
||||||
|
"ipnetwork",
|
||||||
|
"libc",
|
||||||
|
"pnet_base",
|
||||||
|
"pnet_sys",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pnet_macros"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pnet_macros_support"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab"
|
||||||
|
dependencies = [
|
||||||
|
"pnet_base",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pnet_packet"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"pnet_base",
|
||||||
|
"pnet_macros",
|
||||||
|
"pnet_macros_support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pnet_sys"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pnet_transport"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"pnet_base",
|
||||||
|
"pnet_packet",
|
||||||
|
"pnet_sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "route-switcher"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bytes",
|
||||||
|
"clap",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"env_logger",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"netlink-packet-core",
|
||||||
|
"netlink-packet-route",
|
||||||
|
"netlink-sys",
|
||||||
|
"pnet",
|
||||||
|
"pnet_sys",
|
||||||
|
"rand",
|
||||||
|
"signal-hook",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.115"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.49.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.2+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "windows-targets"
|
||||||
|
version = "0.53.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "route-switcher"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pnet = "0.35.0"
|
||||||
|
pnet_sys = "0.35.0"
|
||||||
|
rand = "0.9.1"
|
||||||
|
log = "0.4.27"
|
||||||
|
env_logger = "0.11.8"
|
||||||
|
libc = "0.2.172"
|
||||||
|
signal-hook = "0.3.18"
|
||||||
|
crossbeam-channel = "0.5.15"
|
||||||
|
netlink-packet-route = "0.23.0"
|
||||||
|
netlink-packet-core = "0.7.0"
|
||||||
|
netlink-sys = "0.8.7"
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
bytes = "1.10.1"
|
||||||
|
tokio = { version = "1.42", features = ["full"] }
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
FROM rust:1.93-alpine AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache musl-dev
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
|
# Create dummy main.rs to cache dependencies
|
||||||
|
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||||
|
RUN cargo build --release && rm -rf src
|
||||||
|
|
||||||
|
COPY src ./src
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk add --no-cache iputils iptables
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/target/release/route-switcher .
|
||||||
|
|
||||||
|
ENV RUST_LOG=info
|
||||||
|
ENV PRIMARY_INTERFACE=eth0
|
||||||
|
ENV SECONDARY_INTERFACE=eth1
|
||||||
|
ENV PRIMARY_GATEWAY=192.168.1.1
|
||||||
|
ENV SECONDARY_GATEWAY=192.168.2.1
|
||||||
|
ENV PING_TARGET=8.8.8.8
|
||||||
|
|
||||||
|
CMD ["./route-switcher"]
|
||||||
181
README.md
181
README.md
@@ -0,0 +1,181 @@
|
|||||||
|
# Route-Switcher
|
||||||
|
|
||||||
|
A Linux-based network failover system written in Rust that automatically switches routing between dual network interfaces based on connectivity monitoring.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Route-Switcher monitors connectivity to specified IP addresses via multiple network interfaces and automatically manages routing tables to ensure network redundancy. When the primary interface fails, it seamlessly switches to a secondary interface, and automatically fails back when the primary connection is restored.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **Async Pingers** (`src/pinger.rs`)
|
||||||
|
- Dual-interface ICMP monitoring
|
||||||
|
- Explicit interface binding (equivalent to `ping -I <interface>`)
|
||||||
|
- Configurable ping targets and intervals
|
||||||
|
- Async/await implementation with tokio
|
||||||
|
|
||||||
|
2. **Route Manager** (`src/routing.rs`)
|
||||||
|
- Netlink-based route manipulation
|
||||||
|
- No external dependencies on `ip` command
|
||||||
|
- Route addition and deletion
|
||||||
|
- Metric-based route prioritization
|
||||||
|
|
||||||
|
3. **State Machine** (`src/main.rs`)
|
||||||
|
- Failover logic with anti-flapping protection
|
||||||
|
- Three consecutive failures trigger failover
|
||||||
|
- One minute of stable connectivity triggers failback
|
||||||
|
- Prevents switching when both interfaces fail
|
||||||
|
|
||||||
|
4. **Configuration**
|
||||||
|
- Interface definitions (primary/secondary)
|
||||||
|
- Gateway configurations
|
||||||
|
- Ping targets and timing
|
||||||
|
- Route metrics
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Dual Interface Monitoring**: Simultaneous ping testing via both network interfaces
|
||||||
|
- **Automatic Failover**: Switches to secondary interface after 3 consecutive ping failures
|
||||||
|
- **Smart Failback**: Returns to primary interface after 1 minute of stable connectivity
|
||||||
|
- **Anti-Flapping**: Prevents frequent switching between interfaces
|
||||||
|
- **Edge Case Handling**: Won't switch if secondary interface is also down
|
||||||
|
- **Netlink Integration**: Direct kernel communication for route management
|
||||||
|
- **Async Architecture**: Non-blocking monitoring and management
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Linux operating system
|
||||||
|
- Rust 2024 edition
|
||||||
|
- Two network interfaces with internet connectivity
|
||||||
|
- Root privileges for route manipulation
|
||||||
|
- Netlink kernel support
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Network Setup Example
|
||||||
|
```bash
|
||||||
|
# Primary interface
|
||||||
|
eth0: 192.168.1.10/24, gateway 192.168.1.1
|
||||||
|
|
||||||
|
# Secondary interface
|
||||||
|
eth1: 192.168.2.10/24, gateway 192.168.2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Configuration
|
||||||
|
The application is configured through command-line arguments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cargo run -- \
|
||||||
|
--primary-interface eth0 \
|
||||||
|
--secondary-interface eth1 \
|
||||||
|
--primary-gateway 192.168.1.1 \
|
||||||
|
--secondary-gateway 192.168.2.1 \
|
||||||
|
--ping-target 8.8.8.8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
```bash
|
||||||
|
# Run with default settings
|
||||||
|
sudo cargo run
|
||||||
|
|
||||||
|
# Run with custom configuration
|
||||||
|
sudo cargo run -- \
|
||||||
|
--primary-interface eth0 \
|
||||||
|
--secondary-interface eth1 \
|
||||||
|
--primary-gateway 192.168.1.1 \
|
||||||
|
--secondary-gateway 192.168.2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Run with debug logging
|
||||||
|
RUST_LOG=debug sudo cargo run
|
||||||
|
|
||||||
|
# Run with custom log level
|
||||||
|
RUST_LOG=info sudo cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Environment
|
||||||
|
|
||||||
|
### Podman-Compose Setup
|
||||||
|
The project includes a complete testing environment using podman-compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start test environment
|
||||||
|
podman-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
podman-compose logs -f route-switcher
|
||||||
|
|
||||||
|
# Stop test environment
|
||||||
|
podman-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### End-to-End Testing
|
||||||
|
```bash
|
||||||
|
# Simulate primary interface failure
|
||||||
|
podman-compose exec primary ip link set eth0 down
|
||||||
|
|
||||||
|
# Observe failover in logs
|
||||||
|
podman-compose logs -f route-switcher
|
||||||
|
|
||||||
|
# Restore primary interface
|
||||||
|
podman-compose exec primary ip link set eth0 up
|
||||||
|
|
||||||
|
# Observe failback after 1 minute
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### State Machine
|
||||||
|
```
|
||||||
|
[Boot] -> [Primary] (after initial connectivity check)
|
||||||
|
[Primary] -> [Fallback] (after 3 consecutive failures)
|
||||||
|
[Fallback] -> [Primary] (after 60 seconds of stability)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route Management
|
||||||
|
- Primary route: `ip r add default via <primary-gw> dev <primary-iface> metric 10`
|
||||||
|
- Secondary route: `ip r add default via <secondary-gw> dev <secondary-iface> metric 20`
|
||||||
|
- Routes are managed via netlink, not external commands
|
||||||
|
|
||||||
|
### Failover Logic
|
||||||
|
1. **Detection**: 3 consecutive ping failures on primary interface
|
||||||
|
2. **Verification**: Secondary interface must be responsive
|
||||||
|
3. **Switch**: Update routing table to use secondary gateway
|
||||||
|
4. **Monitor**: Continue monitoring both interfaces
|
||||||
|
5. **Recovery**: After 60 seconds of stable primary connectivity, switch back
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Graceful degradation on interface failures
|
||||||
|
- Comprehensive logging for debugging
|
||||||
|
- Signal handling for clean shutdown
|
||||||
|
- Recovery from temporary network issues
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `tokio` - Async runtime
|
||||||
|
- `pnet` - Packet networking
|
||||||
|
- `netlink-sys` - Netlink kernel communication
|
||||||
|
- `anyhow` - Error handling
|
||||||
|
- `log` + `env_logger` - Logging
|
||||||
|
- `crossbeam-channel` - Inter-thread communication
|
||||||
|
- `signal-hook` - Signal handling
|
||||||
|
|
||||||
|
## Development Phases
|
||||||
|
|
||||||
|
- [ ] End-to-end automated tests
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPLv3
|
||||||
167
doc/ARCHITECTURE.md
Normal file
167
doc/ARCHITECTURE.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Architecture Documentation
|
||||||
|
|
||||||
|
## System Overview
|
||||||
|
|
||||||
|
Route-Switcher is a network failover system that operates at the application layer to provide automatic network redundancy. The system monitors network connectivity through multiple interfaces and manages routing tables to ensure continuous connectivity.
|
||||||
|
|
||||||
|
## Component Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
|
│ Main Thread │ │ Async Pingers │ │ Route Manager │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ • State Machine │◄──►│ • Interface A │◄──►│ • Netlink API │
|
||||||
|
│ • Decision Logic│ │ • Interface B │ │ • Route Add/Del │
|
||||||
|
│ • Coordination │ │ • ICMP Monitoring│ │ • Metric Mgmt │
|
||||||
|
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||||
|
│ │ │
|
||||||
|
└───────────────────────┼───────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Linux Kernel │
|
||||||
|
│ │
|
||||||
|
│ • Routing Table │
|
||||||
|
│ • Network Stack │
|
||||||
|
│ • Netlink Socket │
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. **Monitoring Phase**
|
||||||
|
- Async pingers send ICMP packets via both interfaces
|
||||||
|
- Results are collected and sent to main thread
|
||||||
|
- State machine evaluates connectivity patterns
|
||||||
|
|
||||||
|
2. **Decision Phase**
|
||||||
|
- State machine determines if failover is needed
|
||||||
|
- Verifies secondary interface health
|
||||||
|
- Triggers route changes if conditions are met
|
||||||
|
|
||||||
|
3. **Action Phase**
|
||||||
|
- Route manager updates kernel routing table
|
||||||
|
- Changes are applied via netlink interface
|
||||||
|
- System continues monitoring in new state
|
||||||
|
|
||||||
|
## State Machine Design
|
||||||
|
|
||||||
|
### States
|
||||||
|
- **Boot**: Initial state, gathering connectivity data
|
||||||
|
- **Primary**: Using primary interface for routing
|
||||||
|
- **Fallback**: Using secondary interface for routing
|
||||||
|
|
||||||
|
### Transitions
|
||||||
|
```
|
||||||
|
Boot → Primary: After 10 seconds of sampling (regardless of ping results)
|
||||||
|
Primary → Fallback: After 3 consecutive failures AND secondary is healthy
|
||||||
|
Fallback → Primary: After 60 seconds of stable primary connectivity
|
||||||
|
```
|
||||||
|
|
||||||
|
### Routing Behavior
|
||||||
|
- **Boot State**: Both routes are set up initially - primary (metric 10) and secondary (metric 20)
|
||||||
|
- **Primary State**: Primary route (metric 10) and secondary route (metric 20) present
|
||||||
|
- **Fallback State**: All three routes present - primary (metric 10), secondary (metric 20), and failover secondary (metric 5)
|
||||||
|
- **Exit**: Only the failover route (metric 5) is removed
|
||||||
|
|
||||||
|
### Route Management Strategy
|
||||||
|
The system follows a "both routes always present, extra failover on-demand" approach:
|
||||||
|
1. **Initialization**: Set up primary route (metric 10) and secondary route (metric 20)
|
||||||
|
2. **Boot Phase**: Collect 10 seconds of ping samples to establish baseline connectivity
|
||||||
|
3. **Normal Operation**: Primary route serves traffic (metric 10), secondary available as backup (metric 20)
|
||||||
|
4. **Failover**: Add extra secondary route with highest priority (metric 5) for immediate failover
|
||||||
|
5. **Failback**: Remove extra failover route when primary recovers
|
||||||
|
6. **Cleanup**: Only remove the extra failover route on exit, preserving base routes
|
||||||
|
|
||||||
|
### State Persistence
|
||||||
|
- Current state is maintained in memory
|
||||||
|
- State changes are logged for debugging
|
||||||
|
- No persistent storage required (state rebuilds on restart)
|
||||||
|
|
||||||
|
## Interface Design
|
||||||
|
|
||||||
|
### Pinger Interface
|
||||||
|
```rust
|
||||||
|
pub trait Pinger {
|
||||||
|
async fn ping(&self, target: Ipv4Addr, interface: &str) -> PingResult;
|
||||||
|
async fn start_monitoring(&self, targets: &[Ipv4Addr], interfaces: &[String]) -> Receiver<PingResult>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route Manager Interface
|
||||||
|
```rust
|
||||||
|
pub trait RouteManager {
|
||||||
|
fn add_default_route(&self, gateway: Ipv4Addr, interface: &str, metric: u32) -> Result<()>;
|
||||||
|
fn delete_default_route(&self, gateway: Ipv4Addr, interface: &str, metric: u32) -> Result<()>;
|
||||||
|
fn get_current_routes(&self) -> Result<Vec<RouteInfo>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Threading Model
|
||||||
|
|
||||||
|
### Main Thread
|
||||||
|
- Runs the state machine
|
||||||
|
- Handles signals and graceful shutdown
|
||||||
|
- Coordinates between components
|
||||||
|
|
||||||
|
### Async Pinger Tasks
|
||||||
|
- One task per interface
|
||||||
|
- Non-blocking ICMP operations
|
||||||
|
- Results sent via channels
|
||||||
|
|
||||||
|
### Route Manager
|
||||||
|
- Synchronous operations (netlink is sync)
|
||||||
|
- Called from main thread
|
||||||
|
- Thread-safe operations
|
||||||
|
|
||||||
|
## Error Handling Strategy
|
||||||
|
|
||||||
|
### Categories
|
||||||
|
1. **Network Errors**: Temporary connectivity issues
|
||||||
|
2. **System Errors**: Permission problems, interface not found
|
||||||
|
3. **Configuration Errors**: Invalid IP addresses, missing interfaces
|
||||||
|
|
||||||
|
### Recovery Mechanisms
|
||||||
|
- **Network Errors**: Retry with exponential backoff
|
||||||
|
- **System Errors**: Log and exit (requires admin intervention)
|
||||||
|
- **Configuration Errors**: Validate on startup, exit if invalid
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Privileges
|
||||||
|
- Requires root privileges for route manipulation
|
||||||
|
- Drops unnecessary privileges where possible
|
||||||
|
- Validates all user inputs
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
- Only sends ICMP packets to configured targets
|
||||||
|
- No arbitrary packet crafting
|
||||||
|
- Interface binding prevents traffic leakage
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
### Resource Usage
|
||||||
|
- **Memory**: Minimal (~10MB)
|
||||||
|
- **CPU**: Low (periodic ICMP packets)
|
||||||
|
- **Network**: Very low (only ping traffic)
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Single target machine design
|
||||||
|
- Supports multiple ping targets
|
||||||
|
- Limited to 2 interfaces (current design)
|
||||||
|
|
||||||
|
## Testing Architecture
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Individual component testing
|
||||||
|
- Mock network interfaces
|
||||||
|
- State machine logic verification
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Component interaction testing
|
||||||
|
- Real network interface usage
|
||||||
|
- Netlink operation verification
|
||||||
|
|
||||||
|
### End-to-End Tests
|
||||||
|
- Full system testing in containers
|
||||||
|
- Network failure simulation
|
||||||
|
- Failover timing verification
|
||||||
270
doc/TESTING.md
Normal file
270
doc/TESTING.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Testing Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the testing strategy and environment for the Route-Switcher project.
|
||||||
|
|
||||||
|
## Testing Environment
|
||||||
|
|
||||||
|
### Podman-Compose Setup
|
||||||
|
|
||||||
|
The testing environment uses podman-compose to create a realistic network topology with routers and a single ICMP target:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Route-Switcher │ │ Primary Router│ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ eth0 ────────────┼────►│ eth0 ──────────┼────►│ ICMP Target │
|
||||||
|
│ eth1 ────────────┼────►│ eth1 ──────────┼────►│ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
primary-net secondary-net target-net
|
||||||
|
192.168.1.0/24 192.168.2.0/24 10.0.0.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Architecture
|
||||||
|
|
||||||
|
- **route-switcher**: Dual interfaces (eth0→primary-net, eth1→secondary-net)
|
||||||
|
- **primary-router**: Connects primary-net ↔ target-net (192.168.1.1 ↔ 10.0.0.1)
|
||||||
|
- **secondary-router**: Connects secondary-net ↔ target-net (192.168.2.1 ↔ 10.0.0.2)
|
||||||
|
- **icmp-target**: Single IP on target-net (10.0.0.100), reachable via either router
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the testing environment
|
||||||
|
podman-compose up -d
|
||||||
|
|
||||||
|
# Run automated failover test
|
||||||
|
./scripts/test-failover.sh
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
podman-compose logs -f route-switcher
|
||||||
|
|
||||||
|
# Stop environment
|
||||||
|
podman-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
|
||||||
|
**Route-Switcher:**
|
||||||
|
- eth0: 192.168.1.10 (primary network)
|
||||||
|
- eth1: 192.168.2.10 (secondary network)
|
||||||
|
- Default gateway: 192.168.1.1 (primary router)
|
||||||
|
|
||||||
|
**Primary Router:**
|
||||||
|
- eth0: 192.168.1.1 (primary network)
|
||||||
|
- eth1: 10.0.0.1 (target network)
|
||||||
|
- Routes traffic between networks with NAT
|
||||||
|
|
||||||
|
**Secondary Router:**
|
||||||
|
- eth0: 192.168.2.1 (secondary network)
|
||||||
|
- eth1: 10.0.0.2 (target network)
|
||||||
|
- Routes traffic between networks with NAT
|
||||||
|
|
||||||
|
**ICMP Target:**
|
||||||
|
- Single IP: 10.0.0.100
|
||||||
|
- Default route: 10.0.0.1 (primary router)
|
||||||
|
- Responds to ping from both routers
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
### 1. Basic Connectivity Test
|
||||||
|
**Objective**: Verify basic ping functionality on both interfaces
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start environment
|
||||||
|
podman-compose up -d
|
||||||
|
|
||||||
|
# Test primary connectivity
|
||||||
|
podman-compose exec route-switcher ping -c 3 -I eth0 10.0.0.100
|
||||||
|
|
||||||
|
# Test secondary connectivity
|
||||||
|
podman-compose exec route-switcher ping -c 3 -I eth1 10.0.0.100
|
||||||
|
|
||||||
|
# Check routing table
|
||||||
|
podman-compose exec route-switcher ip route show
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Failover Test
|
||||||
|
**Objective**: Verify automatic failover when primary router fails
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start monitoring logs
|
||||||
|
podman-compose logs -f route-switcher &
|
||||||
|
|
||||||
|
# Simulate primary router failure
|
||||||
|
podman-compose exec primary-router ip link set eth0 down
|
||||||
|
|
||||||
|
# Verify failover occurs (should see in logs)
|
||||||
|
# Wait for state change to Fallback
|
||||||
|
|
||||||
|
# Check routing table after failover
|
||||||
|
podman-compose exec route-switcher ip route show
|
||||||
|
|
||||||
|
# Test connectivity via secondary router
|
||||||
|
podman-compose exec route-switcher ping -c 3 10.0.0.100
|
||||||
|
|
||||||
|
# Restore primary router
|
||||||
|
podman-compose exec primary-router ip link set eth0 up
|
||||||
|
|
||||||
|
# Verify failback after 60 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Dual Failure Test
|
||||||
|
**Objective**: Verify system doesn't failover when both routers fail
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start monitoring logs
|
||||||
|
podman-compose logs -f route-switcher &
|
||||||
|
|
||||||
|
# Fail both routers
|
||||||
|
podman-compose exec primary-router ip link set eth0 down
|
||||||
|
podman-compose exec secondary-router ip link set eth0 down
|
||||||
|
|
||||||
|
# Verify no routing changes occur
|
||||||
|
# System should remain in current state
|
||||||
|
|
||||||
|
# Restore routers
|
||||||
|
podman-compose exec primary-router ip link set eth0 up
|
||||||
|
podman-compose exec secondary-router ip link set eth0 up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Router Target Interface Failure
|
||||||
|
**Objective**: Test upstream network failure simulation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fail primary router's connection to target network
|
||||||
|
podman-compose exec primary-router ip link set eth1 down
|
||||||
|
|
||||||
|
# Should trigger failover to secondary router
|
||||||
|
# Verify connectivity still works via secondary path
|
||||||
|
|
||||||
|
# Restore primary router's target connection
|
||||||
|
podman-compose exec primary-router ip link set eth1 up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Automated Failover Test
|
||||||
|
**Objective**: Run complete automated test sequence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the comprehensive test script
|
||||||
|
./scripts/test-failover.sh
|
||||||
|
|
||||||
|
# This script will:
|
||||||
|
# 1. Start the environment
|
||||||
|
# 2. Verify initial connectivity
|
||||||
|
# 3. Simulate primary router failure
|
||||||
|
# 4. Monitor failover
|
||||||
|
# 5. Restore primary router
|
||||||
|
# 6. Verify failback after 60 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unit Tests
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Run specific test module
|
||||||
|
cargo test pinger
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
cargo tarpaulin --out Html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── unit/
|
||||||
|
│ ├── pinger_tests.rs
|
||||||
|
│ ├── routing_tests.rs
|
||||||
|
│ └── state_machine_tests.rs
|
||||||
|
├── integration/
|
||||||
|
│ ├── netlink_tests.rs
|
||||||
|
│ └── dual_interface_tests.rs
|
||||||
|
└── e2e/
|
||||||
|
└── failover_tests.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Testing
|
||||||
|
|
||||||
|
### Load Testing
|
||||||
|
```bash
|
||||||
|
# Test with multiple ping targets
|
||||||
|
cargo run -- --ping-target 8.8.8.8
|
||||||
|
|
||||||
|
# Monitor resource usage
|
||||||
|
podman stats route-switcher
|
||||||
|
|
||||||
|
# Test long-running stability
|
||||||
|
# Run for 24 hours and monitor for memory leaks
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Latency Testing
|
||||||
|
```bash
|
||||||
|
# Measure failover time
|
||||||
|
# Start script to time the state transition
|
||||||
|
start_time=$(date +%s%N)
|
||||||
|
# Trigger failure
|
||||||
|
# Wait for state change
|
||||||
|
end_time=$(date +%s%N)
|
||||||
|
failover_time=$((($end_time - $start_time) / 1000000))
|
||||||
|
echo "Failover time: ${failover_time}ms"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Tests
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
1. **Permission Denied**: Ensure containers run with privileged mode
|
||||||
|
2. **Interface Not Found**: Check network configuration in compose file
|
||||||
|
3. **Netlink Errors**: Verify kernel supports required operations
|
||||||
|
4. **Timing Issues**: Adjust test timeouts for your environment
|
||||||
|
|
||||||
|
### Debug Commands
|
||||||
|
```bash
|
||||||
|
# Check container network interfaces
|
||||||
|
podman-compose exec route-switcher ip addr show
|
||||||
|
|
||||||
|
# Check routing table
|
||||||
|
podman-compose exec route-switcher ip route show
|
||||||
|
|
||||||
|
# Monitor network traffic
|
||||||
|
podman-compose exec route-switcher tcpdump -i any icmp
|
||||||
|
|
||||||
|
# Check system logs
|
||||||
|
podman-compose exec route-switcher dmesg | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Data
|
||||||
|
|
||||||
|
### Sample Ping Results
|
||||||
|
```rust
|
||||||
|
// Mock data for testing
|
||||||
|
let mock_ping_results = vec![
|
||||||
|
PingResult::Ok, // Normal operation
|
||||||
|
PingResult::Failed, // Single failure
|
||||||
|
PingResult::Failed, // Consecutive failure
|
||||||
|
PingResult::Failed, // Trigger failover
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
```bash
|
||||||
|
# Test network setup
|
||||||
|
ip addr add 192.168.1.10/24 dev eth0
|
||||||
|
ip addr add 192.168.2.10/24 dev eth1
|
||||||
|
ip route add default via 192.168.1.1 dev eth0 metric 10
|
||||||
|
ip route add default via 192.168.2.1 dev eth1 metric 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Coverage Goals
|
||||||
|
|
||||||
|
- **Unit Tests**: 90%+ code coverage
|
||||||
|
- **Integration Tests**: All major component interactions
|
||||||
|
- **E2E Tests**: All user scenarios and edge cases
|
||||||
|
- **Performance Tests**: Resource usage and timing validation
|
||||||
149
docker-compose.yml
Normal file
149
docker-compose.yml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
route-switcher:
|
||||||
|
build: .
|
||||||
|
privileged: true
|
||||||
|
depends_on:
|
||||||
|
- primary-router
|
||||||
|
- secondary-router
|
||||||
|
- icmp-target
|
||||||
|
networks:
|
||||||
|
primary-net:
|
||||||
|
ipv4_address: 192.168.200.10
|
||||||
|
secondary-net:
|
||||||
|
ipv4_address: 192.168.201.10
|
||||||
|
environment:
|
||||||
|
- RUST_LOG=debug
|
||||||
|
- PRIMARY_INTERFACE=eth0
|
||||||
|
- SECONDARY_INTERFACE=eth1
|
||||||
|
- PRIMARY_GATEWAY=192.168.200.11
|
||||||
|
- SECONDARY_GATEWAY=192.168.201.11
|
||||||
|
- PING_TARGET=192.168.202.100
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_ADMIN
|
||||||
|
volumes:
|
||||||
|
- ./scripts:/scripts:ro
|
||||||
|
|
||||||
|
route-switcher-dev:
|
||||||
|
image: rust:1.93-bullseye
|
||||||
|
privileged: true
|
||||||
|
working_dir: /app
|
||||||
|
command: |
|
||||||
|
sh -c "
|
||||||
|
echo nameserver 192.168.10.1 > /etc/resolv.conf &&
|
||||||
|
/bin/sleep infinity
|
||||||
|
"
|
||||||
|
networks:
|
||||||
|
primary-net:
|
||||||
|
ipv4_address: 192.168.200.20
|
||||||
|
secondary-net:
|
||||||
|
ipv4_address: 192.168.201.20
|
||||||
|
default:
|
||||||
|
dns:
|
||||||
|
- 192.168.10.1
|
||||||
|
environment:
|
||||||
|
- RUST_LOG=debug
|
||||||
|
- PRIMARY_INTERFACE=eth0
|
||||||
|
- SECONDARY_INTERFACE=eth1
|
||||||
|
- PRIMARY_GATEWAY=192.168.200.11
|
||||||
|
- SECONDARY_GATEWAY=192.168.201.11
|
||||||
|
- PING_TARGET=192.168.202.100
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_ADMIN
|
||||||
|
volumes:
|
||||||
|
- .:/app:rw
|
||||||
|
- cargo-cache:/usr/local/cargo/registry
|
||||||
|
|
||||||
|
primary-router:
|
||||||
|
image: alpine:latest
|
||||||
|
privileged: true
|
||||||
|
networks:
|
||||||
|
primary-net:
|
||||||
|
ipv4_address: 192.168.200.11
|
||||||
|
target-net:
|
||||||
|
ipv4_address: 192.168.202.11
|
||||||
|
default:
|
||||||
|
dns:
|
||||||
|
- 192.168.10.1
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_ADMIN
|
||||||
|
command: sh /scripts/setup-router.sh primary
|
||||||
|
volumes:
|
||||||
|
- ./scripts:/scripts:ro
|
||||||
|
|
||||||
|
secondary-router:
|
||||||
|
image: alpine:latest
|
||||||
|
privileged: true
|
||||||
|
networks:
|
||||||
|
secondary-net:
|
||||||
|
ipv4_address: 192.168.201.11
|
||||||
|
target-net:
|
||||||
|
ipv4_address: 192.168.202.12
|
||||||
|
default:
|
||||||
|
dns:
|
||||||
|
- 192.168.10.1
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_ADMIN
|
||||||
|
command: sh /scripts/setup-router.sh secondary
|
||||||
|
volumes:
|
||||||
|
- ./scripts:/scripts:ro
|
||||||
|
|
||||||
|
icmp-target:
|
||||||
|
image: alpine:latest
|
||||||
|
networks:
|
||||||
|
target-net:
|
||||||
|
ipv4_address: 192.168.202.100
|
||||||
|
default:
|
||||||
|
dns:
|
||||||
|
- 192.168.10.1
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
command: |
|
||||||
|
sh -c "
|
||||||
|
echo 'ICMP Target configured at 192.168.202.100' &&
|
||||||
|
echo 'Routes:' &&
|
||||||
|
ip route show &&
|
||||||
|
while true; do
|
||||||
|
echo $(date): ICMP Target alive
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
|
"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "ping", "-c", "1", "192.168.202.11"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
primary-net:
|
||||||
|
driver: bridge
|
||||||
|
internal: true
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 192.168.200.0/24
|
||||||
|
|
||||||
|
secondary-net:
|
||||||
|
driver: bridge
|
||||||
|
internal: true
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 192.168.201.0/24
|
||||||
|
|
||||||
|
target-net:
|
||||||
|
driver: bridge
|
||||||
|
internal: true
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 192.168.202.0/24
|
||||||
|
|
||||||
|
default:
|
||||||
|
external: true
|
||||||
|
name: podman
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cargo-cache:
|
||||||
44
route-switcher.service
Normal file
44
route-switcher.service
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Route-Switcher - Automatic network failover service
|
||||||
|
Documentation=https://git.hudrydum.cz/michal.humpula/route-switcher
|
||||||
|
After=network.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
ConditionPathExists=/usr/local/sbin/route-switcher
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/sbin/route-switcher
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
# Environment variables (customize these for your network)
|
||||||
|
Environment=RUST_LOG=info
|
||||||
|
Environment=PRIMARY_INTERFACE=eth0
|
||||||
|
Environment=SECONDARY_INTERFACE=eth1
|
||||||
|
Environment=PRIMARY_GATEWAY=192.168.1.1
|
||||||
|
Environment=SECONDARY_GATEWAY=192.168.2.1
|
||||||
|
Environment=PING_TARGET=8.8.8.8
|
||||||
|
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
|
||||||
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
LimitNOFILE=65536
|
||||||
|
LimitNPROC=4096
|
||||||
|
|
||||||
|
# Process isolation
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/log
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
53
scripts/setup-router.sh
Executable file
53
scripts/setup-router.sh
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ROUTER_TYPE="$1"
|
||||||
|
|
||||||
|
echo "Setting up $ROUTER_TYPE router..."
|
||||||
|
|
||||||
|
# fix dns
|
||||||
|
echo "nameserver 192.168.10.1" > /etc/resolv.conf
|
||||||
|
|
||||||
|
apk add --no-cache iputils iptables
|
||||||
|
|
||||||
|
# Enable IP forwarding
|
||||||
|
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||||
|
sysctl -w net.ipv4.ip_forward=1
|
||||||
|
|
||||||
|
if [ "$ROUTER_TYPE" = "primary" ]; then
|
||||||
|
echo "Configuring PRIMARY router (192.168.200.11 192.168.202.11 172.17.0.2)"
|
||||||
|
|
||||||
|
ip addr show
|
||||||
|
echo "Routes:"
|
||||||
|
ip route show
|
||||||
|
|
||||||
|
# NAT for traffic from primary network to target network
|
||||||
|
iptables -t nat -A POSTROUTING -s 192.168.200.0/24 -d 192.168.202.0/24 -j MASQUERADE
|
||||||
|
iptables -P FORWARD ACCEPT
|
||||||
|
|
||||||
|
elif [ "$ROUTER_TYPE" = "secondary" ]; then
|
||||||
|
echo "Configuring SECONDARY router (192.168.201.11 ↔ 192.168.202.12 ↔ 172.17.0.3)"
|
||||||
|
|
||||||
|
ip addr show
|
||||||
|
echo "Routes:"
|
||||||
|
ip route show
|
||||||
|
|
||||||
|
# NAT for traffic from secondary network to target network
|
||||||
|
iptables -t nat -A POSTROUTING -s 192.168.201.0/24 -d 192.168.202.0/24 -j MASQUERADE
|
||||||
|
iptables -P FORWARD ACCEPT
|
||||||
|
else
|
||||||
|
echo "Error: Invalid router type. Use 'primary' or 'secondary'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Secondary router setup complete"
|
||||||
|
echo "NAT rules:"
|
||||||
|
iptables -t nat -L POSTROUTING -n -v
|
||||||
|
|
||||||
|
# Keep container running
|
||||||
|
echo "Router is running. Monitoring interfaces..."
|
||||||
|
while true; do
|
||||||
|
echo "$(date): Router $ROUTER_TYPE status - interfaces up"
|
||||||
|
sleep 60
|
||||||
|
done
|
||||||
118
scripts/test-failover.sh
Executable file
118
scripts/test-failover.sh
Executable file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Route-Switcher Failover Test Script ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if podman-compose is available
|
||||||
|
if ! command -v podman-compose &> /dev/null; then
|
||||||
|
print_error "podman-compose is not installed or not in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the environment
|
||||||
|
print_status "Starting test environment..."
|
||||||
|
podman-compose up -d
|
||||||
|
|
||||||
|
# Wait for containers to be ready
|
||||||
|
print_status "Waiting for containers to initialize..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Check if all containers are running
|
||||||
|
print_status "Checking container status..."
|
||||||
|
podman-compose ps
|
||||||
|
|
||||||
|
# Set up initial default route for route-switcher (testing scenario only)
|
||||||
|
# In production, this would be configured by the network admin
|
||||||
|
print_status "Setting up initial default route via primary router..."
|
||||||
|
podman-compose exec route-switcher ip route add default via 192.168.200.11 dev eth0
|
||||||
|
|
||||||
|
# Verify network connectivity
|
||||||
|
print_status "Testing initial connectivity..."
|
||||||
|
|
||||||
|
# Test from route-switcher to target (should use default route via primary)
|
||||||
|
print_status "Testing connectivity via primary router..."
|
||||||
|
podman-compose exec route-switcher ping -c 3 192.168.202.100 || {
|
||||||
|
print_error "Primary connectivity test failed"
|
||||||
|
podman-compose down
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test specific interface connectivity
|
||||||
|
print_status "Testing connectivity via secondary interface..."
|
||||||
|
podman-compose exec route-switcher ping -c 3 -I eth1 192.168.202.100 || {
|
||||||
|
print_warning "Secondary interface connectivity test failed (might be expected initially)"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_status "Initial connectivity tests passed!"
|
||||||
|
|
||||||
|
# Show current routing table
|
||||||
|
print_status "Current routing table in route-switcher:"
|
||||||
|
podman-compose exec route-switcher ip route show
|
||||||
|
|
||||||
|
echo
|
||||||
|
print_warning "=== Starting Failover Test ==="
|
||||||
|
print_status "Monitoring route-switcher logs (press Ctrl+C to stop monitoring)..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Start monitoring logs in background
|
||||||
|
podman-compose logs -f route-switcher &
|
||||||
|
LOGS_PID=$!
|
||||||
|
|
||||||
|
# Wait a bit for initial logs
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
print_status "Simulating primary router failure by shutting down eth0..."
|
||||||
|
podman-compose exec primary-router ip link set eth0 down
|
||||||
|
|
||||||
|
print_status "Waiting for failover to occur..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Check if failover happened
|
||||||
|
print_status "Checking routing table after primary failure..."
|
||||||
|
podman-compose exec route-switcher ip route show
|
||||||
|
|
||||||
|
print_status "Testing connectivity after failover..."
|
||||||
|
podman-compose exec route-switcher ping -c 3 192.168.202.100 || {
|
||||||
|
print_warning "Connectivity test after failover failed (this might be expected during transition)"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_status "Restoring primary router..."
|
||||||
|
podman-compose exec primary-router ip link set eth0 up
|
||||||
|
|
||||||
|
print_status "Waiting for failback (should take ~60 seconds of stable connection)..."
|
||||||
|
sleep 70
|
||||||
|
|
||||||
|
print_status "Final routing table check:"
|
||||||
|
podman-compose exec route-switcher ip route show
|
||||||
|
|
||||||
|
print_status "Final connectivity test:"
|
||||||
|
podman-compose exec route-switcher ping -c 3 192.168.202.100
|
||||||
|
|
||||||
|
# Stop monitoring logs
|
||||||
|
kill $LOGS_PID 2>/dev/null || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
print_status "=== Test Complete ==="
|
||||||
|
print_status "To stop the environment: podman-compose down"
|
||||||
|
print_status "To view logs: podman-compose logs route-switcher"
|
||||||
35
scripts/verify-setup.sh
Executable file
35
scripts/verify-setup.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Quick Setup Verification ==="
|
||||||
|
|
||||||
|
# Start containers
|
||||||
|
echo "Starting containers..."
|
||||||
|
podman-compose up -d
|
||||||
|
|
||||||
|
# Wait for initialization
|
||||||
|
echo "Waiting for containers..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
echo "Container status:"
|
||||||
|
podman-compose ps
|
||||||
|
|
||||||
|
# Set up initial route
|
||||||
|
echo "Setting up initial default route..."
|
||||||
|
podman-compose exec route-switcher ip route add default via 192.168.200.11 dev eth0
|
||||||
|
|
||||||
|
# Show routing table
|
||||||
|
echo "Route-switcher routing table:"
|
||||||
|
podman-compose exec route-switcher ip route show
|
||||||
|
|
||||||
|
# Test basic connectivity
|
||||||
|
echo "Testing connectivity to target..."
|
||||||
|
if podman-compose exec route-switcher ping -c 2 192.168.202.100; then
|
||||||
|
echo "✓ Connectivity test passed"
|
||||||
|
else
|
||||||
|
echo "✗ Connectivity test failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Setup complete. Use './scripts/test-failover.sh' for full failover testing."
|
||||||
226
src/main.rs
Normal file
226
src/main.rs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use env_logger::{Builder, Env};
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use signal_hook::consts::signal::*;
|
||||||
|
use signal_hook::flag as signal_flag;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
use std::panic;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::signal;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
mod pinger;
|
||||||
|
mod routing;
|
||||||
|
mod state_machine;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct Config {
|
||||||
|
/// Primary network interface
|
||||||
|
#[arg(long, default_value = "eth0")]
|
||||||
|
primary_interface: String,
|
||||||
|
|
||||||
|
/// Secondary network interface
|
||||||
|
#[arg(long, default_value = "eth1")]
|
||||||
|
secondary_interface: String,
|
||||||
|
|
||||||
|
/// Primary gateway IP
|
||||||
|
#[arg(long, default_value = "192.168.1.1")]
|
||||||
|
primary_gateway: String,
|
||||||
|
|
||||||
|
/// Secondary gateway IP
|
||||||
|
#[arg(long, default_value = "192.168.2.1")]
|
||||||
|
secondary_gateway: String,
|
||||||
|
|
||||||
|
/// Ping target IP
|
||||||
|
#[arg(long, default_value = "8.8.8.8")]
|
||||||
|
ping_target: String,
|
||||||
|
|
||||||
|
/// Ping interval in seconds
|
||||||
|
#[arg(long, default_value = "1")]
|
||||||
|
ping_interval: u64,
|
||||||
|
|
||||||
|
/// Failover threshold (consecutive failures)
|
||||||
|
#[arg(long, default_value = "3")]
|
||||||
|
failover_threshold: usize,
|
||||||
|
|
||||||
|
/// Failback delay in seconds
|
||||||
|
#[arg(long, default_value = "60")]
|
||||||
|
failback_delay: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let env = Env::default().filter_or("RUST_LOG", "info");
|
||||||
|
let mut builder = Builder::from_env(env);
|
||||||
|
builder.init();
|
||||||
|
info!("Starting Route-Switcher...");
|
||||||
|
|
||||||
|
let config = Config::parse();
|
||||||
|
|
||||||
|
// Override with environment variables if present
|
||||||
|
let primary_interface =
|
||||||
|
std::env::var("PRIMARY_INTERFACE").unwrap_or(config.primary_interface.clone());
|
||||||
|
let secondary_interface =
|
||||||
|
std::env::var("SECONDARY_INTERFACE").unwrap_or(config.secondary_interface.clone());
|
||||||
|
let primary_gateway =
|
||||||
|
std::env::var("PRIMARY_GATEWAY").unwrap_or(config.primary_gateway.clone());
|
||||||
|
let secondary_gateway =
|
||||||
|
std::env::var("SECONDARY_GATEWAY").unwrap_or(config.secondary_gateway.clone());
|
||||||
|
let ping_target = std::env::var("PING_TARGET").unwrap_or(config.ping_target.clone());
|
||||||
|
|
||||||
|
let mut config_with_env = config;
|
||||||
|
config_with_env.primary_interface = primary_interface;
|
||||||
|
config_with_env.secondary_interface = secondary_interface;
|
||||||
|
config_with_env.primary_gateway = primary_gateway;
|
||||||
|
config_with_env.secondary_gateway = secondary_gateway;
|
||||||
|
config_with_env.ping_target = ping_target;
|
||||||
|
|
||||||
|
debug!("Configuration: {:?}", config_with_env);
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
let primary_gateway: Ipv4Addr = config_with_env
|
||||||
|
.primary_gateway
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid primary gateway: {}", e))?;
|
||||||
|
let secondary_gateway: Ipv4Addr = config_with_env
|
||||||
|
.secondary_gateway
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid secondary gateway: {}", e))?;
|
||||||
|
let ping_target: Ipv4Addr = config_with_env
|
||||||
|
.ping_target
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid ping target: {}", e))?;
|
||||||
|
|
||||||
|
// Set up signal handling
|
||||||
|
let term = Arc::new(AtomicBool::new(false));
|
||||||
|
signal_flag::register(SIGINT, Arc::clone(&term))?;
|
||||||
|
signal_flag::register(SIGTERM, Arc::clone(&term))?;
|
||||||
|
signal_flag::register(SIGQUIT, Arc::clone(&term))?;
|
||||||
|
|
||||||
|
let (shutdown_tx, _) = broadcast::channel::<()>(1);
|
||||||
|
|
||||||
|
// Run the main service
|
||||||
|
let result = main_service(
|
||||||
|
config_with_env,
|
||||||
|
primary_gateway,
|
||||||
|
secondary_gateway,
|
||||||
|
ping_target,
|
||||||
|
term,
|
||||||
|
shutdown_tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
error!("Service error: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Route-Switcher shut down gracefully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
use state_machine::StateMachine;
|
||||||
|
|
||||||
|
async fn main_service(
|
||||||
|
config: Config,
|
||||||
|
primary_gateway: Ipv4Addr,
|
||||||
|
secondary_gateway: Ipv4Addr,
|
||||||
|
ping_target: Ipv4Addr,
|
||||||
|
term: Arc<AtomicBool>,
|
||||||
|
shutdown_tx: broadcast::Sender<()>,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!(
|
||||||
|
"Starting main service with interfaces: {} (primary), {} (secondary)",
|
||||||
|
config.primary_interface, config.secondary_interface
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize route manager and set up initial routes
|
||||||
|
let mut route_manager = routing::RouteManager::new();
|
||||||
|
route_manager.setup_initial_routes(
|
||||||
|
primary_gateway,
|
||||||
|
config.primary_interface.clone(),
|
||||||
|
secondary_gateway,
|
||||||
|
config.secondary_interface.clone(),
|
||||||
|
)?;
|
||||||
|
info!("Initial routes configured: primary (metric 10), secondary (metric 20)");
|
||||||
|
|
||||||
|
// Create pingers
|
||||||
|
let primary_pinger = pinger::AsyncPinger::new(config.primary_interface.clone());
|
||||||
|
let secondary_pinger = pinger::AsyncPinger::new(config.secondary_interface.clone());
|
||||||
|
|
||||||
|
// Start monitoring
|
||||||
|
let ping_interval = Duration::from_secs(config.ping_interval);
|
||||||
|
let primary_shutdown = shutdown_tx.subscribe();
|
||||||
|
let secondary_shutdown = shutdown_tx.subscribe();
|
||||||
|
|
||||||
|
let mut primary_rx = primary_pinger
|
||||||
|
.start_monitoring(ping_target, ping_interval, primary_shutdown)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut secondary_rx = secondary_pinger
|
||||||
|
.start_monitoring(ping_target, ping_interval, secondary_shutdown)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Initialize state machine
|
||||||
|
let mut state_machine = StateMachine::new(
|
||||||
|
config.failover_threshold,
|
||||||
|
Duration::from_secs(config.failback_delay),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Main event loop
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// Handle primary ping results
|
||||||
|
Some(result) = primary_rx.recv() => {
|
||||||
|
debug!("Primary ping result: {}", result);
|
||||||
|
state_machine.add_primary_result(result);
|
||||||
|
|
||||||
|
if let Some((old_state, new_state)) = state_machine.update_state() {
|
||||||
|
state_machine::handle_state_change(new_state, old_state, &mut route_manager, &primary_gateway, &secondary_gateway, &config)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle secondary ping results
|
||||||
|
Some(result) = secondary_rx.recv() => {
|
||||||
|
debug!("Secondary ping result: {}", result);
|
||||||
|
state_machine.add_secondary_result(result);
|
||||||
|
|
||||||
|
if let Some((old_state, new_state)) = state_machine.update_state() {
|
||||||
|
state_machine::handle_state_change(new_state, old_state, &mut route_manager, &primary_gateway, &secondary_gateway, &config)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle shutdown signal
|
||||||
|
_ = signal::ctrl_c() => {
|
||||||
|
info!("Received Ctrl+C, shutting down...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check termination flag
|
||||||
|
_ = tokio::task::spawn_blocking({
|
||||||
|
let term = Arc::clone(&term);
|
||||||
|
move || {
|
||||||
|
while !term.load(Ordering::Relaxed) {
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
info!("Received termination signal");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up only the failover route on exit if we're in Fallback state
|
||||||
|
if state_machine.get_state() == &state_machine::State::Fallback {
|
||||||
|
route_manager
|
||||||
|
.remove_failover_route(secondary_gateway, config.secondary_interface.clone())?;
|
||||||
|
info!("Failover route cleared on exit");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
205
src/pinger.rs
Normal file
205
src/pinger.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use log;
|
||||||
|
use pnet::packet::Packet;
|
||||||
|
use pnet::packet::icmp::echo_reply::EchoReplyPacket;
|
||||||
|
use pnet::packet::icmp::{IcmpTypes, IcmpTypes::EchoReply, echo_request};
|
||||||
|
use pnet::transport::{
|
||||||
|
TransportChannelType::Layer4, TransportProtocol::Ipv4, icmp_packet_iter, transport_channel,
|
||||||
|
};
|
||||||
|
use pnet_sys;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
const ICMP_HEADER_SIZE: usize = 8;
|
||||||
|
const PAYLOAD: &str = "hello world!";
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum PingResult {
|
||||||
|
Ok,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for PingResult {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
PingResult::Ok => write!(f, "Ok"),
|
||||||
|
PingResult::Failed => write!(f, "Failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AsyncPinger {
|
||||||
|
interface: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncPinger {
|
||||||
|
pub fn new(interface: String) -> Self {
|
||||||
|
Self { interface }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn ping(&self, destination: Ipv4Addr) -> PingResult {
|
||||||
|
let (mut tx, mut rx) = match create_transport_channel() {
|
||||||
|
Ok((tx, rx)) => (tx, rx),
|
||||||
|
Err(_) => return PingResult::Failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fd = tx.socket.fd;
|
||||||
|
let it = self.interface.as_bytes();
|
||||||
|
|
||||||
|
let res = unsafe {
|
||||||
|
pnet_sys::setsockopt(
|
||||||
|
fd,
|
||||||
|
pnet_sys::SOL_SOCKET,
|
||||||
|
libc::SO_BINDTODEVICE,
|
||||||
|
(it.as_ptr() as *const libc::c_char) as pnet_sys::Buf,
|
||||||
|
it.len() as pnet_sys::SockLen,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if res == -1 {
|
||||||
|
log::error!("Failed to bind to {}", self.interface);
|
||||||
|
return PingResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
let identifier = rand::random::<u16>();
|
||||||
|
let sequence_number = rand::random::<u16>();
|
||||||
|
|
||||||
|
if let Err(_) = send_echo_request(&mut tx, destination, identifier, sequence_number) {
|
||||||
|
log::error!("Failed to send echo request");
|
||||||
|
return PingResult::Failed;
|
||||||
|
}
|
||||||
|
let r = receive_echo_reply(
|
||||||
|
&mut rx,
|
||||||
|
destination,
|
||||||
|
identifier,
|
||||||
|
sequence_number,
|
||||||
|
&self.interface,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_monitoring(
|
||||||
|
&self,
|
||||||
|
destination: Ipv4Addr,
|
||||||
|
interval: Duration,
|
||||||
|
mut shutdown: tokio::sync::broadcast::Receiver<()>,
|
||||||
|
) -> tokio::sync::mpsc::UnboundedReceiver<PingResult> {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
let interface = self.interface.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut interval_timer = tokio::time::interval(interval);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = interval_timer.tick() => {
|
||||||
|
let pinger = AsyncPinger::new(interface.clone());
|
||||||
|
let result = pinger.ping(destination).await;
|
||||||
|
if tx.send(result).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = shutdown.recv() => {
|
||||||
|
log::info!("Shutting down pinger for interface {}", interface);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_transport_channel() -> Result<(
|
||||||
|
pnet::transport::TransportSender,
|
||||||
|
pnet::transport::TransportReceiver,
|
||||||
|
)> {
|
||||||
|
transport_channel(
|
||||||
|
1024,
|
||||||
|
Layer4(Ipv4(pnet::packet::ip::IpNextHeaderProtocols::Icmp)),
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to create transport channel: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_echo_request(
|
||||||
|
tx: &mut pnet::transport::TransportSender,
|
||||||
|
destination: Ipv4Addr,
|
||||||
|
identifier: u16,
|
||||||
|
sequence_number: u16,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut buffer = [0u8; ICMP_HEADER_SIZE + 4 + PAYLOAD.len()];
|
||||||
|
let mut echo_request_packet = echo_request::MutableEchoRequestPacket::new(&mut buffer).unwrap();
|
||||||
|
echo_request_packet.set_icmp_type(IcmpTypes::EchoRequest);
|
||||||
|
echo_request_packet.set_identifier(identifier);
|
||||||
|
echo_request_packet.set_sequence_number(sequence_number);
|
||||||
|
echo_request_packet.set_payload(PAYLOAD.as_bytes());
|
||||||
|
echo_request_packet.set_checksum(pnet::util::checksum(echo_request_packet.packet(), 1));
|
||||||
|
|
||||||
|
tx.send_to(echo_request_packet, std::net::IpAddr::V4(destination))
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to send packet: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_echo_reply(
|
||||||
|
rx: &mut pnet::transport::TransportReceiver,
|
||||||
|
destination: Ipv4Addr,
|
||||||
|
identifier: u16,
|
||||||
|
sequence_number: u16,
|
||||||
|
interface: &str,
|
||||||
|
) -> PingResult {
|
||||||
|
let timeout = Duration::from_secs(3);
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut iter = icmp_packet_iter(rx);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match iter.next_with_timeout(Duration::from_secs(1)) {
|
||||||
|
Ok(None) => {
|
||||||
|
log::debug!(
|
||||||
|
"ICMP receive timeout for {} on interface {} (elapsed: {} ms)",
|
||||||
|
destination,
|
||||||
|
interface,
|
||||||
|
start.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Some((packet, addr)))
|
||||||
|
if addr == destination && packet.get_icmp_type() == EchoReply =>
|
||||||
|
{
|
||||||
|
if let Some(reply) = EchoReplyPacket::new(packet.packet()) {
|
||||||
|
if reply.get_identifier() == identifier
|
||||||
|
&& reply.get_sequence_number() == sequence_number
|
||||||
|
{
|
||||||
|
log::debug!(
|
||||||
|
"Received ICMP Echo Reply from {} on interface {} in {} ms",
|
||||||
|
addr,
|
||||||
|
interface,
|
||||||
|
start.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
return PingResult::Ok;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("Got something, but it looks like it is not mine!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some((_packet, _addr))) => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error receiving packet: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start.elapsed() >= timeout {
|
||||||
|
log::debug!(
|
||||||
|
"Request timed out for {} on interface {}",
|
||||||
|
destination,
|
||||||
|
interface
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PingResult::Failed
|
||||||
|
}
|
||||||
340
src/routing.rs
Normal file
340
src/routing.rs
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use libc::if_nametoindex;
|
||||||
|
use log::{debug, info};
|
||||||
|
use netlink_packet_route::route::RouteAddress;
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
const MAIN_TABLE_ID: u8 = 254;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RouteInfo {
|
||||||
|
pub gateway: Ipv4Addr,
|
||||||
|
pub interface: String,
|
||||||
|
pub metric: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RouteManager {
|
||||||
|
routes: Vec<RouteInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RouteManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
RouteManager { routes: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_route(&mut self, gateway: Ipv4Addr, interface: String, metric: u32) -> Result<()> {
|
||||||
|
let route_info = RouteInfo {
|
||||||
|
gateway,
|
||||||
|
interface: interface.clone(),
|
||||||
|
metric,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.add_default_route_internal(&route_info)?;
|
||||||
|
self.routes.push(route_info);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Added route: {} via {} dev {} metric {}",
|
||||||
|
Ipv4Addr::new(0, 0, 0, 0),
|
||||||
|
gateway,
|
||||||
|
interface,
|
||||||
|
metric
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_route(&mut self, gateway: Ipv4Addr, interface: &str, metric: u32) -> Result<()> {
|
||||||
|
self.delete_default_route_internal(gateway, interface, metric)?;
|
||||||
|
|
||||||
|
self.routes
|
||||||
|
.retain(|r| !(r.gateway == gateway && r.interface == interface && r.metric == metric));
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Removed route: {} via {} dev {} metric {}",
|
||||||
|
Ipv4Addr::new(0, 0, 0, 0),
|
||||||
|
gateway,
|
||||||
|
interface,
|
||||||
|
metric
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_primary_route(&mut self, gateway: Ipv4Addr, interface: String) -> Result<()> {
|
||||||
|
let primary_metric = 10;
|
||||||
|
|
||||||
|
// Remove existing routes for this interface if any
|
||||||
|
if let Some(pos) = self.routes.iter().position(|r| r.interface == interface) {
|
||||||
|
let existing_route = self.routes[pos].clone();
|
||||||
|
self.remove_route(
|
||||||
|
existing_route.gateway,
|
||||||
|
&existing_route.interface,
|
||||||
|
existing_route.metric,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add as primary route
|
||||||
|
self.add_route(gateway, interface, primary_metric)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_initial_routes(
|
||||||
|
&mut self,
|
||||||
|
primary_gateway: Ipv4Addr,
|
||||||
|
primary_interface: String,
|
||||||
|
secondary_gateway: Ipv4Addr,
|
||||||
|
secondary_interface: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Set primary route with metric 10 (default)
|
||||||
|
self.set_primary_route(primary_gateway, primary_interface)?;
|
||||||
|
|
||||||
|
// Set secondary route with metric 20 (lower priority)
|
||||||
|
let secondary_metric = 20;
|
||||||
|
self.add_route(secondary_gateway, secondary_interface, secondary_metric)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_failover_route(&mut self, gateway: Ipv4Addr, interface: String) -> Result<()> {
|
||||||
|
let failover_metric = 5; // Higher priority than both primary (10) and secondary (20)
|
||||||
|
self.add_route(gateway, interface, failover_metric)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_failover_route(&mut self, gateway: Ipv4Addr, interface: String) -> Result<()> {
|
||||||
|
let failover_metric = 5;
|
||||||
|
self.remove_route(gateway, &interface, failover_metric)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_routes(&self) -> &[RouteInfo] {
|
||||||
|
&self.routes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_default_route_internal(&self, route_info: &RouteInfo) -> Result<()> {
|
||||||
|
let index = get_interface_index(&route_info.interface)?;
|
||||||
|
|
||||||
|
if index == 0 {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Interface '{}' not found",
|
||||||
|
route_info.interface
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
use netlink_packet_core::{
|
||||||
|
NLM_F_ACK, NLM_F_CREATE, NLM_F_REQUEST, NetlinkHeader, NetlinkMessage, NetlinkPayload,
|
||||||
|
};
|
||||||
|
use netlink_packet_route::{
|
||||||
|
AddressFamily, RouteNetlinkMessage,
|
||||||
|
route::RouteProtocol,
|
||||||
|
route::RouteScope,
|
||||||
|
route::{RouteAttribute, RouteHeader, RouteMessage, RouteType},
|
||||||
|
};
|
||||||
|
use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE};
|
||||||
|
|
||||||
|
let mut socket = Socket::new(NETLINK_ROUTE)?;
|
||||||
|
let _port_number = socket.bind_auto()?.port_number();
|
||||||
|
socket.connect(&SocketAddr::new(0, 0))?;
|
||||||
|
let route_msg_hdr = RouteHeader {
|
||||||
|
address_family: AddressFamily::Inet,
|
||||||
|
table: MAIN_TABLE_ID,
|
||||||
|
destination_prefix_length: 0, // Default route
|
||||||
|
protocol: RouteProtocol::Boot,
|
||||||
|
scope: RouteScope::Universe,
|
||||||
|
kind: RouteType::Unicast,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut route_msg = RouteMessage::default();
|
||||||
|
route_msg.header = route_msg_hdr;
|
||||||
|
route_msg.attributes = vec![
|
||||||
|
RouteAttribute::Gateway(RouteAddress::Inet(route_info.gateway)),
|
||||||
|
RouteAttribute::Oif(index),
|
||||||
|
RouteAttribute::Priority(route_info.metric),
|
||||||
|
];
|
||||||
|
let mut nl_hdr = NetlinkHeader::default();
|
||||||
|
nl_hdr.flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK; // Remove NLM_F_EXCL to allow updates
|
||||||
|
|
||||||
|
let mut msg = NetlinkMessage::new(
|
||||||
|
nl_hdr,
|
||||||
|
NetlinkPayload::from(RouteNetlinkMessage::NewRoute(route_msg)),
|
||||||
|
);
|
||||||
|
|
||||||
|
msg.finalize();
|
||||||
|
let mut buf = vec![0; 1024 * 8];
|
||||||
|
|
||||||
|
msg.serialize(&mut buf[..msg.buffer_len()]);
|
||||||
|
|
||||||
|
// Debug: Log the netlink message being sent
|
||||||
|
debug!("Netlink message being sent: {:?}", &buf[..msg.buffer_len()]);
|
||||||
|
debug!(
|
||||||
|
"Route addition attempt: gateway={}, interface={}, metric={}, interface_index={}",
|
||||||
|
route_info.gateway, route_info.interface, route_info.metric, index
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.send(&buf, 0)?;
|
||||||
|
|
||||||
|
let mut receive_buffer = Vec::with_capacity(256);
|
||||||
|
let size = socket.recv(&mut receive_buffer, 0)?;
|
||||||
|
|
||||||
|
if size == 0 {
|
||||||
|
return Err(anyhow::anyhow!("No response from netlink"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = &receive_buffer[..size];
|
||||||
|
let rx_packet = <NetlinkMessage<RouteNetlinkMessage>>::deserialize(bytes);
|
||||||
|
|
||||||
|
match rx_packet {
|
||||||
|
Ok(rx_packet) => {
|
||||||
|
if let NetlinkPayload::Error(error_msg) = rx_packet.payload {
|
||||||
|
if let Some(code) = error_msg.code {
|
||||||
|
let error_code = code.get();
|
||||||
|
if error_code == -17 {
|
||||||
|
// EEXIST - Route already exists, treat as success
|
||||||
|
info!(
|
||||||
|
"Route already exists: {} via {} dev {} metric {}",
|
||||||
|
Ipv4Addr::new(0, 0, 0, 0),
|
||||||
|
route_info.gateway,
|
||||||
|
route_info.interface,
|
||||||
|
route_info.metric
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let error_str = match error_code {
|
||||||
|
-1 => "EPERM - Operation not permitted (need root privileges)",
|
||||||
|
-2 => "ENOENT - No such file or directory",
|
||||||
|
-13 => "EACCES - Permission denied",
|
||||||
|
-22 => "EINVAL - Invalid argument",
|
||||||
|
_ => "Unknown error",
|
||||||
|
};
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to add route: {} (code: {}): {:?}",
|
||||||
|
error_str,
|
||||||
|
error_code,
|
||||||
|
error_msg
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("Route added successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to deserialize netlink message: {}",
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_default_route_internal(
|
||||||
|
&self,
|
||||||
|
gateway: Ipv4Addr,
|
||||||
|
interface: &str,
|
||||||
|
metric: u32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let index = get_interface_index(interface)?;
|
||||||
|
|
||||||
|
if index == 0 {
|
||||||
|
return Err(anyhow::anyhow!("Interface '{}' not found", interface));
|
||||||
|
}
|
||||||
|
|
||||||
|
use netlink_packet_core::{
|
||||||
|
NLM_F_ACK, NLM_F_REQUEST, NetlinkHeader, NetlinkMessage, NetlinkPayload,
|
||||||
|
};
|
||||||
|
use netlink_packet_route::{
|
||||||
|
AddressFamily, RouteNetlinkMessage,
|
||||||
|
route::RouteProtocol,
|
||||||
|
route::RouteScope,
|
||||||
|
route::{RouteAttribute, RouteHeader, RouteMessage, RouteType},
|
||||||
|
};
|
||||||
|
use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE};
|
||||||
|
|
||||||
|
let mut socket = Socket::new(NETLINK_ROUTE)?;
|
||||||
|
let _port_number = socket.bind_auto()?.port_number();
|
||||||
|
socket.connect(&SocketAddr::new(0, 0))?;
|
||||||
|
|
||||||
|
let route_msg_hdr = RouteHeader {
|
||||||
|
address_family: AddressFamily::Inet,
|
||||||
|
table: MAIN_TABLE_ID,
|
||||||
|
destination_prefix_length: 0, // Default route
|
||||||
|
protocol: RouteProtocol::Boot,
|
||||||
|
scope: RouteScope::Universe,
|
||||||
|
kind: RouteType::Unicast,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut route_msg = RouteMessage::default();
|
||||||
|
route_msg.header = route_msg_hdr;
|
||||||
|
route_msg.attributes = vec![
|
||||||
|
RouteAttribute::Gateway(RouteAddress::Inet(gateway)),
|
||||||
|
RouteAttribute::Oif(index),
|
||||||
|
RouteAttribute::Priority(metric),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut nl_hdr = NetlinkHeader::default();
|
||||||
|
nl_hdr.flags = NLM_F_REQUEST | NLM_F_ACK;
|
||||||
|
|
||||||
|
let mut msg = NetlinkMessage::new(
|
||||||
|
nl_hdr,
|
||||||
|
NetlinkPayload::from(RouteNetlinkMessage::DelRoute(route_msg)),
|
||||||
|
);
|
||||||
|
|
||||||
|
msg.finalize();
|
||||||
|
let mut buf = vec![0; 1024 * 8];
|
||||||
|
|
||||||
|
msg.serialize(&mut buf[..msg.buffer_len()]);
|
||||||
|
|
||||||
|
// Debug: Log the netlink message being sent
|
||||||
|
debug!(
|
||||||
|
"Netlink delete message being sent: {:?}",
|
||||||
|
&buf[..msg.buffer_len()]
|
||||||
|
);
|
||||||
|
debug!(
|
||||||
|
"Route deletion attempt: gateway={}, interface={}, metric={}, interface_index={}",
|
||||||
|
gateway, interface, metric, index
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.send(&buf, 0)?;
|
||||||
|
|
||||||
|
let mut receive_buffer = Vec::with_capacity(256);
|
||||||
|
let size = socket.recv(&mut receive_buffer, 0)?;
|
||||||
|
|
||||||
|
if size == 0 {
|
||||||
|
return Err(anyhow::anyhow!("No response from netlink"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = &receive_buffer[..size];
|
||||||
|
let rx_packet = <NetlinkMessage<RouteNetlinkMessage>>::deserialize(bytes);
|
||||||
|
|
||||||
|
match rx_packet {
|
||||||
|
Ok(rx_packet) => {
|
||||||
|
if let NetlinkPayload::Error(error_msg) = rx_packet.payload {
|
||||||
|
if error_msg.code.is_some() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to delete route: {:?}", error_msg));
|
||||||
|
}
|
||||||
|
debug!("Route deleted successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to deserialize netlink message: {}",
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_interface_index(iface_name: &str) -> Result<u32> {
|
||||||
|
let c_name = CString::new(iface_name)?;
|
||||||
|
let index = unsafe { if_nametoindex(c_name.as_ptr()) };
|
||||||
|
|
||||||
|
if index == 0 {
|
||||||
|
Err(anyhow::anyhow!("Interface '{}' not found", iface_name))
|
||||||
|
} else {
|
||||||
|
Ok(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
487
src/state_machine.rs
Normal file
487
src/state_machine.rs
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
use log::{info, warn};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::pinger;
|
||||||
|
use crate::routing;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum State {
|
||||||
|
Boot,
|
||||||
|
Primary,
|
||||||
|
Fallback,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StateMachine {
|
||||||
|
state: State,
|
||||||
|
primary_history: VecDeque<pinger::PingResult>,
|
||||||
|
secondary_history: VecDeque<pinger::PingResult>,
|
||||||
|
failover_threshold: usize,
|
||||||
|
failback_delay: Duration,
|
||||||
|
last_failover: Option<std::time::Instant>,
|
||||||
|
boot_start_time: std::time::Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateMachine {
|
||||||
|
pub fn new(failover_threshold: usize, failback_delay: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
state: State::Boot,
|
||||||
|
primary_history: VecDeque::with_capacity(60),
|
||||||
|
secondary_history: VecDeque::with_capacity(60),
|
||||||
|
failover_threshold,
|
||||||
|
failback_delay,
|
||||||
|
last_failover: None,
|
||||||
|
boot_start_time: std::time::Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_primary_result(&mut self, result: pinger::PingResult) {
|
||||||
|
self.primary_history.push_front(result);
|
||||||
|
if self.primary_history.len() > 60 {
|
||||||
|
self.primary_history.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_secondary_result(&mut self, result: pinger::PingResult) {
|
||||||
|
self.secondary_history.push_front(result);
|
||||||
|
if self.secondary_history.len() > 60 {
|
||||||
|
self.secondary_history.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_fallback(&self) -> bool {
|
||||||
|
if self.primary_history.len() < self.failover_threshold {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have enough consecutive failures
|
||||||
|
self.primary_history
|
||||||
|
.iter()
|
||||||
|
.take(self.failover_threshold)
|
||||||
|
.all(|x| *x == pinger::PingResult::Failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_failback(&self) -> bool {
|
||||||
|
if self.secondary_history.len() < 3 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if secondary is working
|
||||||
|
let secondary_working = self
|
||||||
|
.secondary_history
|
||||||
|
.iter()
|
||||||
|
.take(3)
|
||||||
|
.any(|x| *x == pinger::PingResult::Ok);
|
||||||
|
|
||||||
|
if !secondary_working {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if primary is working consistently
|
||||||
|
if self.primary_history.len() < 10 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let primary_working = self
|
||||||
|
.primary_history
|
||||||
|
.iter()
|
||||||
|
.take(10)
|
||||||
|
.all(|x| *x == pinger::PingResult::Ok);
|
||||||
|
|
||||||
|
if !primary_working {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if enough time has passed since last failover
|
||||||
|
if let Some(last_failover) = self.last_failover {
|
||||||
|
last_failover.elapsed() >= self.failback_delay
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_state(&self) -> &State {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_state(&mut self) -> Option<(State, State)> {
|
||||||
|
let old_state = self.state.clone();
|
||||||
|
|
||||||
|
match self.state {
|
||||||
|
State::Boot => {
|
||||||
|
// Check if 10 seconds have passed since boot start
|
||||||
|
if self.boot_start_time.elapsed() >= Duration::from_secs(10) {
|
||||||
|
self.state = State::Primary;
|
||||||
|
info!("Boot phase complete (10s sampling), switching to Primary state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::Primary => {
|
||||||
|
if self.should_fallback() {
|
||||||
|
// Verify secondary is working before failing over
|
||||||
|
if self.secondary_history.len() >= 3 {
|
||||||
|
let secondary_working = self
|
||||||
|
.secondary_history
|
||||||
|
.iter()
|
||||||
|
.take(3)
|
||||||
|
.all(|x| *x == pinger::PingResult::Ok);
|
||||||
|
|
||||||
|
if secondary_working {
|
||||||
|
self.state = State::Fallback;
|
||||||
|
self.last_failover = Some(std::time::Instant::now());
|
||||||
|
warn!("Primary interface failing over to secondary");
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Primary interface failing but secondary also down - not switching"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::Fallback => {
|
||||||
|
if self.should_failback() {
|
||||||
|
self.state = State::Primary;
|
||||||
|
info!("Primary interface restored, failing back");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if old_state != self.state {
|
||||||
|
Some((old_state, self.state.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_state_change(
|
||||||
|
new_state: State,
|
||||||
|
old_state: State,
|
||||||
|
route_manager: &mut routing::RouteManager,
|
||||||
|
_primary_gateway: &std::net::Ipv4Addr,
|
||||||
|
secondary_gateway: &std::net::Ipv4Addr,
|
||||||
|
config: &crate::Config,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
match (old_state, new_state) {
|
||||||
|
(State::Fallback, State::Primary) => {
|
||||||
|
// Only remove failover route when transitioning from Fallback to Primary
|
||||||
|
info!("Switching to primary route (failback)");
|
||||||
|
route_manager
|
||||||
|
.remove_failover_route(*secondary_gateway, config.secondary_interface.clone())?;
|
||||||
|
}
|
||||||
|
(_, State::Fallback) => {
|
||||||
|
info!("Switching to secondary route (failover)");
|
||||||
|
// Add extra failover route with higher priority (metric 5)
|
||||||
|
route_manager
|
||||||
|
.add_failover_route(*secondary_gateway, config.secondary_interface.clone())?;
|
||||||
|
}
|
||||||
|
(State::Boot, State::Primary) => {
|
||||||
|
// Boot -> Primary transition should not remove any routes
|
||||||
|
info!("Boot phase complete, entering Primary state");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Other transitions don't require route changes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::pinger::PingResult;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_state_machine_initialization() {
|
||||||
|
let sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
assert_eq!(sm.state, State::Boot);
|
||||||
|
assert_eq!(sm.failover_threshold, 3);
|
||||||
|
assert_eq!(sm.failback_delay, Duration::from_secs(60));
|
||||||
|
assert!(sm.primary_history.is_empty());
|
||||||
|
assert!(sm.secondary_history.is_empty());
|
||||||
|
assert!(sm.last_failover.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_primary_result() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
assert_eq!(sm.primary_history.len(), 1);
|
||||||
|
assert_eq!(sm.primary_history[0], PingResult::Ok);
|
||||||
|
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
assert_eq!(sm.primary_history.len(), 2);
|
||||||
|
assert_eq!(sm.primary_history[0], PingResult::Failed);
|
||||||
|
assert_eq!(sm.primary_history[1], PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_secondary_result() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
assert_eq!(sm.secondary_history.len(), 1);
|
||||||
|
assert_eq!(sm.secondary_history[0], PingResult::Ok);
|
||||||
|
|
||||||
|
sm.add_secondary_result(PingResult::Failed);
|
||||||
|
assert_eq!(sm.secondary_history.len(), 2);
|
||||||
|
assert_eq!(sm.secondary_history[0], PingResult::Failed);
|
||||||
|
assert_eq!(sm.secondary_history[1], PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_history_capacity_limit() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Add 65 results (more than capacity of 60)
|
||||||
|
for _ in 0..65 {
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(sm.primary_history.len(), 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_fallback_insufficient_history() {
|
||||||
|
let sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
assert!(!sm.should_fallback()); // Empty history
|
||||||
|
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
assert!(!sm.should_fallback()); // Only 2 results, need 3
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_fallback_consecutive_failures() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Add 3 consecutive failures
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
|
||||||
|
assert!(sm.should_fallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_fallback_mixed_results() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Add mixed results (not all failures)
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
|
||||||
|
assert!(!sm.should_fallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_failback_insufficient_secondary_history() {
|
||||||
|
let sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
assert!(!sm.should_failback()); // Empty secondary history
|
||||||
|
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
assert!(!sm.should_failback()); // Only 2 results, need 3
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_failback_secondary_not_working() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Add 3 secondary failures
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_secondary_result(PingResult::Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(!sm.should_failback());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_failback_insufficient_primary_history() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Add 3 working secondary results
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add only 5 primary results (need 10)
|
||||||
|
for _ in 0..5 {
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(!sm.should_failback());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_failback_primary_not_working() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Add 3 working secondary results
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 10 mixed primary results
|
||||||
|
for _ in 0..5 {
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(!sm.should_failback());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_failback_success() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Add 3 working secondary results
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 10 working primary results
|
||||||
|
for _ in 0..10 {
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(sm.should_failback());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_failback_respect_delay() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Set up conditions for failback
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
for _ in 0..10 {
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate recent failover
|
||||||
|
sm.last_failover = Some(std::time::Instant::now());
|
||||||
|
|
||||||
|
assert!(!sm.should_failback()); // Should not failback immediately
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_state_boot_to_primary() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Simulate boot phase completion by manipulating boot_start_time
|
||||||
|
sm.boot_start_time = std::time::Instant::now() - Duration::from_secs(11);
|
||||||
|
|
||||||
|
let transition = sm.update_state();
|
||||||
|
assert!(transition.is_some());
|
||||||
|
assert_eq!(transition.unwrap(), (State::Boot, State::Primary));
|
||||||
|
assert_eq!(sm.state, State::Primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_state_still_in_boot() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
|
||||||
|
// Still in boot phase (less than 10 seconds)
|
||||||
|
let transition = sm.update_state();
|
||||||
|
assert!(transition.is_none());
|
||||||
|
assert_eq!(sm.state, State::Boot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_state_primary_to_fallback() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
sm.state = State::Primary;
|
||||||
|
|
||||||
|
// Add consecutive primary failures
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add working secondary results
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition = sm.update_state();
|
||||||
|
assert!(transition.is_some());
|
||||||
|
assert_eq!(transition.unwrap(), (State::Primary, State::Fallback));
|
||||||
|
assert_eq!(sm.state, State::Fallback);
|
||||||
|
assert!(sm.last_failover.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_state_primary_no_fallback_secondary_down() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
sm.state = State::Primary;
|
||||||
|
|
||||||
|
// Add consecutive primary failures
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add failing secondary results
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_secondary_result(PingResult::Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition = sm.update_state();
|
||||||
|
assert!(transition.is_none());
|
||||||
|
assert_eq!(sm.state, State::Primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_state_fallback_to_primary() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
sm.state = State::Fallback;
|
||||||
|
|
||||||
|
// Set up conditions for failback
|
||||||
|
for _ in 0..3 {
|
||||||
|
sm.add_secondary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
for _ in 0..10 {
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition = sm.update_state();
|
||||||
|
assert!(transition.is_some());
|
||||||
|
assert_eq!(transition.unwrap(), (State::Fallback, State::Primary));
|
||||||
|
assert_eq!(sm.state, State::Primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_state_no_transition() {
|
||||||
|
let mut sm = StateMachine::new(3, Duration::from_secs(60));
|
||||||
|
sm.state = State::Primary;
|
||||||
|
|
||||||
|
// Add some mixed results that don't trigger transitions
|
||||||
|
sm.add_primary_result(PingResult::Ok);
|
||||||
|
sm.add_primary_result(PingResult::Failed);
|
||||||
|
|
||||||
|
let transition = sm.update_state();
|
||||||
|
assert!(transition.is_none());
|
||||||
|
assert_eq!(sm.state, State::Primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_state_equality() {
|
||||||
|
assert_eq!(State::Boot, State::Boot);
|
||||||
|
assert_eq!(State::Primary, State::Primary);
|
||||||
|
assert_eq!(State::Fallback, State::Fallback);
|
||||||
|
|
||||||
|
assert_ne!(State::Boot, State::Primary);
|
||||||
|
assert_ne!(State::Primary, State::Fallback);
|
||||||
|
assert_ne!(State::Fallback, State::Boot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_state_clone() {
|
||||||
|
let state = State::Primary;
|
||||||
|
let cloned = state.clone();
|
||||||
|
assert_eq!(state, cloned);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user