Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: Rust-based integration test PoC #879

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
431 changes: 404 additions & 27 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -107,6 +109,7 @@ resolver = "2"
members = [
"crates/*",
"clis/*",
"systests",
]
exclude = [
"wireguard-go-rust-wrapper"
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 5 additions & 3 deletions crates/telio-relay/src/derp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
}
}
Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion crates/telio-wg/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ impl FromStr for AdapterType {
}
}

#[cfg(not(any(test, feature = "test-adapter")))]
pub(crate) fn start(
adapter: AdapterType,
name: &str,
Expand Down
2 changes: 0 additions & 2 deletions crates/telio-wg/src/adapter/boring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<dyn Fn(&[u8; 32], &[u8]) -> bool + Send + Sync>>;

pub struct BoringTun {
Expand All @@ -26,7 +25,6 @@ pub struct BoringTun {
}

impl BoringTun {
#[cfg(not(any(test, feature = "test-adapter")))]
pub fn start(
name: &str,
tun: Option<NativeTun>,
Expand Down
1 change: 0 additions & 1 deletion crates/telio-wg/src/adapter/linux_native_wg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ pub enum Error {
}

impl LinuxNativeWg {
#[cfg(not(any(test, feature = "test-adapter")))]
pub fn start(name: &str, _tun: Option<NativeTun>) -> Result<Self, AdapterError> {
let mut rtsocket = RouteSocket::connect().map_err(Error::from)?;

Expand Down
24 changes: 3 additions & 21 deletions crates/telio-wg/src/wg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -376,7 +370,6 @@ impl DynamicWg {
}
}

#[cfg(not(any(test, feature = "test-adapter")))]
fn start_adapter(cfg: Config) -> Result<Box<dyn Adapter>, Error> {
adapter::start(
cfg.adapter,
Expand All @@ -388,17 +381,6 @@ impl DynamicWg {
cfg.firewall_reset_connections,
)
}

#[cfg(any(test, feature = "test-adapter"))]
fn start_adapter(_cfg: Config) -> Result<Box<dyn Adapter>, Error> {
use std::sync::Mutex;

if let Some(adapter) = tests::RUNTIME_ADAPTER.lock().unwrap().take() {
Ok(adapter)
} else {
Err(Error::RestartFailed)
}
}
}

#[async_trait]
Expand Down
59 changes: 59 additions & 0 deletions systests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
[package]
name = "systests"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have a readme.md with some minimal instructions

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
9 changes: 9 additions & 0 deletions systests/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use tests::{meshnet::test_meshnet_poc, vpn::test_vpn_poc};

mod tests;
mod utils;

fn main() {
Copy link
Contributor

@tomasz-grz tomasz-grz Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we use the tests module approach here?

#[cfg(test)]
mod test_vpn {
    use super::*;

    #[test]
    fn test_vpn_poc_works() {
        test_vpn_poc();
    }
}

then we could get nicer test result reports like

thread 'test_vpn::test_vpn_poc_works' panicked at systests/src/tests/vpn.rs:51:5:
assertion failed: false
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Successfully executed 'ip link delete tun10'
Successfully executed 'ip rule del priority 32111'


failures:
    test_vpn::test_vpn_poc_works

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.22s

test_meshnet_poc();
test_vpn_poc();
}
79 changes: 79 additions & 0 deletions systests/src/tests/meshnet.rs
Original file line number Diff line number Diff line change
@@ -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<RwLock<DerpService>> = 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"),
};
}
2 changes: 2 additions & 0 deletions systests/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod meshnet;
pub mod vpn;
56 changes: 56 additions & 0 deletions systests/src/tests/vpn.rs
Original file line number Diff line number Diff line change
@@ -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"),
};
}
20 changes: 20 additions & 0 deletions systests/src/utils/derp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::str::FromStr;

use telio_crypto::PublicKey;
use telio_model::config::{RelayState, Server};

pub fn get_derp_servers() -> Vec<Server> {
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,
}]
}
Loading
Loading