From 433519d077111e4d8cd5b50eb99e06907b067746 Mon Sep 17 00:00:00 2001 From: Mathias Peters Date: Thu, 17 Oct 2024 12:12:00 +0200 Subject: [PATCH] Rust-based integration test PoC --- Cargo.lock | 431 ++++++++++++++++-- Cargo.toml | 5 +- crates/telio-relay/src/derp.rs | 8 +- crates/telio-wg/src/adapter.rs | 1 - crates/telio-wg/src/adapter/boring.rs | 2 - .../telio-wg/src/adapter/linux_native_wg.rs | 1 - crates/telio-wg/src/wg.rs | 24 +- systests/Cargo.toml | 59 +++ systests/src/main.rs | 9 + systests/src/tests/meshnet.rs | 79 ++++ systests/src/tests/mod.rs | 2 + systests/src/tests/vpn.rs | 56 +++ systests/src/utils/derp.rs | 20 + systests/src/utils/interface_helper.rs | 70 +++ systests/src/utils/ip.rs | 17 + systests/src/utils/mod.rs | 6 + systests/src/utils/process.rs | 25 + systests/src/utils/test_client.rs | 149 ++++++ systests/src/utils/vpn.rs | 70 +++ tests/integration/logger.rs | 48 ++ tests/integration/main.rs | 4 + tests/integration/meshnet.rs | 45 ++ tests/integration/utils/derp.rs | 20 + tests/integration/utils/interface_helper.rs | 70 +++ tests/integration/utils/ip.rs | 17 + tests/integration/utils/mod.rs | 6 + tests/integration/utils/process.rs | 25 + tests/integration/utils/test_client.rs | 149 ++++++ tests/integration/utils/vpn.rs | 70 +++ tests/integration/vpn.rs | 57 +++ tests/logger.rs | 54 --- 31 files changed, 1489 insertions(+), 110 deletions(-) create mode 100644 systests/Cargo.toml create mode 100644 systests/src/main.rs create mode 100644 systests/src/tests/meshnet.rs create mode 100644 systests/src/tests/mod.rs create mode 100644 systests/src/tests/vpn.rs create mode 100644 systests/src/utils/derp.rs create mode 100644 systests/src/utils/interface_helper.rs create mode 100644 systests/src/utils/ip.rs create mode 100644 systests/src/utils/mod.rs create mode 100644 systests/src/utils/process.rs create mode 100644 systests/src/utils/test_client.rs create mode 100644 systests/src/utils/vpn.rs create mode 100644 tests/integration/logger.rs create mode 100644 tests/integration/main.rs create mode 100644 tests/integration/meshnet.rs create mode 100644 tests/integration/utils/derp.rs create mode 100644 tests/integration/utils/interface_helper.rs create mode 100644 tests/integration/utils/ip.rs create mode 100644 tests/integration/utils/mod.rs create mode 100644 tests/integration/utils/process.rs create mode 100644 tests/integration/utils/test_client.rs create mode 100644 tests/integration/utils/vpn.rs create mode 100644 tests/integration/vpn.rs delete mode 100644 tests/logger.rs diff --git a/Cargo.lock b/Cargo.lock index 418f79abc..c9e9e9c27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -69,12 +84,55 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.89" @@ -299,7 +357,7 @@ version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" dependencies = [ - "http", + "http 0.2.12", "log", "url", "wildmatch", @@ -349,6 +407,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "basic-toml" version = "0.1.9" @@ -618,6 +682,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -689,8 +766,10 @@ version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ + "anstream", "anstyle", "clap_lex 0.7.2", + "strsim 0.11.1", ] [[package]] @@ -744,6 +823,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "codec" +version = "0.1.0" +source = "git+https://github.com/mathiaspeters/dersp.git?branch=librarification#b69fca856bb812dec49b5a0b4dbb5e845b1c56b4" +dependencies = [ + "codec-derive", +] + +[[package]] +name = "codec-derive" +version = "0.1.0" +source = "git+https://github.com/mathiaspeters/dersp.git?branch=librarification#b69fca856bb812dec49b5a0b4dbb5e845b1c56b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "colored" version = "2.1.0" @@ -1093,6 +1196,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9377eb110cece2e9431deb8d7d2ec8c116510b896741f9f2bf02b352147aa2a6" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive_builder" version = "0.10.2" @@ -1148,6 +1261,38 @@ dependencies = [ "url", ] +[[package]] +name = "dersp" +version = "0.1.0" +source = "git+https://github.com/mathiaspeters/dersp.git?branch=librarification#b69fca856bb812dec49b5a0b4dbb5e845b1c56b4" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.13.1", + "clap 4.5.18", + "codec", + "crypto_box", + "env_logger 0.10.2", + "futures-channel", + "futures-util", + "h2 0.4.6", + "hex", + "http 1.1.0", + "httparse", + "log", + "num_enum 0.7.3", + "rand", + "rand_core", + "rustc-hash", + "serde", + "serde_json", + "serde_with", + "strum 0.25.0", + "thiserror", + "tokio", + "tokio-tungstenite", +] + [[package]] name = "diff" version = "0.1.13" @@ -1311,6 +1456,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1607,7 +1765,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", "indexmap 2.5.0", "slab", "tokio", @@ -1775,7 +1952,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1333fad8d94b82cab989da428b0b36a3435db3870d85e971a1d6dc0a8576722" dependencies = [ - "sha1", + "sha1 0.2.0", ] [[package]] @@ -1800,6 +1977,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -1807,7 +1995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite", ] @@ -1839,8 +2027,8 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", + "h2 0.3.26", + "http 0.2.12", "http-body", "httparse", "httpdate", @@ -1860,13 +2048,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.12", "hyper", "rustls", "tokio", "tokio-rustls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys 0.8.7", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1912,7 +2123,7 @@ dependencies = [ "attohttpc", "bytes", "futures", - "http", + "http 0.2.12", "hyper", "log", "rand", @@ -1929,6 +2140,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1939,6 +2151,7 @@ checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -2068,6 +2281,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -2545,6 +2764,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -2580,7 +2805,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive 0.7.3", ] [[package]] @@ -2595,6 +2829,18 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -2859,6 +3105,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -3196,8 +3448,8 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", + "h2 0.3.26", + "http 0.2.12", "http-body", "hyper", "hyper-rustls", @@ -3589,9 +3841,16 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", "serde", "serde_derive", + "serde_json", "serde_with_macros", + "time", ] [[package]] @@ -3663,6 +3922,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -3898,6 +4168,15 @@ dependencies = [ "strum_macros 0.24.3", ] +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.3", +] + [[package]] name = "strum_macros" version = "0.24.3" @@ -3911,6 +4190,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.77", +] + [[package]] name = "strum_macros" version = "0.26.4" @@ -4057,6 +4349,50 @@ dependencies = [ "libc", ] +[[package]] +name = "systests" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.13.1", + "clap 3.2.25", + "crypto_box", + "dersp", + "dirs", + "env_logger 0.10.2", + "futures", + "hex", + "interprocess 1.2.1", + "ipnet", + "parking_lot", + "rand", + "regex", + "reqwest", + "rustyline", + "serde", + "serde_json", + "sha2", + "shellwords", + "sysinfo", + "telio", + "telio-crypto", + "telio-model", + "telio-nat-detect", + "telio-proto", + "telio-relay", + "telio-sockets", + "telio-task", + "telio-traversal", + "telio-utils", + "telio-wg", + "thiserror", + "time", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + [[package]] name = "take-until" version = "0.1.0" @@ -4123,9 +4459,10 @@ dependencies = [ "cc", "cfg-if", "crypto_box", + "dersp", "ffi_helpers", "futures", - "h2", + "h2 0.3.26", "if-addrs", "ipnet", "jni", @@ -4276,13 +4613,13 @@ dependencies = [ "ipnet", "itertools", "modifier", - "num_enum", + "num_enum 0.6.1", "once_cell", "pretty_assertions", "serde", "serde_json", "smart-default", - "strum", + "strum 0.24.1", "strum_macros 0.26.4", "telio-crypto", "telio-utils", @@ -4407,7 +4744,7 @@ dependencies = [ "bytes", "protobuf", "protobuf-codegen-pure", - "strum", + "strum 0.24.1", "telio-crypto", "telio-model", "telio-utils", @@ -4445,7 +4782,7 @@ dependencies = [ "async-trait", "bytes", "crypto_box", - "env_logger", + "env_logger 0.9.3", "futures", "generic-array", "hex", @@ -4454,7 +4791,7 @@ dependencies = [ "libc", "mockall_double", "ntest", - "num_enum", + "num_enum 0.6.1", "rand", "rand_core", "rstest", @@ -4463,7 +4800,7 @@ dependencies = [ "serde", "smart-default", "static_assertions", - "strum", + "strum 0.24.1", "telio-crypto", "telio-model", "telio-nurse", @@ -4561,9 +4898,9 @@ dependencies = [ "boringtun", "bytecodec", "enum-map", - "env_logger", + "env_logger 0.9.3", "futures", - "http", + "http 0.2.12", "httparse", "if-addrs", "igd", @@ -4576,7 +4913,7 @@ dependencies = [ "rand", "rstest", "socket2", - "strum", + "strum 0.24.1", "stun_codec", "surge-ping", "telio-batcher", @@ -4764,13 +5101,16 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ + "deranged", "itoa", "libc", + "num-conv", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -4778,16 +5118,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -4872,6 +5213,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.12" @@ -5022,6 +5375,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1 0.10.6", + "thiserror", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -5230,6 +5601,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 8bd8ade94..48f70b897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,8 @@ pretty_assertions.workspace = true rstest.workspace = true tokio = { workspace = true, features = ["test-util"] } +dersp = { git = "https://github.com/mathiaspeters/dersp.git", branch = "librarification"} + telio-dns = { workspace = true, features = ["mockall"] } telio-firewall = { workspace = true, features = ["mockall"] } telio-nurse = { workspace = true, features = ["mockall"] } @@ -107,6 +109,7 @@ resolver = "2" members = [ "crates/*", "clis/*", + "systests", ] exclude = [ "wireguard-go-rust-wrapper" @@ -161,7 +164,7 @@ socket2 = "0.5" strum = { version = "0.24.0", features = ["derive"] } surge-ping = { version = "0.8.0" } thiserror = "1.0" -time = { version = "0.3.20", features = ["formatting"] } +time = { version = "0.3.36", features = ["formatting"] } tokio = ">=1.22" tracing-subscriber = { version = "0.3.17", features = ["local-time"] } tracing-appender = "0.2.3" diff --git a/crates/telio-relay/src/derp.rs b/crates/telio-relay/src/derp.rs index e55dd4467..762712781 100644 --- a/crates/telio-relay/src/derp.rs +++ b/crates/telio-relay/src/derp.rs @@ -607,9 +607,11 @@ impl State { } } else { telio_log_debug!( - "({}) DERP --> Rx, received a packet with unknown pubkey: {}", + "({}) DERP --> Rx, received a packet with unknown pubkey: {}. Config: {:?}. SK: {}", Self::NAME, - pk + pk, + config, + config.secret_key.public().to_string() ); } } @@ -696,7 +698,7 @@ impl Runtime for State { tokio::select! { // Connection returned, reconnect (err, _, _) = conn_join => { - telio_log_info!("Disconnecting from DERP server, due to transmission tasks error"); + telio_log_info!("Disconnecting from DERP server, due to transmission tasks error: {err:?}"); self.last_disconnection_reason = match err { Ok(Ok(())) => RelayConnectionChangeReason::ConfigurationChange, diff --git a/crates/telio-wg/src/adapter.rs b/crates/telio-wg/src/adapter.rs index 0ae4b71fa..f57de2b05 100644 --- a/crates/telio-wg/src/adapter.rs +++ b/crates/telio-wg/src/adapter.rs @@ -199,7 +199,6 @@ impl FromStr for AdapterType { } } -#[cfg(not(any(test, feature = "test-adapter")))] pub(crate) fn start( adapter: AdapterType, name: &str, diff --git a/crates/telio-wg/src/adapter/boring.rs b/crates/telio-wg/src/adapter/boring.rs index bf2c1543b..d88d268cb 100644 --- a/crates/telio-wg/src/adapter/boring.rs +++ b/crates/telio-wg/src/adapter/boring.rs @@ -17,7 +17,6 @@ pub use boringtun::device::Error; use libc::socket; use telio_sockets::SocketPool; -#[cfg(not(any(test, feature = "test-adapter")))] pub type FirewallCb = Option bool + Send + Sync>>; pub struct BoringTun { @@ -26,7 +25,6 @@ pub struct BoringTun { } impl BoringTun { - #[cfg(not(any(test, feature = "test-adapter")))] pub fn start( name: &str, tun: Option, diff --git a/crates/telio-wg/src/adapter/linux_native_wg.rs b/crates/telio-wg/src/adapter/linux_native_wg.rs index 1626261a8..3149b1189 100644 --- a/crates/telio-wg/src/adapter/linux_native_wg.rs +++ b/crates/telio-wg/src/adapter/linux_native_wg.rs @@ -43,7 +43,6 @@ pub enum Error { } impl LinuxNativeWg { - #[cfg(not(any(test, feature = "test-adapter")))] pub fn start(name: &str, _tun: Option) -> Result { let mut rtsocket = RouteSocket::connect().map_err(Error::from)?; diff --git a/crates/telio-wg/src/wg.rs b/crates/telio-wg/src/wg.rs index bd157dc1a..7c3c7ead4 100644 --- a/crates/telio-wg/src/wg.rs +++ b/crates/telio-wg/src/wg.rs @@ -244,19 +244,13 @@ struct State { const POLL_MILLIS: u64 = 1000; const MAX_UAPI_FAIL_COUNT: i32 = 10; -#[cfg(all(not(any(test, feature = "test-adapter")), windows))] +#[cfg(windows)] const DEFAULT_NAME: &str = "NordLynx"; -#[cfg(all( - not(any(test, feature = "test-adapter")), - any(target_os = "macos", target_os = "ios", target_os = "tvos") -))] +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))] const DEFAULT_NAME: &str = "utun10"; -#[cfg(all( - not(any(test, feature = "test-adapter")), - any(target_os = "linux", target_os = "android") -))] +#[cfg(any(target_os = "linux", target_os = "android"))] const DEFAULT_NAME: &str = "nlx0"; impl DynamicWg { @@ -376,7 +370,6 @@ impl DynamicWg { } } - #[cfg(not(any(test, feature = "test-adapter")))] fn start_adapter(cfg: Config) -> Result, Error> { adapter::start( cfg.adapter, @@ -388,17 +381,6 @@ impl DynamicWg { cfg.firewall_reset_connections, ) } - - #[cfg(any(test, feature = "test-adapter"))] - fn start_adapter(_cfg: Config) -> Result, Error> { - use std::sync::Mutex; - - if let Some(adapter) = tests::RUNTIME_ADAPTER.lock().unwrap().take() { - Ok(adapter) - } else { - Err(Error::RestartFailed) - } - } } #[async_trait] diff --git a/systests/Cargo.toml b/systests/Cargo.toml new file mode 100644 index 000000000..22dfb9021 --- /dev/null +++ b/systests/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "systests" +version = "0.1.0" +edition = "2018" +license = "GPL-3.0-only" +repository = "https://github.com/NordSecurity/libtelio" + +[[bin]] +name = "systests" + +[dependencies] +dersp = { git = "https://github.com/mathiaspeters/dersp.git", branch = "librarification"} +env_logger = "0.10.1" +dirs = "4.0.0" +reqwest = { version = "0.11.16", default-features = false, features = [ + "json", + "blocking", + "rustls-tls", +] } +rustyline = "11.0.0" +shellwords = "1.1.0" +# Used only for checking if the daemon is running. +sysinfo = { version = "0.30.11", optional = true } +# Used as a lightweight and safe (because a TCP server has the risk of remote code execution) +# way for the API and daemon to communicate. +# Tokio support is needed, because the daemon runs on the async runtime. +interprocess = { version = "1.2.1", optional = true } + +anyhow.workspace = true +base64.workspace = true +clap.workspace = true +crypto_box.workspace = true +hex.workspace = true +ipnet.workspace = true +futures.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +tracing-appender.workspace = true +parking_lot.workspace = true +rand = { workspace = true, features = ["std", "std_rng"] } +regex.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +thiserror.workspace = true +time.workspace = true +tokio = { workspace = true, features = ["full"] } + +telio = { path = ".." } +telio-crypto.workspace = true +telio-model.workspace = true +telio-nat-detect.workspace = true +telio-proto.workspace = true +telio-relay.workspace = true +telio-sockets.workspace = true +telio-task.workspace = true +telio-traversal.workspace = true +telio-utils.workspace = true +telio-wg.workspace = true diff --git a/systests/src/main.rs b/systests/src/main.rs new file mode 100644 index 000000000..cd84c45e1 --- /dev/null +++ b/systests/src/main.rs @@ -0,0 +1,9 @@ +use tests::{meshnet::test_meshnet_poc, vpn::test_vpn_poc}; + +mod tests; +mod utils; + +fn main() { + test_meshnet_poc(); + test_vpn_poc(); +} diff --git a/systests/src/tests/meshnet.rs b/systests/src/tests/meshnet.rs new file mode 100644 index 000000000..04c0b28ba --- /dev/null +++ b/systests/src/tests/meshnet.rs @@ -0,0 +1,79 @@ +use std::panic::AssertUnwindSafe; +use std::sync::Arc; + +use dersp::{ + service::{DerpService, Service}, + Config, +}; +use telio::defaults_builder::FeaturesDefaultsBuilder; + +use telio_crypto::SecretKey; +use telio_model::features::PathType; +use tokio::net::TcpListener; +use tokio::sync::RwLock; +use tracing::level_filters::LevelFilter; + +use crate::utils::interface_helper::InterfaceHelper; +use crate::utils::test_client::TestClient; + +pub fn test_meshnet_poc() { + let (non_blocking_writer, _tracing_worker_guard) = + tracing_appender::non_blocking(std::fs::File::create("tcli.log").unwrap()); + tracing_subscriber::fmt() + .with_max_level(LevelFilter::DEBUG) + .with_writer(non_blocking_writer) + .with_ansi(false) + .with_line_number(true) + .with_level(true) + .init(); + + let derp_rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + let _derp_handle = derp_rt.spawn(async move { + let config = Config { + listen_on: "0.0.0.0:8765".to_owned(), + mesh_peers: Vec::new(), + meshkey: Some(SecretKey::gen().public().to_string()), + }; + + let listener = TcpListener::bind(&config.listen_on).await?; + let service: Arc> = DerpService::new(config).await?; + + service.run(listener).await + }); + + let mut ifc_helper = InterfaceHelper::new(); + let test_result = std::panic::catch_unwind(AssertUnwindSafe(|| { + let features = Arc::new(FeaturesDefaultsBuilder::new()); + let features = features.enable_direct().build(); + let mut clients = + TestClient::generate_clients(vec!["alpha", "beta"], &mut ifc_helper, features); + + let mut alpha = clients.remove("alpha").unwrap(); + let mut beta = clients.remove("beta").unwrap(); + + alpha.start(); + beta.start(); + + alpha.set_meshnet_config(&[&beta]); + beta.set_meshnet_config(&[&alpha]); + + alpha + .wait_for_connection_peer(beta.peer.public_key, &[PathType::Direct]) + .unwrap(); + beta.wait_for_connection_peer(alpha.peer.public_key, &[PathType::Direct]) + .unwrap(); + + alpha.stop(); + alpha.shutdown(); + + beta.stop(); + beta.shutdown(); + })); + match test_result { + Ok(()) => println!("test_meshnet_poc passed\n\n"), + Err(e) => println!("test_meshnet_poc failed with error {e:?}\n\n"), + }; +} diff --git a/systests/src/tests/mod.rs b/systests/src/tests/mod.rs new file mode 100644 index 000000000..e488701ae --- /dev/null +++ b/systests/src/tests/mod.rs @@ -0,0 +1,2 @@ +pub mod meshnet; +pub mod vpn; diff --git a/systests/src/tests/vpn.rs b/systests/src/tests/vpn.rs new file mode 100644 index 000000000..0a0841783 --- /dev/null +++ b/systests/src/tests/vpn.rs @@ -0,0 +1,56 @@ +use std::panic::AssertUnwindSafe; + +use telio_model::{features::PathType, mesh::ExitNode}; + +use crate::utils::{ + interface_helper::InterfaceHelper, + test_client::TestClient, + vpn::{setup_vpn_servers, VpnConfig}, +}; + +pub fn test_vpn_poc() { + let mut ifc_helper = InterfaceHelper::new(); + let test_result = std::panic::catch_unwind(AssertUnwindSafe(|| { + let mut clients = + TestClient::generate_clients(vec!["alpha"], &mut ifc_helper, Default::default()); + let mut alpha = clients.remove("alpha").unwrap(); + let vpn_config = VpnConfig::get_config(); + setup_vpn_servers(&[&alpha.peer], &vpn_config); + + alpha.start(); + + if !alpha.ifc_configured { + InterfaceHelper::configure_ifc(&alpha.ifc_name, alpha.ip); + InterfaceHelper::create_vpn_route(&alpha.ifc_name); + alpha.ifc_configured = true; + } + + let node = ExitNode { + identifier: "wgserver".to_owned(), + public_key: vpn_config.key.public(), + allowed_ips: None, + endpoint: Some( + format!("{}:{}", vpn_config.ip, vpn_config.port) + .parse() + .expect("Should be valid"), + ), + }; + alpha.connect_to_exit_node(&node); + + alpha + .wait_for_connection_peer( + vpn_config.key.public(), + &[PathType::Relay, PathType::Direct], + ) + .unwrap(); + + // stun should return VPN IP + + alpha.stop(); + alpha.shutdown(); + })); + match test_result { + Ok(()) => println!("test_vpn_poc passed\n\n"), + Err(e) => println!("test_vpn_poc failed with error {e:?}\n\n"), + }; +} diff --git a/systests/src/utils/derp.rs b/systests/src/utils/derp.rs new file mode 100644 index 000000000..ea0d7d9ae --- /dev/null +++ b/systests/src/utils/derp.rs @@ -0,0 +1,20 @@ +use std::str::FromStr; + +use telio_crypto::PublicKey; +use telio_model::config::{RelayState, Server}; + +pub fn get_derp_servers() -> Vec { + vec![Server { + region_code: "nl".to_owned(), + name: "Natlab #0001".to_owned(), + hostname: "derp-01".to_owned(), + ipv4: "0.0.0.0".parse().unwrap(), + relay_port: 8765, + stun_port: 3479, + stun_plaintext_port: 3478, + public_key: PublicKey::from_str("qK/ICYOGBu45EIGnopVu+aeHDugBrkLAZDroKGTuKU0=").unwrap(), + weight: 1, + use_plain_text: true, + conn_state: RelayState::Disconnected, + }] +} diff --git a/systests/src/utils/interface_helper.rs b/systests/src/utils/interface_helper.rs new file mode 100644 index 000000000..fa00d08c8 --- /dev/null +++ b/systests/src/utils/interface_helper.rs @@ -0,0 +1,70 @@ +use std::net::IpAddr; + +use super::process::run_command; + +pub struct InterfaceHelper { + next_tun_num: u32, +} + +impl InterfaceHelper { + pub fn new() -> Self { + Self { next_tun_num: 10 } + } + + pub fn new_ifc_name(&mut self) -> String { + let tun_name = format!("tun{}", self.next_tun_num); + self.next_tun_num += 1; + tun_name + } + + pub fn configure_ifc(ifc_name: &str, ip: IpAddr) { + run_command( + &[ + "ip", + "-4", + "addr", + "add", + ip.to_string().as_str(), + "dev", + ifc_name, + ], + &[""], + ) + .unwrap(); + run_command(&["ip", "link", "set", "up", "dev", ifc_name], &[""]).unwrap(); + } + + pub fn create_vpn_route(ifc_name: &str) { + for network in ["10.0.0.0/16", "100.64.0.1", "10.5.0.0/16"] { + run_command( + &[ + "ip", "route", "add", network, "dev", ifc_name, "table", "73110", + ], + &[""], + ) + .unwrap(); + } + let _ = run_command( + &[ + "ip", "rule", "add", "priority", "32111", "not", "from", "all", "fwmark", + "11673110", "lookup", "73110", + ], + &["RTNETLINK answers: File exists"], + ) + .unwrap(); + } +} + +impl Drop for InterfaceHelper { + fn drop(&mut self) { + for i in 10..self.next_tun_num { + let ifc_name = format!("tun{i}"); + run_command( + &["ip", "link", "delete", ifc_name.as_str()], + &["Cannot find device"], + ) + .unwrap(); + } + run_command(&["ip", "rule", "del", "priority", "32111"], &[""]).unwrap(); + } +} diff --git a/systests/src/utils/ip.rs b/systests/src/utils/ip.rs new file mode 100644 index 000000000..1ca8dc4a4 --- /dev/null +++ b/systests/src/utils/ip.rs @@ -0,0 +1,17 @@ +use std::net::IpAddr; + +use rand::Rng; + +pub fn generate_ipv4() -> IpAddr { + fn rand_range(lower: u8, upper: u8) -> u8 { + rand::thread_rng().gen_range(lower..=upper) + } + format!( + "100.{}.{}.{}", + rand_range(64, 127), + rand_range(0, 255), + rand_range(8, 254) + ) + .parse() + .unwrap() +} diff --git a/systests/src/utils/mod.rs b/systests/src/utils/mod.rs new file mode 100644 index 000000000..0c0a644c5 --- /dev/null +++ b/systests/src/utils/mod.rs @@ -0,0 +1,6 @@ +pub mod derp; +pub mod interface_helper; +pub mod ip; +pub mod process; +pub mod test_client; +pub mod vpn; diff --git a/systests/src/utils/process.rs b/systests/src/utils/process.rs new file mode 100644 index 000000000..ab028976f --- /dev/null +++ b/systests/src/utils/process.rs @@ -0,0 +1,25 @@ +use std::process::{Command, ExitStatus}; + +#[derive(Debug)] +pub enum ProcessExecError { + FailedToRun(std::io::Error), + FailedDuringRun(ExitStatus, String), +} + +pub fn run_command(args: &[&str], allowed_errors: &[&str]) -> Result { + let full_command = args.join(" "); + let res = Command::new(args[0]) + .args(&args[1..]) + .output() + .map_err(ProcessExecError::FailedToRun)?; + let stderr = + String::from_utf8(res.stderr).expect("Command output shhould be valid utf8 string"); + if res.status.success() || allowed_errors.iter().any(|err| stderr.contains(err)) { + println!("Successfully executed '{full_command}'"); + Ok(String::from_utf8(res.stdout).expect("Command output should be valid utf8 string")) + } else { + let err = ProcessExecError::FailedDuringRun(res.status, stderr); + println!("Failed to execute '{full_command}' with error {err:?}"); + Err(err) + } +} diff --git a/systests/src/utils/test_client.rs b/systests/src/utils/test_client.rs new file mode 100644 index 000000000..6f0254d08 --- /dev/null +++ b/systests/src/utils/test_client.rs @@ -0,0 +1,149 @@ +use std::{ + collections::HashMap, + net::IpAddr, + sync::{Arc, Mutex}, +}; + +use telio::device::{Device, DeviceConfig}; +use telio_crypto::{PublicKey, SecretKey}; +use telio_model::{ + config::{Config, Peer, PeerBase}, + event::Event, + features::{Features, PathType}, + mesh::{ExitNode, NodeState}, +}; +use telio_utils::Hidden; +use telio_wg::AdapterType; + +use super::{derp::get_derp_servers, interface_helper::InterfaceHelper, ip::generate_ipv4}; + +pub struct TestClient { + pub ifc_name: String, + pub ifc_configured: bool, + pub peer: Peer, + pub private_key: SecretKey, + pub ip: IpAddr, + pub dev: Device, + pub events: Arc>>, +} + +impl TestClient { + pub fn generate_clients( + ids: Vec<&'static str>, + ifc_helper: &mut InterfaceHelper, + features: Features, + ) -> HashMap<&'static str, TestClient> { + ids.into_iter() + .map(|id| { + let private_key = SecretKey::gen(); + let ip = generate_ipv4(); + let peer = Peer { + base: PeerBase { + identifier: id.to_owned(), + public_key: private_key.public(), + hostname: Hidden(format!("{id}.nord")), + ip_addresses: Some(vec![ip]), + nickname: None, + }, + is_local: false, + allow_incoming_connections: true, + allow_peer_send_files: false, + allow_multicast: false, + peer_allows_multicast: false, + }; + let events = Arc::new(Mutex::new(Vec::new())); + let events_clone = events.clone(); + let event_dispatcher = move |event: Box| { + println!("Event({id}): {event:?}"); + let mut event_vec = events_clone.lock().unwrap(); + event_vec.push(*event); + }; + let dev = Device::new(features.clone(), event_dispatcher, None).unwrap(); + let client = TestClient { + ifc_name: ifc_helper.new_ifc_name(), + ifc_configured: false, + peer, + private_key, + ip, + dev, + events, + }; + (id, client) + }) + .collect::>() + } + + pub fn start(&mut self) { + self.dev + .start(&DeviceConfig { + private_key: self.private_key, + adapter: AdapterType::BoringTun, + fwmark: None, + name: Some(self.ifc_name.to_owned()), + tun: None, + }) + .unwrap(); + self.dev.set_fwmark(11673110).unwrap(); + } + + pub fn set_meshnet_config(&mut self, peers: &[&TestClient]) { + let config = Config { + this: self.peer.base.clone(), + peers: Some( + peers + .iter() + .filter_map(|client| { + if client.peer.base.identifier == self.peer.base.identifier { + None + } else { + Some(client.peer.clone()) + } + }) + .collect(), + ), + derp_servers: Some(get_derp_servers()), + dns: None, + }; + self.dev.set_config(&Some(config)).unwrap(); + } + + pub fn connect_to_exit_node(&mut self, node: &ExitNode) { + self.dev.connect_exit_node(node).unwrap(); + } + + pub fn wait_for_connection_peer( + &mut self, + peer_key: PublicKey, + path_types: &[PathType], + ) -> Result<(), String> { + let timeout_millis = 10_000; + let iteration_wait_millis = 250; + for _ in 0..(timeout_millis / iteration_wait_millis) { + std::thread::sleep(std::time::Duration::from_millis(iteration_wait_millis)); + let mut events = self.events.lock().unwrap(); + let last_event = events + .iter() + .rev() + .find(|e| matches!(e, Event::Node { body } if body.public_key == peer_key)) + .cloned(); + events.retain(|e| !matches!(e, Event::Node { body } if body.public_key == peer_key)); + if let Some(Event::Node { body }) = last_event { + if path_types.contains(&body.path) && matches!(body.state, NodeState::Connected) { + return Ok(()); + } + } + } + Err(format!( + "{} failed to connect to {}", + self.peer.base.identifier, peer_key + )) + } + + pub fn stop(&mut self) { + self.dev.stop(); + } + + pub fn shutdown(&mut self) { + self.dev.shutdown_art(); + } +} diff --git a/systests/src/utils/vpn.rs b/systests/src/utils/vpn.rs new file mode 100644 index 000000000..948d24537 --- /dev/null +++ b/systests/src/utils/vpn.rs @@ -0,0 +1,70 @@ +use std::net::IpAddr; + +use telio_crypto::SecretKey; +use telio_model::config::Peer; + +use super::process::run_command; + +pub struct VpnConfig { + pub ip: IpAddr, + pub port: u16, + pub key: SecretKey, + pub container: String, +} + +impl VpnConfig { + pub fn get_config() -> Self { + VpnConfig { + ip: "10.0.100.1".parse().unwrap(), + port: 1023, + key: SecretKey::gen(), + container: "nat-lab-vpn-01-1".to_owned(), + } + } +} + +pub fn setup_vpn_servers(peers: &[&Peer], vpn_config: &VpnConfig) { + let mut wg_conf = format!( + "[Interface]\nPrivateKey = {}\nListenPort = {}\nAddress = 100.64.0.1/10\n\n", + vpn_config.key, vpn_config.port + ); + let peer_config = peers.iter().map(|peer| { + let allowed_ips = peer + .base + .ip_addresses + .as_ref() + .map(|ips| { + ips.iter() + .filter_map(|ip| { + if matches!(ip, IpAddr::V4(_)) { + Some(format!("{ip}/32")) + } else { + None + } + }) + .collect::>() + .join(", ") + }) + .unwrap_or_default(); + format!( + "[Peer]\nPublicKey = {}\nAllowedIPs = {allowed_ips}\n\n", + peer.public_key + ) + }); + wg_conf.extend(peer_config); + let docker_command = format!("echo \"{}\" > /etc/wireguard/wg0.conf; wg-quick down /etc/wireguard/wg0.conf; wg-quick up /etc/wireguard/wg0.conf", wg_conf); + + run_command( + &[ + "docker", + "exec", + "--privileged", + vpn_config.container.as_str(), + "bash", + "-c", + docker_command.as_str(), + ], + &[], + ) + .unwrap(); +} diff --git a/tests/integration/logger.rs b/tests/integration/logger.rs new file mode 100644 index 000000000..57d7eb01d --- /dev/null +++ b/tests/integration/logger.rs @@ -0,0 +1,48 @@ +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +use telio::ffi_types::{FfiResult, TelioLoggerCb}; + +#[test] +fn test_logger() { + // Line number of tracing::info! location + const INFO_LINE: u32 = 50; + + let call_count = Arc::new(AtomicUsize::new(0)); + + #[derive(Debug)] + struct TestLogger { + call_count: Arc, + } + impl TelioLoggerCb for TestLogger { + fn log( + &self, + log_level: telio::ffi_types::TelioLogLevel, + payload: String, + ) -> FfiResult<()> { + assert!(matches!(log_level, telio::ffi_types::TelioLogLevel::Info)); + assert_eq!( + format!(r#""logger::test_module":{INFO_LINE} test message"#), + payload + ); + assert_eq!(0, self.call_count.fetch_add(1, Ordering::Relaxed)); + Ok(()) + } + } + + let logger = TestLogger { + call_count: call_count.clone(), + }; + + let tracing_subscriber = telio::ffi::logging::build_subscriber( + telio::ffi_types::TelioLogLevel::Info, + Box::new(logger), + ); + tracing::subscriber::set_global_default(tracing_subscriber).unwrap(); + + tracing::info!("test message"); + assert_eq!(1, call_count.load(Ordering::Relaxed)); + tracing::debug!("this will be ignored since it's below info"); +} diff --git a/tests/integration/main.rs b/tests/integration/main.rs new file mode 100644 index 000000000..7e65acff0 --- /dev/null +++ b/tests/integration/main.rs @@ -0,0 +1,4 @@ +mod logger; +mod meshnet; +mod utils; +mod vpn; diff --git a/tests/integration/meshnet.rs b/tests/integration/meshnet.rs new file mode 100644 index 000000000..35eda887f --- /dev/null +++ b/tests/integration/meshnet.rs @@ -0,0 +1,45 @@ +use std::panic::AssertUnwindSafe; +use std::sync::Arc; + +use telio::defaults_builder::FeaturesDefaultsBuilder; + +use telio_model::features::PathType; + +use crate::utils::interface_helper::InterfaceHelper; +use crate::utils::test_client::TestClient; + +#[test] +fn test_poc_meshnet() { + let mut ifc_helper = InterfaceHelper::new(); + let test_result = std::panic::catch_unwind(AssertUnwindSafe(|| { + let features = Arc::new(FeaturesDefaultsBuilder::new()); + let features = features.enable_direct().build(); + let mut clients = + TestClient::generate_clients(vec!["alpha", "beta"], &mut ifc_helper, features); + + let mut alpha = clients.remove("alpha").unwrap(); + let mut beta = clients.remove("beta").unwrap(); + + alpha.start(); + beta.start(); + + alpha.set_meshnet_config(&[&beta]); + beta.set_meshnet_config(&[&alpha]); + + alpha + .wait_for_connection_peer(beta.peer.public_key, &[PathType::Direct]) + .unwrap(); + beta.wait_for_connection_peer(alpha.peer.public_key, &[PathType::Direct]) + .unwrap(); + + alpha.stop(); + alpha.shutdown(); + + beta.stop(); + beta.shutdown(); + })); + match test_result { + Ok(()) => println!("test_meshnet_poc passed\n\n"), + Err(e) => println!("test_meshnet_poc failed with error {e:?}\n\n"), + }; +} diff --git a/tests/integration/utils/derp.rs b/tests/integration/utils/derp.rs new file mode 100644 index 000000000..ea0d7d9ae --- /dev/null +++ b/tests/integration/utils/derp.rs @@ -0,0 +1,20 @@ +use std::str::FromStr; + +use telio_crypto::PublicKey; +use telio_model::config::{RelayState, Server}; + +pub fn get_derp_servers() -> Vec { + vec![Server { + region_code: "nl".to_owned(), + name: "Natlab #0001".to_owned(), + hostname: "derp-01".to_owned(), + ipv4: "0.0.0.0".parse().unwrap(), + relay_port: 8765, + stun_port: 3479, + stun_plaintext_port: 3478, + public_key: PublicKey::from_str("qK/ICYOGBu45EIGnopVu+aeHDugBrkLAZDroKGTuKU0=").unwrap(), + weight: 1, + use_plain_text: true, + conn_state: RelayState::Disconnected, + }] +} diff --git a/tests/integration/utils/interface_helper.rs b/tests/integration/utils/interface_helper.rs new file mode 100644 index 000000000..fa00d08c8 --- /dev/null +++ b/tests/integration/utils/interface_helper.rs @@ -0,0 +1,70 @@ +use std::net::IpAddr; + +use super::process::run_command; + +pub struct InterfaceHelper { + next_tun_num: u32, +} + +impl InterfaceHelper { + pub fn new() -> Self { + Self { next_tun_num: 10 } + } + + pub fn new_ifc_name(&mut self) -> String { + let tun_name = format!("tun{}", self.next_tun_num); + self.next_tun_num += 1; + tun_name + } + + pub fn configure_ifc(ifc_name: &str, ip: IpAddr) { + run_command( + &[ + "ip", + "-4", + "addr", + "add", + ip.to_string().as_str(), + "dev", + ifc_name, + ], + &[""], + ) + .unwrap(); + run_command(&["ip", "link", "set", "up", "dev", ifc_name], &[""]).unwrap(); + } + + pub fn create_vpn_route(ifc_name: &str) { + for network in ["10.0.0.0/16", "100.64.0.1", "10.5.0.0/16"] { + run_command( + &[ + "ip", "route", "add", network, "dev", ifc_name, "table", "73110", + ], + &[""], + ) + .unwrap(); + } + let _ = run_command( + &[ + "ip", "rule", "add", "priority", "32111", "not", "from", "all", "fwmark", + "11673110", "lookup", "73110", + ], + &["RTNETLINK answers: File exists"], + ) + .unwrap(); + } +} + +impl Drop for InterfaceHelper { + fn drop(&mut self) { + for i in 10..self.next_tun_num { + let ifc_name = format!("tun{i}"); + run_command( + &["ip", "link", "delete", ifc_name.as_str()], + &["Cannot find device"], + ) + .unwrap(); + } + run_command(&["ip", "rule", "del", "priority", "32111"], &[""]).unwrap(); + } +} diff --git a/tests/integration/utils/ip.rs b/tests/integration/utils/ip.rs new file mode 100644 index 000000000..1ca8dc4a4 --- /dev/null +++ b/tests/integration/utils/ip.rs @@ -0,0 +1,17 @@ +use std::net::IpAddr; + +use rand::Rng; + +pub fn generate_ipv4() -> IpAddr { + fn rand_range(lower: u8, upper: u8) -> u8 { + rand::thread_rng().gen_range(lower..=upper) + } + format!( + "100.{}.{}.{}", + rand_range(64, 127), + rand_range(0, 255), + rand_range(8, 254) + ) + .parse() + .unwrap() +} diff --git a/tests/integration/utils/mod.rs b/tests/integration/utils/mod.rs new file mode 100644 index 000000000..0c0a644c5 --- /dev/null +++ b/tests/integration/utils/mod.rs @@ -0,0 +1,6 @@ +pub mod derp; +pub mod interface_helper; +pub mod ip; +pub mod process; +pub mod test_client; +pub mod vpn; diff --git a/tests/integration/utils/process.rs b/tests/integration/utils/process.rs new file mode 100644 index 000000000..ab028976f --- /dev/null +++ b/tests/integration/utils/process.rs @@ -0,0 +1,25 @@ +use std::process::{Command, ExitStatus}; + +#[derive(Debug)] +pub enum ProcessExecError { + FailedToRun(std::io::Error), + FailedDuringRun(ExitStatus, String), +} + +pub fn run_command(args: &[&str], allowed_errors: &[&str]) -> Result { + let full_command = args.join(" "); + let res = Command::new(args[0]) + .args(&args[1..]) + .output() + .map_err(ProcessExecError::FailedToRun)?; + let stderr = + String::from_utf8(res.stderr).expect("Command output shhould be valid utf8 string"); + if res.status.success() || allowed_errors.iter().any(|err| stderr.contains(err)) { + println!("Successfully executed '{full_command}'"); + Ok(String::from_utf8(res.stdout).expect("Command output should be valid utf8 string")) + } else { + let err = ProcessExecError::FailedDuringRun(res.status, stderr); + println!("Failed to execute '{full_command}' with error {err:?}"); + Err(err) + } +} diff --git a/tests/integration/utils/test_client.rs b/tests/integration/utils/test_client.rs new file mode 100644 index 000000000..6f0254d08 --- /dev/null +++ b/tests/integration/utils/test_client.rs @@ -0,0 +1,149 @@ +use std::{ + collections::HashMap, + net::IpAddr, + sync::{Arc, Mutex}, +}; + +use telio::device::{Device, DeviceConfig}; +use telio_crypto::{PublicKey, SecretKey}; +use telio_model::{ + config::{Config, Peer, PeerBase}, + event::Event, + features::{Features, PathType}, + mesh::{ExitNode, NodeState}, +}; +use telio_utils::Hidden; +use telio_wg::AdapterType; + +use super::{derp::get_derp_servers, interface_helper::InterfaceHelper, ip::generate_ipv4}; + +pub struct TestClient { + pub ifc_name: String, + pub ifc_configured: bool, + pub peer: Peer, + pub private_key: SecretKey, + pub ip: IpAddr, + pub dev: Device, + pub events: Arc>>, +} + +impl TestClient { + pub fn generate_clients( + ids: Vec<&'static str>, + ifc_helper: &mut InterfaceHelper, + features: Features, + ) -> HashMap<&'static str, TestClient> { + ids.into_iter() + .map(|id| { + let private_key = SecretKey::gen(); + let ip = generate_ipv4(); + let peer = Peer { + base: PeerBase { + identifier: id.to_owned(), + public_key: private_key.public(), + hostname: Hidden(format!("{id}.nord")), + ip_addresses: Some(vec![ip]), + nickname: None, + }, + is_local: false, + allow_incoming_connections: true, + allow_peer_send_files: false, + allow_multicast: false, + peer_allows_multicast: false, + }; + let events = Arc::new(Mutex::new(Vec::new())); + let events_clone = events.clone(); + let event_dispatcher = move |event: Box| { + println!("Event({id}): {event:?}"); + let mut event_vec = events_clone.lock().unwrap(); + event_vec.push(*event); + }; + let dev = Device::new(features.clone(), event_dispatcher, None).unwrap(); + let client = TestClient { + ifc_name: ifc_helper.new_ifc_name(), + ifc_configured: false, + peer, + private_key, + ip, + dev, + events, + }; + (id, client) + }) + .collect::>() + } + + pub fn start(&mut self) { + self.dev + .start(&DeviceConfig { + private_key: self.private_key, + adapter: AdapterType::BoringTun, + fwmark: None, + name: Some(self.ifc_name.to_owned()), + tun: None, + }) + .unwrap(); + self.dev.set_fwmark(11673110).unwrap(); + } + + pub fn set_meshnet_config(&mut self, peers: &[&TestClient]) { + let config = Config { + this: self.peer.base.clone(), + peers: Some( + peers + .iter() + .filter_map(|client| { + if client.peer.base.identifier == self.peer.base.identifier { + None + } else { + Some(client.peer.clone()) + } + }) + .collect(), + ), + derp_servers: Some(get_derp_servers()), + dns: None, + }; + self.dev.set_config(&Some(config)).unwrap(); + } + + pub fn connect_to_exit_node(&mut self, node: &ExitNode) { + self.dev.connect_exit_node(node).unwrap(); + } + + pub fn wait_for_connection_peer( + &mut self, + peer_key: PublicKey, + path_types: &[PathType], + ) -> Result<(), String> { + let timeout_millis = 10_000; + let iteration_wait_millis = 250; + for _ in 0..(timeout_millis / iteration_wait_millis) { + std::thread::sleep(std::time::Duration::from_millis(iteration_wait_millis)); + let mut events = self.events.lock().unwrap(); + let last_event = events + .iter() + .rev() + .find(|e| matches!(e, Event::Node { body } if body.public_key == peer_key)) + .cloned(); + events.retain(|e| !matches!(e, Event::Node { body } if body.public_key == peer_key)); + if let Some(Event::Node { body }) = last_event { + if path_types.contains(&body.path) && matches!(body.state, NodeState::Connected) { + return Ok(()); + } + } + } + Err(format!( + "{} failed to connect to {}", + self.peer.base.identifier, peer_key + )) + } + + pub fn stop(&mut self) { + self.dev.stop(); + } + + pub fn shutdown(&mut self) { + self.dev.shutdown_art(); + } +} diff --git a/tests/integration/utils/vpn.rs b/tests/integration/utils/vpn.rs new file mode 100644 index 000000000..948d24537 --- /dev/null +++ b/tests/integration/utils/vpn.rs @@ -0,0 +1,70 @@ +use std::net::IpAddr; + +use telio_crypto::SecretKey; +use telio_model::config::Peer; + +use super::process::run_command; + +pub struct VpnConfig { + pub ip: IpAddr, + pub port: u16, + pub key: SecretKey, + pub container: String, +} + +impl VpnConfig { + pub fn get_config() -> Self { + VpnConfig { + ip: "10.0.100.1".parse().unwrap(), + port: 1023, + key: SecretKey::gen(), + container: "nat-lab-vpn-01-1".to_owned(), + } + } +} + +pub fn setup_vpn_servers(peers: &[&Peer], vpn_config: &VpnConfig) { + let mut wg_conf = format!( + "[Interface]\nPrivateKey = {}\nListenPort = {}\nAddress = 100.64.0.1/10\n\n", + vpn_config.key, vpn_config.port + ); + let peer_config = peers.iter().map(|peer| { + let allowed_ips = peer + .base + .ip_addresses + .as_ref() + .map(|ips| { + ips.iter() + .filter_map(|ip| { + if matches!(ip, IpAddr::V4(_)) { + Some(format!("{ip}/32")) + } else { + None + } + }) + .collect::>() + .join(", ") + }) + .unwrap_or_default(); + format!( + "[Peer]\nPublicKey = {}\nAllowedIPs = {allowed_ips}\n\n", + peer.public_key + ) + }); + wg_conf.extend(peer_config); + let docker_command = format!("echo \"{}\" > /etc/wireguard/wg0.conf; wg-quick down /etc/wireguard/wg0.conf; wg-quick up /etc/wireguard/wg0.conf", wg_conf); + + run_command( + &[ + "docker", + "exec", + "--privileged", + vpn_config.container.as_str(), + "bash", + "-c", + docker_command.as_str(), + ], + &[], + ) + .unwrap(); +} diff --git a/tests/integration/vpn.rs b/tests/integration/vpn.rs new file mode 100644 index 000000000..862356f22 --- /dev/null +++ b/tests/integration/vpn.rs @@ -0,0 +1,57 @@ +use std::panic::AssertUnwindSafe; + +use telio_model::{features::PathType, mesh::ExitNode}; + +use crate::utils::{ + interface_helper::InterfaceHelper, + test_client::TestClient, + vpn::{setup_vpn_servers, VpnConfig}, +}; + +#[test] +fn test_poc_vpn() { + let mut ifc_helper = InterfaceHelper::new(); + let test_result = std::panic::catch_unwind(AssertUnwindSafe(|| { + let mut clients = + TestClient::generate_clients(vec!["alpha"], &mut ifc_helper, Default::default()); + let mut alpha = clients.remove("alpha").unwrap(); + let vpn_config = VpnConfig::get_config(); + setup_vpn_servers(&[&alpha.peer], &vpn_config); + + alpha.start(); + + if !alpha.ifc_configured { + InterfaceHelper::configure_ifc(&alpha.ifc_name, alpha.ip); + InterfaceHelper::create_vpn_route(&alpha.ifc_name); + alpha.ifc_configured = true; + } + + let node = ExitNode { + identifier: "wgserver".to_owned(), + public_key: vpn_config.key.public(), + allowed_ips: None, + endpoint: Some( + format!("{}:{}", vpn_config.ip, vpn_config.port) + .parse() + .expect("Should be valid"), + ), + }; + alpha.connect_to_exit_node(&node); + + alpha + .wait_for_connection_peer( + vpn_config.key.public(), + &[PathType::Relay, PathType::Direct], + ) + .unwrap(); + + // stun should return VPN IP + + alpha.stop(); + alpha.shutdown(); + })); + match test_result { + Ok(()) => println!("test_vpn_poc passed\n\n"), + Err(e) => println!("test_vpn_poc failed with error {e:?}\n\n"), + }; +} diff --git a/tests/logger.rs b/tests/logger.rs deleted file mode 100644 index 130aab38f..000000000 --- a/tests/logger.rs +++ /dev/null @@ -1,54 +0,0 @@ -use telio; - -mod test_module { - use std::sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }; - - use telio::ffi_types::{FfiResult, TelioLoggerCb}; - - use super::*; - - #[test] - fn test_logger() { - // Line number of tracing::info! location - const INFO_LINE: u32 = 50; - - let call_count = Arc::new(AtomicUsize::new(0)); - - #[derive(Debug)] - struct TestLogger { - call_count: Arc, - } - impl TelioLoggerCb for TestLogger { - fn log( - &self, - log_level: telio::ffi_types::TelioLogLevel, - payload: String, - ) -> FfiResult<()> { - assert!(matches!(log_level, telio::ffi_types::TelioLogLevel::Info)); - assert_eq!( - format!(r#""logger::test_module":{INFO_LINE} test message"#), - payload - ); - assert_eq!(0, self.call_count.fetch_add(1, Ordering::Relaxed)); - Ok(()) - } - } - - let logger = TestLogger { - call_count: call_count.clone(), - }; - - let tracing_subscriber = telio::ffi::logging::build_subscriber( - telio::ffi_types::TelioLogLevel::Info, - Box::new(logger), - ); - tracing::subscriber::set_global_default(tracing_subscriber).unwrap(); - - tracing::info!("test message"); - assert_eq!(1, call_count.load(Ordering::Relaxed)); - tracing::debug!("this will be ignored since it's below info"); - } -}