mod cli; mod config; mod connkey; mod errors; mod framing; mod generate; mod redact; mod signal; mod socks5; mod tls; mod tunnel; use std::path::Path; use std::process; use std::sync::Arc; use std::time::Duration; use clap::Parser; use crate::cli::Cli; use crate::connkey::ConnectionKey; use crate::tunnel::{ ClientTlsMaterial, ConnectorConfig, ListenerConfig, PerformanceConfig, ServerTlsMaterial, }; fn main() { // Install the default crypto provider (ring) for rustls let _ = rustls::crypto::ring::default_provider().install_default(); // Initialize tracing/logger — secrets are never printed by design tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .without_time() .init(); let cli = Cli::parse(); let exit_code = match cli.command { crate::cli::Commands::Listen { listen, advertise, connection_key, socks, cert, key, ca_cert, auth_token, auth_token_file, insecure_skip_tls_verify, max_open_files, max_tunnel_sessions, max_socks_connections, max_streams_per_tunnel, handshake_timeout_ms, connect_timeout_ms, stream_open_timeout_ms, } => run_listen( listen, advertise, connection_key, socks, cert, key, ca_cert, auth_token, auth_token_file, insecure_skip_tls_verify, max_open_files, max_tunnel_sessions, max_socks_connections, max_streams_per_tunnel, handshake_timeout_ms, connect_timeout_ms, stream_open_timeout_ms, ), crate::cli::Commands::Connect { connection_key_arg, target, connection_key, socks, cert, key, ca_cert, auth_token, auth_token_file, insecure_skip_tls_verify, max_open_files, max_socks_connections, max_streams_per_tunnel, handshake_timeout_ms, connect_timeout_ms, stream_open_timeout_ms, } => run_connect( connection_key_arg, target, connection_key, socks, cert, key, ca_cert, auth_token, auth_token_file, insecure_skip_tls_verify, max_open_files, max_socks_connections, max_streams_per_tunnel, handshake_timeout_ms, connect_timeout_ms, stream_open_timeout_ms, ), crate::cli::Commands::Generate { out, ca_name, server_name, client_name, } => run_generate(&out, &ca_name, &server_name, &client_name), crate::cli::Commands::Keygen { target, ca_name, server_name, client_name, } => run_keygen(&target, &ca_name, &server_name, &client_name), crate::cli::Commands::Version => run_version(), }; process::exit(exit_code); } #[allow(clippy::too_many_arguments)] fn run_listen( listen: String, advertise: Option, connection_key: Option, socks: Option, cert: String, key: String, ca_cert: String, auth_token: String, auth_token_file: Option, insecure_skip_tls_verify: bool, max_open_files: u64, max_tunnel_sessions: usize, max_socks_connections: usize, max_streams_per_tunnel: usize, handshake_timeout_ms: u64, connect_timeout_ms: u64, stream_open_timeout_ms: u64, ) -> i32 { configure_performance( max_open_files, max_tunnel_sessions, max_socks_connections, max_streams_per_tunnel, handshake_timeout_ms, connect_timeout_ms, stream_open_timeout_ms, ); let (host, port) = match cli::parse_host_port(&listen) { Ok(v) => v, Err(e) => { eprintln!("Error: {}", e); return 1; } }; // Resolve hostname to SocketAddr — no panic on failure let bind_addr = match tokio::runtime::Runtime::new() .unwrap() .block_on(tunnel::resolve_host(&host, port)) { Ok(addrs) => addrs[0], Err(e) => { eprintln!("Error: failed to resolve '{}': {}", host, e); return 1; } }; let socks_addr = if let Some(socks) = socks { let (socks_host, socks_port) = match cli::parse_host_port(&socks) { Ok(v) => v, Err(e) => { eprintln!("Error: {}", e); return 1; } }; match tokio::runtime::Runtime::new() .unwrap() .block_on(tunnel::resolve_host(&socks_host, socks_port)) { Ok(addrs) => Some(addrs[0]), Err(e) => { eprintln!( "Error: failed to resolve SOCKS5 address '{}': {}", socks_host, e ); return 1; } } } else { None }; let (tls, auth_token, generated_key) = if let Some(raw_key) = connection_key { match ConnectionKey::decode(&raw_key) { Ok(k) => ( ServerTlsMaterial::Pem { cert: Arc::new(k.server_cert_pem), key: Arc::new(k.server_key_pem), ca_cert: Arc::new(k.ca_cert_pem), }, k.auth_token, None, ), Err(e) => { eprintln!("Error: {}", e); return 1; } } } else if cert.is_empty() && key.is_empty() && ca_cert.is_empty() && auth_token.is_empty() && auth_token_file.is_none() { let target = advertise.unwrap_or_else(|| { if host == "0.0.0.0" || host == "::" { format!("127.0.0.1:{}", port) } else { listen.clone() } }); if let Err(e) = cli::parse_host_port(&target) { eprintln!("Error: invalid --advertise value: {}", e); return 1; } match generate_key( &target, "rustunnel-ca", "rustunnel-server", "rustunnel-client", ) { Ok((key, encoded)) => ( ServerTlsMaterial::Pem { cert: Arc::new(key.server_cert_pem), key: Arc::new(key.server_key_pem), ca_cert: Arc::new(key.ca_cert_pem), }, key.auth_token, Some(encoded), ), Err(e) => { eprintln!("Error generating connection key: {}", e); return 1; } } } else { if cert.is_empty() { eprintln!("Error: --cert is required for listen command unless using a connection key"); return 1; } if key.is_empty() { eprintln!("Error: --key is required for listen command unless using a connection key"); return 1; } if ca_cert.is_empty() { eprintln!( "Error: --ca-cert is required for listen command unless using a connection key" ); return 1; } if !std::path::Path::new(&cert).exists() { eprintln!("Error: server certificate not found: {}", cert); return 1; } if !std::path::Path::new(&key).exists() { eprintln!("Error: server key not found: {}", key); return 1; } if !std::path::Path::new(&ca_cert).exists() { eprintln!("Error: CA certificate not found: {}", ca_cert); return 1; } let token_path: Option<&Path> = auth_token_file.as_deref().map(Path::new); let token_value: Option<&str> = if auth_token.is_empty() { None } else { Some(&auth_token) }; let auth_token = match tunnel::load_auth_token(token_value, token_path) { Ok(t) => t, Err(e) => { eprintln!("Error: {}", e); return 1; } }; ( ServerTlsMaterial::from_paths(&cert, &key, &ca_cert), auth_token, None, ) }; tracing::info!( "Starting HTTPS tunnel listener on {}:{} (HTTPS default transport, endpoint: /tunnel)", host, port ); if let Some(key) = generated_key { println!("Connection key:\n{}\n", key); println!("Connect with:\nrustunnel connect {}", key); } tracing::info!( "Auth token configured: {}", redact::Redacted::new(&auth_token) ); if insecure_skip_tls_verify { tracing::warn!( "Listener running with --insecure-skip-tls-verify: client certificate verification is DISABLED" ); } let config = ListenerConfig { bind_addr, socks_addr, tls, auth_token: Arc::new(auth_token), insecure_skip_tls_verify, }; // Run the async listener if let Err(e) = tokio::runtime::Runtime::new() .unwrap() .block_on(tunnel::run_listener(config)) { eprintln!("Error: {}", e); return 1; } 0 } #[allow(clippy::too_many_arguments)] fn run_connect( connection_key_arg: Option, target: Option, connection_key: Option, socks: String, cert: String, key: String, ca_cert: String, auth_token: String, auth_token_file: Option, insecure_skip_tls_verify: bool, max_open_files: u64, max_socks_connections: usize, max_streams_per_tunnel: usize, handshake_timeout_ms: u64, connect_timeout_ms: u64, stream_open_timeout_ms: u64, ) -> i32 { configure_performance( max_open_files, 1, max_socks_connections, max_streams_per_tunnel, handshake_timeout_ms, connect_timeout_ms, stream_open_timeout_ms, ); let raw_connection_key = connection_key_arg.or(connection_key); let decoded_key = if let Some(raw) = raw_connection_key { match ConnectionKey::decode(&raw) { Ok(k) => Some(k), Err(e) => { eprintln!("Error: {}", e); return 1; } } } else { None }; let target_value = match select_connect_target(target.as_deref(), decoded_key.as_ref()) { Ok(v) => v, Err(e) => { eprintln!("Error: {}", e); return 1; } }; if should_warn_target_override(target.as_deref(), decoded_key.as_ref()) { tracing::warn!( "--target overrides the listener address embedded in the connection key; TLS trust still uses the key's CA/client certificate material" ); } let (target_host, target_port) = match cli::parse_host_port(target_value) { Ok(v) => v, Err(e) => { eprintln!("Error: {}", e); return 1; } }; let (socks_host, socks_port) = match cli::parse_host_port(&socks) { Ok(v) => v, Err(e) => { eprintln!("Error: {}", e); return 1; } }; let (tls, auth_token) = if let Some(k) = decoded_key { ( ClientTlsMaterial::Pem { cert: Arc::new(k.client_cert_pem), key: Arc::new(k.client_key_pem), ca_cert: Arc::new(k.ca_cert_pem), }, k.auth_token, ) } else { if cert.is_empty() { eprintln!( "Error: --cert is required for connect command unless using a connection key" ); return 1; } if key.is_empty() { eprintln!("Error: --key is required for connect command unless using a connection key"); return 1; } if ca_cert.is_empty() { eprintln!( "Error: --ca-cert is required for connect command unless using a connection key" ); return 1; } if !std::path::Path::new(&cert).exists() { eprintln!("Error: client certificate not found: {}", cert); return 1; } if !std::path::Path::new(&key).exists() { eprintln!("Error: client key not found: {}", key); return 1; } if !std::path::Path::new(&ca_cert).exists() { eprintln!("Error: CA certificate not found: {}", ca_cert); return 1; } let token_path: Option<&Path> = auth_token_file.as_deref().map(Path::new); let token_value: Option<&str> = if auth_token.is_empty() { None } else { Some(&auth_token) }; let auth_token = match tunnel::load_auth_token(token_value, token_path) { Ok(t) => t, Err(e) => { eprintln!("Error: {}", e); return 1; } }; ( ClientTlsMaterial::from_paths(&cert, &key, &ca_cert), auth_token, ) }; tracing::info!( "Connecting to HTTPS tunnel at {}:{} (HTTPS default transport)", target_host, target_port ); tracing::info!( "SOCKS5 proxy at {}:{} ready for local clients", socks_host, socks_port ); tracing::info!( "Auth token configured: {}", redact::Redacted::new(&auth_token) ); // Resolve SOCKS5 bind address let socks_addr = match tokio::runtime::Runtime::new() .unwrap() .block_on(tunnel::resolve_host(&socks_host, socks_port)) { Ok(addrs) => addrs[0], Err(e) => { eprintln!( "Error: failed to resolve SOCKS5 address '{}': {}", socks_host, e ); return 1; } }; if insecure_skip_tls_verify { tracing::warn!( "Connector running with --insecure-skip-tls-verify: server certificate verification is DISABLED" ); } let config = ConnectorConfig { target_host, target_port, tls, auth_token: Arc::new(auth_token), insecure_skip_tls_verify, }; // Run the async connector with SOCKS5 proxy if let Err(e) = tokio::runtime::Runtime::new() .unwrap() .block_on(tunnel::run_connector_with_socks(config, socks_addr)) { eprintln!("Error: {}", e); return 1; } 0 } fn configure_performance( max_open_files: u64, max_tunnel_sessions: usize, max_socks_connections: usize, max_streams_per_tunnel: usize, handshake_timeout_ms: u64, connect_timeout_ms: u64, stream_open_timeout_ms: u64, ) { tunnel::set_performance_config(PerformanceConfig { max_open_files, max_tunnel_sessions, max_socks_connections, max_streams_per_tunnel, handshake_timeout: Duration::from_millis(handshake_timeout_ms), connect_timeout: Duration::from_millis(connect_timeout_ms), stream_open_timeout: Duration::from_millis(stream_open_timeout_ms), }); } fn select_connect_target<'a>( target: Option<&'a str>, decoded_key: Option<&'a ConnectionKey>, ) -> anyhow::Result<&'a str> { if let Some(target) = target { return Ok(target); } if let Some(k) = decoded_key { return Ok(k.target.as_str()); } anyhow::bail!("--target is required for connect unless using a connection key") } fn should_warn_target_override(target: Option<&str>, decoded_key: Option<&ConnectionKey>) -> bool { matches!((target, decoded_key), (Some(target), Some(k)) if target != k.target) } fn generate_key( target: &str, ca_name: &str, server_name: &str, client_name: &str, ) -> anyhow::Result<(ConnectionKey, String)> { let (host, _) = cli::parse_host_port(target)?; let material = generate::generate_material_with_server_host( ca_name, server_name, client_name, Some(&host), )?; let key = ConnectionKey::new(target.to_string(), material); let encoded = key.encode()?; Ok((key, encoded)) } fn run_keygen(target: &str, ca_name: &str, server_name: &str, client_name: &str) -> i32 { if let Err(e) = cli::parse_host_port(target) { eprintln!("Error: {}", e); return 1; } match generate_key(target, ca_name, server_name, client_name) { Ok((_key, encoded)) => { println!("{}", encoded); 0 } Err(e) => { eprintln!("Error generating connection key: {}", e); 1 } } } fn run_generate(out: &str, ca_name: &str, server_name: &str, client_name: &str) -> i32 { let out_dir = std::path::Path::new(out); match generate::generate(out_dir, ca_name, server_name, client_name) { Ok(()) => { println!("Credential material generated in {}", out_dir.display()); println!( "Files created: ca.pem, ca.key, server.crt, server.key, client.crt, client.key, token.txt, config.json" ); 0 } Err(e) => { eprintln!("Error generating credentials: {}", e); 1 } } } fn run_version() -> i32 { println!("rustunnel {}", env!("CARGO_PKG_VERSION")); println!("Edition: 2024"); println!("Platform: {}", std::env::consts::OS); println!("Arch: {}", std::env::consts::ARCH); 0 } #[cfg(test)] mod tests { use super::*; fn test_connection_key(target: &str) -> ConnectionKey { ConnectionKey { version: 2, target: target.to_string(), ca_cert_pem: "ca".to_string(), server_cert_pem: "server-cert".to_string(), server_key_pem: "server-key".to_string(), client_cert_pem: "client-cert".to_string(), client_key_pem: "client-key".to_string(), auth_token: "token".to_string(), } } #[test] fn explicit_connect_target_overrides_key_target() { let key = test_connection_key("198.51.100.10:443"); let selected = select_connect_target(Some("23.167.32.42:443"), Some(&key)).unwrap(); assert_eq!(selected, "23.167.32.42:443"); } #[test] fn connect_uses_key_target_when_explicit_target_absent() { let key = test_connection_key("198.51.100.10:443"); let selected = select_connect_target(None, Some(&key)).unwrap(); assert_eq!(selected, "198.51.100.10:443"); } #[test] fn connect_target_is_required_without_key() { let err = select_connect_target(None, None).unwrap_err(); assert_eq!( err.to_string(), "--target is required for connect unless using a connection key" ); } #[test] fn warns_when_explicit_target_differs_from_key_target() { let key = test_connection_key("198.51.100.10:443"); assert!(should_warn_target_override( Some("23.167.32.42:443"), Some(&key) )); assert!(!should_warn_target_override( Some("198.51.100.10:443"), Some(&key) )); assert!(!should_warn_target_override(Some("23.167.32.42:443"), None)); } }