diff --git a/.gitignore b/.gitignore index 42361f7..56621f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/release **/*.pem **/*.json .env diff --git a/Cargo.toml b/Cargo.toml index 6e3df5a..c0840eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ description = "Cross-platform HTTPS mTLS lab/dev tunneling tool" license = "MIT" [dependencies] -clap = { version = "4", features = ["derive"] } +clap = { version = "4", features = ["derive", "env"] } tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7", features = ["io"] } tracing = "0.1" diff --git a/src/cli.rs b/src/cli.rs index 8dda01f..0b50a36 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -17,19 +17,31 @@ pub enum Commands { #[command(trailing_var_arg = true)] Listen { /// Address to bind the HTTPS listener - #[arg(long, default_value = "127.0.0.1:4180", value_name = "ADDR:PORT")] + #[arg(long, default_value = "0.0.0.0:4180", value_name = "ADDR:PORT")] listen: String, + /// Public listener address embedded in a generated connection key + #[arg(long, value_name = "ADDR:PORT")] + advertise: Option, + + /// Reusable connection key generated by keygen/listen + #[arg(long, env = "RUSTUNNEL_KEY", value_name = "KEY")] + connection_key: Option, + + /// Address to bind the server-side SOCKS5 proxy for access to the connector/client network + #[arg(long, value_name = "ADDR:PORT")] + socks: Option, + /// Path to the server certificate file (PEM) - #[arg(long, value_name = "PATH")] + #[arg(long, default_value = "", value_name = "PATH")] cert: String, /// Path to the server private key file (PEM) - #[arg(long, value_name = "PATH")] + #[arg(long, default_value = "", value_name = "PATH")] key: String, /// Path to the CA certificate file used to validate client certificates (PEM) - #[arg(long, value_name = "PATH")] + #[arg(long, default_value = "", value_name = "PATH")] ca_cert: String, /// Auth/session token required from connecting clients @@ -39,36 +51,44 @@ pub enum Commands { /// Path to a file containing the auth/session token (alternative to --auth-token) #[arg(long, value_name = "PATH")] auth_token_file: Option, + + /// Skip TLS certificate verification (insecure — for DPI/intercepted environments only) + #[arg(long, default_value = "false")] + insecure_skip_tls_verify: bool, }, /// Connect to the HTTPS tunnel listener /// /// Establishes an mTLS connection to the listener and authenticates - /// via the /tunnel HTTPS endpoint. - /// - /// NOTE: SOCKS5 proxy is scheduled for the SOCKS milestone. - /// The --socks address is accepted for configuration continuity - /// but is not yet functional. - #[command(trailing_var_arg = true)] + /// via the /tunnel HTTPS endpoint. Use `listen --socks` on the server + /// to expose SOCKS5 access to this connector's network. Connect { - /// Address of the HTTPS tunnel listener to connect to - #[arg(long, default_value = "127.0.0.1:4180", value_name = "ADDR:PORT")] - target: String, + /// Connection key generated by `rustunnel keygen` or `rustunnel listen` + #[arg(value_name = "CONNECTION_KEY")] + connection_key_arg: Option, - /// Address to bind the local SOCKS5 proxy listener (not yet implemented — scheduled for SOCKS milestone) + /// Address of the HTTPS tunnel listener to connect to + #[arg(long, value_name = "ADDR:PORT")] + target: Option, + + /// Connection key generated by `rustunnel keygen` or `rustunnel listen` + #[arg(long, env = "RUSTUNNEL_KEY", value_name = "KEY")] + connection_key: Option, + + /// Address to bind the local SOCKS5 proxy listener for server-side egress #[arg(long, default_value = "127.0.0.1:1180", value_name = "ADDR:PORT")] socks: String, /// Path to the client certificate file (PEM) - #[arg(long, value_name = "PATH")] + #[arg(long, default_value = "", value_name = "PATH")] cert: String, /// Path to the client private key file (PEM) - #[arg(long, value_name = "PATH")] + #[arg(long, default_value = "", value_name = "PATH")] key: String, /// Path to the CA certificate file used to validate the server certificate (PEM) - #[arg(long, value_name = "PATH")] + #[arg(long, default_value = "", value_name = "PATH")] ca_cert: String, /// Auth/session token to authenticate with the listener @@ -78,6 +98,10 @@ pub enum Commands { /// Path to a file containing the auth/session token (alternative to --auth-token) #[arg(long, value_name = "PATH")] auth_token_file: Option, + + /// Skip TLS certificate verification (insecure — for DPI/intercepted environments only) + #[arg(long, default_value = "false")] + insecure_skip_tls_verify: bool, }, /// Generate local certificate and auth material @@ -103,6 +127,26 @@ pub enum Commands { client_name: String, }, + /// Generate a reusable copy/paste connection key + #[command(trailing_var_arg = true)] + Keygen { + /// Listener address that connectors should dial + #[arg(long, default_value = "127.0.0.1:4180", value_name = "ADDR:PORT")] + target: String, + + /// Common name for the CA certificate + #[arg(long, default_value = "rustunnel-ca", value_name = "CN")] + ca_name: String, + + /// Common name for the server certificate + #[arg(long, default_value = "rustunnel-server", value_name = "CN")] + server_name: String, + + /// Common name for the client certificate + #[arg(long, default_value = "rustunnel-client", value_name = "CN")] + client_name: String, + }, + /// Print version information #[command(trailing_var_arg = true)] Version, diff --git a/src/config.rs b/src/config.rs index 47d2947..e107b2d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,7 +39,7 @@ mod tests { fn default_config() -> Config { Config { - listen_address: "127.0.0.1".into(), + listen_address: "0.0.0.0".into(), listen_port: 4180, socks_address: "127.0.0.1".into(), socks_port: 1180, @@ -72,7 +72,7 @@ mod tests { // Verify it's valid JSON by deserializing let _: serde_json::Value = serde_json::from_str(&json).unwrap(); assert!(json.contains("\"listen_address\"")); - assert!(json.contains("127.0.0.1")); + assert!(json.contains("0.0.0.0")); } #[test] diff --git a/src/connkey.rs b/src/connkey.rs new file mode 100644 index 0000000..80daf3b --- /dev/null +++ b/src/connkey.rs @@ -0,0 +1,93 @@ +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use serde::{Deserialize, Serialize}; + +use crate::generate::GeneratedMaterial; + +const PREFIX: &str = "rtun1."; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ConnectionKey { + pub version: u8, + pub target: String, + pub ca_cert_pem: String, + pub server_cert_pem: String, + pub server_key_pem: String, + pub client_cert_pem: String, + pub client_key_pem: String, + pub auth_token: String, +} + +impl ConnectionKey { + pub fn new(target: String, material: GeneratedMaterial) -> Self { + Self { + version: 1, + target, + ca_cert_pem: material.ca_cert_pem, + server_cert_pem: material.server_cert_pem, + server_key_pem: material.server_key_pem, + client_cert_pem: material.client_cert_pem, + client_key_pem: material.client_key_pem, + auth_token: material.auth_token, + } + } + + pub fn encode(&self) -> anyhow::Result { + let json = serde_json::to_vec(self)?; + Ok(format!("{}{}", PREFIX, URL_SAFE_NO_PAD.encode(json))) + } + + pub fn decode(input: &str) -> anyhow::Result { + let encoded = input + .trim() + .strip_prefix(PREFIX) + .ok_or_else(|| anyhow::anyhow!("connection key must start with {}", PREFIX))?; + let bytes = URL_SAFE_NO_PAD + .decode(encoded) + .map_err(|e| anyhow::anyhow!("invalid connection key encoding: {}", e))?; + let key: Self = serde_json::from_slice(&bytes) + .map_err(|e| anyhow::anyhow!("invalid connection key payload: {}", e))?; + if key.version != 1 { + anyhow::bail!("unsupported connection key version {}", key.version); + } + if key.target.trim().is_empty() + || key.ca_cert_pem.trim().is_empty() + || key.server_cert_pem.trim().is_empty() + || key.server_key_pem.trim().is_empty() + || key.client_cert_pem.trim().is_empty() + || key.client_key_pem.trim().is_empty() + || key.auth_token.trim().is_empty() + { + anyhow::bail!("connection key is missing required material"); + } + Ok(key) + } +} + +#[allow(dead_code)] +pub fn looks_like_connection_key(input: &str) -> bool { + input.trim_start().starts_with(PREFIX) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn connection_key_roundtrip() { + let material = + crate::generate::generate_material("ca", "server", "client").expect("material"); + let key = ConnectionKey::new("127.0.0.1:4180".to_string(), material); + let encoded = key.encode().unwrap(); + assert!(encoded.starts_with("rtun1.")); + let decoded = ConnectionKey::decode(&encoded).unwrap(); + assert_eq!(decoded.version, 1); + assert_eq!(decoded.target, "127.0.0.1:4180"); + assert_eq!(decoded.auth_token, key.auth_token); + } + + #[test] + fn invalid_prefix_fails() { + assert!(ConnectionKey::decode("bad").is_err()); + } +} diff --git a/src/generate.rs b/src/generate.rs index c33125c..0f08bcd 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -9,6 +9,17 @@ use rcgen::{ use crate::config::Config; use crate::redact::Redacted; +#[derive(Debug, Clone)] +pub struct GeneratedMaterial { + pub ca_cert_pem: String, + pub ca_key_pem: String, + pub server_cert_pem: String, + pub server_key_pem: String, + pub client_cert_pem: String, + pub client_key_pem: String, + pub auth_token: String, +} + /// Generate all credential material and write it to the output directory. /// /// Creates: @@ -29,32 +40,29 @@ pub fn generate( std::fs::create_dir_all(out_dir) .with_context(|| format!("failed to create output directory: {}", out_dir.display()))?; - // Generate CA - let ca = generate_ca(ca_name)?; + let material = generate_material(ca_name, server_name, client_name)?; let ca_cert_path = out_dir.join("ca.pem"); let ca_key_path = out_dir.join("ca.key"); - std::fs::write(&ca_cert_path, ca.as_ref().pem()).with_context(|| { + std::fs::write(&ca_cert_path, &material.ca_cert_pem).with_context(|| { format!( "failed to write CA certificate to {}", ca_cert_path.display() ) })?; - std::fs::write(&ca_key_path, ca.key().serialize_pem()) + std::fs::write(&ca_key_path, &material.ca_key_pem) .with_context(|| format!("failed to write CA key to {}", ca_key_path.display()))?; tracing::info!("Generated CA certificate at {}", ca_cert_path.display()); tracing::info!("Generated CA key at {}", ca_key_path.display()); - // Generate server certificate - let (server_cert, server_key) = generate_server_cert(&ca, server_name)?; let server_cert_path = out_dir.join("server.crt"); let server_key_path = out_dir.join("server.key"); - std::fs::write(&server_cert_path, server_cert.pem()).with_context(|| { + std::fs::write(&server_cert_path, &material.server_cert_pem).with_context(|| { format!( "failed to write server cert to {}", server_cert_path.display() ) })?; - std::fs::write(&server_key_path, server_key.serialize_pem()).with_context(|| { + std::fs::write(&server_key_path, &material.server_key_pem).with_context(|| { format!( "failed to write server key to {}", server_key_path.display() @@ -66,17 +74,15 @@ pub fn generate( ); tracing::info!("Generated server key at {}", server_key_path.display()); - // Generate client certificate - let (client_cert, client_key) = generate_client_cert(&ca, client_name)?; let client_cert_path = out_dir.join("client.crt"); let client_key_path = out_dir.join("client.key"); - std::fs::write(&client_cert_path, client_cert.pem()).with_context(|| { + std::fs::write(&client_cert_path, &material.client_cert_pem).with_context(|| { format!( "failed to write client cert to {}", client_cert_path.display() ) })?; - std::fs::write(&client_key_path, client_key.serialize_pem()).with_context(|| { + std::fs::write(&client_key_path, &material.client_key_pem).with_context(|| { format!( "failed to write client key to {}", client_key_path.display() @@ -88,11 +94,9 @@ pub fn generate( ); tracing::info!("Generated client key at {}", client_key_path.display()); - // Generate auth token - let auth_token = generate_auth_token(); - let auth_token_redacted = Redacted::new(auth_token.clone()); + let auth_token_redacted = Redacted::new(material.auth_token.clone()); let token_path = out_dir.join("token.txt"); - std::fs::write(&token_path, auth_token) + std::fs::write(&token_path, &material.auth_token) .with_context(|| format!("failed to write token to {}", token_path.display()))?; tracing::info!( "Generated auth token at {} (value: {})", @@ -102,7 +106,7 @@ pub fn generate( // Generate config let config = Config { - listen_address: "127.0.0.1".to_string(), + listen_address: "0.0.0.0".to_string(), listen_port: 4180, socks_address: "127.0.0.1".to_string(), socks_port: 1180, @@ -128,6 +132,34 @@ pub fn generate( Ok(()) } +pub fn generate_material( + ca_name: &str, + server_name: &str, + client_name: &str, +) -> anyhow::Result { + generate_material_with_server_host(ca_name, server_name, client_name, None) +} + +pub fn generate_material_with_server_host( + ca_name: &str, + server_name: &str, + client_name: &str, + server_host: Option<&str>, +) -> anyhow::Result { + let ca = generate_ca(ca_name)?; + let (server_cert, server_key) = generate_server_cert(&ca, server_name, server_host)?; + let (client_cert, client_key) = generate_client_cert(&ca, client_name)?; + Ok(GeneratedMaterial { + ca_cert_pem: ca.as_ref().pem(), + ca_key_pem: ca.key().serialize_pem(), + server_cert_pem: server_cert.pem(), + server_key_pem: server_key.serialize_pem(), + client_cert_pem: client_cert.pem(), + client_key_pem: client_key.serialize_pem(), + auth_token: generate_auth_token(), + }) +} + fn generate_ca(cn: &str) -> anyhow::Result> { let mut params = CertificateParams::default(); let mut dn = DistinguishedName::new(); @@ -145,6 +177,7 @@ fn generate_ca(cn: &str) -> anyhow::Result> { fn generate_server_cert( ca: &CertifiedIssuer<'_, KeyPair>, cn: &str, + extra_host: Option<&str>, ) -> anyhow::Result<(Certificate, KeyPair)> { let mut params = CertificateParams::default(); let mut dn = DistinguishedName::new(); @@ -157,6 +190,16 @@ fn generate_server_cert( SanType::DnsName("rustunnel".try_into().unwrap()), SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))), ]; + if let Some(host) = extra_host + && host != "0.0.0.0" + && host != "::" + { + if let Ok(ip) = host.parse::() { + params.subject_alt_names.push(SanType::IpAddress(ip)); + } else if let Ok(dns) = host.try_into() { + params.subject_alt_names.push(SanType::DnsName(dns)); + } + } params.serial_number = Some(SerialNumber::from(2)); let key_pair = KeyPair::generate()?; @@ -297,7 +340,7 @@ mod tests { let config_str = std::fs::read_to_string(dir.join("config.json")).unwrap(); let config: Config = serde_json::from_str(&config_str).unwrap(); - assert_eq!(config.listen_address, "127.0.0.1"); + assert_eq!(config.listen_address, "0.0.0.0"); assert_eq!(config.listen_port, 4180); assert_eq!(config.socks_address, "127.0.0.1"); assert_eq!(config.socks_port, 1180); diff --git a/src/main.rs b/src/main.rs index af7dbc8..7c78da0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod cli; mod config; +mod connkey; mod errors; mod framing; mod generate; @@ -16,7 +17,8 @@ use std::sync::Arc; use clap::Parser; use crate::cli::Cli; -use crate::tunnel::{ConnectorConfig, ListenerConfig}; +use crate::connkey::ConnectionKey; +use crate::tunnel::{ClientTlsMaterial, ConnectorConfig, ListenerConfig, ServerTlsMaterial}; fn main() { // Install the default crypto provider (ring) for rustls @@ -36,28 +38,49 @@ fn main() { let exit_code = match cli.command { crate::cli::Commands::Listen { listen, + advertise, + connection_key, + socks, cert, key, ca_cert, auth_token, auth_token_file, - } => run_listen(listen, cert, key, ca_cert, auth_token, auth_token_file), + insecure_skip_tls_verify, + } => run_listen( + listen, + advertise, + connection_key, + socks, + cert, + key, + ca_cert, + auth_token, + auth_token_file, + insecure_skip_tls_verify, + ), crate::cli::Commands::Connect { + connection_key_arg, target, + connection_key, socks, cert, key, ca_cert, auth_token, auth_token_file, + insecure_skip_tls_verify, } => run_connect( + connection_key_arg, target, + connection_key, socks, cert, key, ca_cert, auth_token, auth_token_file, + insecure_skip_tls_verify, ), crate::cli::Commands::Generate { out, @@ -65,34 +88,31 @@ fn main() { 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, ) -> i32 { - // Validate all inputs before opening any sockets - if cert.is_empty() { - eprintln!("Error: --cert is required for listen command"); - return 1; - } - if key.is_empty() { - eprintln!("Error: --key is required for listen command"); - return 1; - } - if ca_cert.is_empty() { - eprintln!("Error: --ca-cert is required for listen command"); - return 1; - } - let (host, port) = match cli::parse_host_port(&listen) { Ok(v) => v, Err(e) => { @@ -113,33 +133,129 @@ fn run_listen( } }; - // Verify files exist before opening sockets - 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; - } - - // Load auth token from file or value - let token_path: Option<&Path> = auth_token_file.as_deref().map(Path::new); - let token_value: Option<&str> = if auth_token.is_empty() { - None + 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 { - Some(&auth_token) + None }; - let auth_token = match tunnel::load_auth_token(token_value, token_path) { - Ok(t) => t, - Err(e) => { - eprintln!("Error: {}", e); + + 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!( @@ -147,20 +263,27 @@ fn run_listen( host, port ); - tracing::info!("Using server certificate: {}", redact::Redacted::new(&cert)); - tracing::info!("Using server key: {}", redact::Redacted::new(&key)); - tracing::info!("Using CA certificate: {}", redact::Redacted::new(&ca_cert)); + 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, - server_cert_path: Arc::new(std::path::PathBuf::from(&cert)), - server_key_path: Arc::new(std::path::PathBuf::from(&key)), - ca_cert_path: Arc::new(std::path::PathBuf::from(&ca_cert)), + socks_addr, + tls, auth_token: Arc::new(auth_token), + insecure_skip_tls_verify, }; // Run the async listener @@ -175,30 +298,45 @@ fn run_listen( 0 } +#[allow(clippy::too_many_arguments)] fn run_connect( - target: String, + 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, ) -> i32 { - // Validate all inputs before opening any sockets - if cert.is_empty() { - eprintln!("Error: --cert is required for connect command"); - return 1; - } - if key.is_empty() { - eprintln!("Error: --key is required for connect command"); - return 1; - } - if ca_cert.is_empty() { - eprintln!("Error: --ca-cert is required for connect command"); - return 1; - } + 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_host, target_port) = match cli::parse_host_port(&target) { + 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); @@ -214,33 +352,61 @@ fn run_connect( } }; - // Verify files exist before opening any sockets - 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; - } - - // Load auth token from file or value - let token_path: Option<&Path> = auth_token_file.as_deref().map(Path::new); - let token_value: Option<&str> = if auth_token.is_empty() { - None + 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 { - Some(&auth_token) - }; - let auth_token = match tunnel::load_auth_token(token_value, token_path) { - Ok(t) => t, - Err(e) => { - eprintln!("Error: {}", e); + 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!( @@ -253,9 +419,6 @@ fn run_connect( socks_host, socks_port ); - tracing::info!("Using client certificate: {}", redact::Redacted::new(&cert)); - tracing::info!("Using client key: {}", redact::Redacted::new(&key)); - tracing::info!("Using CA certificate: {}", redact::Redacted::new(&ca_cert)); tracing::info!( "Auth token configured: {}", redact::Redacted::new(&auth_token) @@ -276,13 +439,18 @@ fn run_connect( } }; + 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, - client_cert_path: Arc::new(std::path::PathBuf::from(&cert)), - client_key_path: Arc::new(std::path::PathBuf::from(&key)), - ca_cert_path: Arc::new(std::path::PathBuf::from(&ca_cert)), + tls, auth_token: Arc::new(auth_token), + insecure_skip_tls_verify, }; // Run the async connector with SOCKS5 proxy @@ -297,6 +465,60 @@ fn run_connect( 0 } +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); @@ -322,3 +544,58 @@ fn run_version() -> i32 { println!("Arch: {}", std::env::consts::ARCH); 0 } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_connection_key(target: &str) -> ConnectionKey { + ConnectionKey { + version: 1, + 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)); + } +} diff --git a/src/redact.rs b/src/redact.rs index 0c07d8d..680ea6e 100644 --- a/src/redact.rs +++ b/src/redact.rs @@ -118,6 +118,11 @@ pub fn is_auth_token(content: &str) -> bool { .all(|c| c.is_alphanumeric() || c == '-' || c == '_') } +#[allow(dead_code)] +pub fn is_connection_key(content: &str) -> bool { + content.trim_start().starts_with("rtun1.") +} + #[cfg(test)] mod tests { use super::*; @@ -206,6 +211,12 @@ mod tests { assert!(!is_auth_token("short")); } + #[test] + fn is_connection_key_detects_rtun_prefix() { + assert!(is_connection_key("rtun1.abc")); + assert!(!is_connection_key("abc")); + } + #[test] fn tracing_logs_redact_secrets() { // Verify that tracing with a Redacted value doesn't leak secrets diff --git a/src/tls.rs b/src/tls.rs index 43efbc3..0f1c3df 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -7,21 +7,93 @@ use std::sync::Arc; use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName}; use rustls::server::danger::ClientCertVerifier; -use rustls::{ClientConfig, RootCertStore, ServerConfig}; +use rustls::{ClientConfig, RootCertStore, ServerConfig, SignatureScheme}; use crate::errors::TlsError; +/// Insecure server certificate verifier that accepts any certificate. +/// Used only when `--insecure-skip-tls-verify` is set on the connector. +#[derive(Debug)] +struct InsecureServerCertVerifier; + +impl rustls::client::danger::ServerCertVerifier for InsecureServerCertVerifier { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + ] + } +} + +pub fn cert_fingerprint_sha256(cert: &CertificateDer<'_>) -> String { + use sha2::{Digest, Sha256}; + + let digest = Sha256::digest(cert.as_ref()); + hex::encode(digest) +} + /// Load PEM-encoded certificates from a file. pub fn load_certs(path: &Path) -> Result>, TlsError> { let content = std::fs::read(path).map_err(TlsError::Io)?; + load_certs_from_pem(&content, "certificate file").map_err(|e| match e { + TlsError::Invalid(_, msg) if msg.starts_with("no certificates found") => TlsError::Invalid( + "certificate file", + format!("no certificates found in {}", path.display()), + ), + other => other, + }) +} + +/// Load PEM-encoded certificates from bytes. +pub fn load_certs_from_pem( + content: &[u8], + label: &'static str, +) -> Result>, TlsError> { let mut cursor = std::io::Cursor::new(&content); let certs = rustls_pemfile::certs(&mut cursor) .collect::, _>>() - .map_err(|e| TlsError::Invalid("certificate file", e.to_string()))?; + .map_err(|e| TlsError::Invalid(label, e.to_string()))?; if certs.is_empty() { return Err(TlsError::Invalid( - "certificate file", - format!("no certificates found in {}", path.display()), + label, + "no certificates found".to_string(), )); } Ok(certs) @@ -30,17 +102,87 @@ pub fn load_certs(path: &Path) -> Result>, TlsError> /// Load a PEM-encoded private key from a file. pub fn load_key(path: &Path) -> Result, TlsError> { let content = std::fs::read(path).map_err(TlsError::Io)?; + load_key_from_pem(&content, "private key file").map_err(|e| match e { + TlsError::Invalid(_, msg) if msg == "no private key found" => TlsError::Invalid( + "private key file", + format!("no private key found in {}", path.display()), + ), + other => other, + }) +} + +/// Load a PEM-encoded private key from bytes. +pub fn load_key_from_pem( + content: &[u8], + label: &'static str, +) -> Result, TlsError> { let mut cursor = std::io::Cursor::new(&content); match rustls_pemfile::private_key(&mut cursor) { Ok(Some(key)) => Ok(key), - Ok(None) => Err(TlsError::Invalid( - "private key file", - format!("no private key found in {}", path.display()), - )), - Err(e) => Err(TlsError::Invalid("private key file", e.to_string())), + Ok(None) => Err(TlsError::Invalid(label, "no private key found".to_string())), + Err(e) => Err(TlsError::Invalid(label, e.to_string())), } } +fn build_server_config_from_parts( + server_certs: Vec>, + server_key: PrivateKeyDer<'static>, + ca_certs: Vec>, +) -> Result, TlsError> { + if server_certs.is_empty() { + return Err(TlsError::Missing("server certificate")); + } + if ca_certs.is_empty() { + return Err(TlsError::Missing("CA certificate for client verification")); + } + + let mut root_store = RootCertStore::empty(); + for ca_cert in &ca_certs { + root_store + .add(ca_cert.clone()) + .map_err(|e| TlsError::Invalid("CA certificate", e.to_string()))?; + } + + let client_verifier: Arc = + rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) + .build() + .map_err(|e| TlsError::Invalid("client verifier", e.to_string()))?; + + let config = ServerConfig::builder() + .with_client_cert_verifier(client_verifier) + .with_single_cert(server_certs, server_key) + .map_err(|e| TlsError::Invalid("server config", e.to_string()))?; + + Ok(Arc::new(config)) +} + +fn build_client_config_from_parts( + client_certs: Vec>, + client_key: PrivateKeyDer<'static>, + ca_certs: Vec>, +) -> Result, TlsError> { + if client_certs.is_empty() { + return Err(TlsError::Missing("client certificate")); + } + if ca_certs.is_empty() { + return Err(TlsError::Missing("CA certificate for server verification")); + } + + let mut root_store = RootCertStore::empty(); + for ca_cert in &ca_certs { + root_store + .add(ca_cert.clone()) + .map_err(|e| TlsError::Invalid("CA certificate", e.to_string()))?; + } + + let config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_client_auth_cert(client_certs, client_key) + .map_err(|e| TlsError::Invalid("client config", e.to_string()))?; + + Ok(Arc::new(config)) +} + /// Build a Rustls server configuration with mTLS. /// /// The server will: @@ -52,42 +194,22 @@ pub fn build_server_config( server_key_path: &Path, ca_cert_path: &Path, ) -> Result, TlsError> { - // Load server certificate let server_certs = load_certs(server_cert_path)?; - if server_certs.is_empty() { - return Err(TlsError::Missing("server certificate")); - } - - // Load server private key let server_key = load_key(server_key_path)?; - - // Load CA certificate for client verification let ca_certs = load_certs(ca_cert_path)?; - if ca_certs.is_empty() { - return Err(TlsError::Missing("CA certificate for client verification")); - } + build_server_config_from_parts(server_certs, server_key, ca_certs) +} - // Build root cert store for client verification - let mut root_store = RootCertStore::empty(); - for ca_cert in &ca_certs { - root_store - .add(ca_cert.clone()) - .map_err(|e| TlsError::Invalid("CA certificate", e.to_string()))?; - } - - // Create client verifier that requires valid client certs - let client_verifier: Arc = - rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) - .build() - .map_err(|e| TlsError::Invalid("client verifier", e.to_string()))?; - - // Build server config - let config = ServerConfig::builder() - .with_client_cert_verifier(client_verifier) - .with_single_cert(server_certs, server_key) - .map_err(|e| TlsError::Invalid("server config", e.to_string()))?; - - Ok(Arc::new(config)) +pub fn build_server_config_from_pem( + server_cert_pem: &str, + server_key_pem: &str, + ca_cert_pem: &str, +) -> Result, TlsError> { + build_server_config_from_parts( + load_certs_from_pem(server_cert_pem.as_bytes(), "server certificate")?, + load_key_from_pem(server_key_pem.as_bytes(), "server private key")?, + load_certs_from_pem(ca_cert_pem.as_bytes(), "CA certificate")?, + ) } /// Build a Rustls client configuration with mTLS. @@ -101,35 +223,88 @@ pub fn build_client_config( client_key_path: &Path, ca_cert_path: &Path, ) -> Result, TlsError> { - // Load client certificate let client_certs = load_certs(client_cert_path)?; + let client_key = load_key(client_key_path)?; + let ca_certs = load_certs(ca_cert_path)?; + build_client_config_from_parts(client_certs, client_key, ca_certs) +} + +pub fn build_client_config_from_pem( + client_cert_pem: &str, + client_key_pem: &str, + ca_cert_pem: &str, +) -> Result, TlsError> { + build_client_config_from_parts( + load_certs_from_pem(client_cert_pem.as_bytes(), "client certificate")?, + load_key_from_pem(client_key_pem.as_bytes(), "client private key")?, + load_certs_from_pem(ca_cert_pem.as_bytes(), "CA certificate")?, + ) +} + +fn build_client_config_insecure_from_parts( + client_certs: Vec>, + client_key: PrivateKeyDer<'static>, +) -> Result, TlsError> { if client_certs.is_empty() { return Err(TlsError::Missing("client certificate")); } - - // Load client private key - let client_key = load_key(client_key_path)?; - - // Load CA certificate for server verification - let ca_certs = load_certs(ca_cert_path)?; - if ca_certs.is_empty() { - return Err(TlsError::Missing("CA certificate for server verification")); - } - - // Build root cert store for server verification - let mut root_store = RootCertStore::empty(); - for ca_cert in &ca_certs { - root_store - .add(ca_cert.clone()) - .map_err(|e| TlsError::Invalid("CA certificate", e.to_string()))?; - } - - // Build client config let config = ClientConfig::builder() - .with_root_certificates(root_store) + .dangerous() + .with_custom_certificate_verifier(Arc::new(InsecureServerCertVerifier)) .with_client_auth_cert(client_certs, client_key) .map_err(|e| TlsError::Invalid("client config", e.to_string()))?; + Ok(Arc::new(config)) +} +pub fn build_client_config_insecure( + client_cert_path: &Path, + client_key_path: &Path, +) -> Result, TlsError> { + let client_certs = load_certs(client_cert_path)?; + let client_key = load_key(client_key_path)?; + build_client_config_insecure_from_parts(client_certs, client_key) +} + +pub fn build_client_config_insecure_from_pem( + client_cert_pem: &str, + client_key_pem: &str, +) -> Result, TlsError> { + build_client_config_insecure_from_parts( + load_certs_from_pem(client_cert_pem.as_bytes(), "client certificate")?, + load_key_from_pem(client_key_pem.as_bytes(), "client private key")?, + ) +} + +pub fn build_server_config_insecure( + server_cert_path: &Path, + server_key_path: &Path, +) -> Result, TlsError> { + let server_certs = load_certs(server_cert_path)?; + let server_key = load_key(server_key_path)?; + build_server_config_insecure_from_parts(server_certs, server_key) +} + +pub fn build_server_config_insecure_from_pem( + server_cert_pem: &str, + server_key_pem: &str, +) -> Result, TlsError> { + build_server_config_insecure_from_parts( + load_certs_from_pem(server_cert_pem.as_bytes(), "server certificate")?, + load_key_from_pem(server_key_pem.as_bytes(), "server private key")?, + ) +} + +fn build_server_config_insecure_from_parts( + server_certs: Vec>, + server_key: PrivateKeyDer<'static>, +) -> Result, TlsError> { + if server_certs.is_empty() { + return Err(TlsError::Missing("server certificate")); + } + let config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(server_certs, server_key) + .map_err(|e| TlsError::Invalid("server config", e.to_string()))?; Ok(Arc::new(config)) } @@ -325,6 +500,19 @@ mod tests { assert!(validate_cert_identity(&cert_der, "example.com").is_err()); } + #[test] + fn cert_fingerprint_sha256_is_stable_hex() { + let dir = tempfile::tempdir().unwrap(); + let out_dir = dir.path(); + crate::generate::generate(out_dir, "ca", "server", "client").unwrap(); + let cert = load_certs(&out_dir.join("server.crt")).unwrap()[0].clone(); + let fingerprint = cert_fingerprint_sha256(&cert); + + assert_eq!(fingerprint.len(), 64); + assert!(fingerprint.chars().all(|c| c.is_ascii_hexdigit())); + assert_eq!(fingerprint, cert_fingerprint_sha256(&cert)); + } + #[test] fn check_cert_not_expired_with_valid_cert() { init_crypto(); @@ -380,4 +568,42 @@ mod tests { ); assert!(config.is_err()); } + + #[test] + fn build_client_config_insecure_skips_ca() { + init_crypto(); + let dir = tempfile::tempdir().unwrap(); + let out_dir = dir.path(); + crate::generate::generate(out_dir, "test-ca", "test-server", "test-client").unwrap(); + + let config = + build_client_config_insecure(&out_dir.join("client.crt"), &out_dir.join("client.key")); + assert!(config.is_ok()); + } + + #[test] + fn build_server_config_insecure_skips_ca() { + init_crypto(); + let dir = tempfile::tempdir().unwrap(); + let out_dir = dir.path(); + crate::generate::generate(out_dir, "test-ca", "test-server", "test-client").unwrap(); + + let config = + build_server_config_insecure(&out_dir.join("server.crt"), &out_dir.join("server.key")); + assert!(config.is_ok()); + } + + #[test] + fn build_client_config_insecure_from_pem_skips_ca() { + init_crypto(); + let dir = tempfile::tempdir().unwrap(); + let out_dir = dir.path(); + crate::generate::generate(out_dir, "test-ca", "test-server", "test-client").unwrap(); + + let client_cert = std::fs::read_to_string(out_dir.join("client.crt")).unwrap(); + let client_key = std::fs::read_to_string(out_dir.join("client.key")).unwrap(); + + let config = build_client_config_insecure_from_pem(&client_cert, &client_key); + assert!(config.is_ok()); + } } diff --git a/src/tunnel.rs b/src/tunnel.rs index 78c7cfe..620fed8 100644 --- a/src/tunnel.rs +++ b/src/tunnel.rs @@ -115,10 +115,10 @@ pub async fn resolve_target(host: &str, port: u16) -> Result, - pub server_key_path: Arc, - pub ca_cert_path: Arc, + pub socks_addr: Option, + pub tls: ServerTlsMaterial, pub auth_token: Arc, + pub insecure_skip_tls_verify: bool, } /// Configuration for the tunnel connector. @@ -126,10 +126,159 @@ pub struct ListenerConfig { pub struct ConnectorConfig { pub target_host: String, pub target_port: u16, - pub client_cert_path: Arc, - pub client_key_path: Arc, - pub ca_cert_path: Arc, + pub tls: ClientTlsMaterial, pub auth_token: Arc, + pub insecure_skip_tls_verify: bool, +} + +#[derive(Debug, Clone)] +pub enum ServerTlsMaterial { + Paths { + cert: Arc, + key: Arc, + ca_cert: Arc, + }, + Pem { + cert: Arc, + key: Arc, + ca_cert: Arc, + }, +} + +#[derive(Debug, Clone)] +pub enum ClientTlsMaterial { + Paths { + cert: Arc, + key: Arc, + ca_cert: Arc, + }, + Pem { + cert: Arc, + key: Arc, + ca_cert: Arc, + }, +} + +impl ServerTlsMaterial { + pub fn from_paths( + cert: impl Into, + key: impl Into, + ca_cert: impl Into, + ) -> Self { + Self::Paths { + cert: Arc::new(cert.into()), + key: Arc::new(key.into()), + ca_cert: Arc::new(ca_cert.into()), + } + } + + fn build_config( + &self, + insecure: bool, + ) -> Result, crate::errors::TlsError> { + if insecure { + match self { + Self::Paths { cert, key, .. } => { + tls::build_server_config_insecure(cert.as_ref(), key.as_ref()) + } + Self::Pem { cert, key, .. } => { + tls::build_server_config_insecure_from_pem(cert, key) + } + } + } else { + match self { + Self::Paths { cert, key, ca_cert } => { + tls::build_server_config(cert.as_ref(), key.as_ref(), ca_cert.as_ref()) + } + Self::Pem { cert, key, ca_cert } => { + tls::build_server_config_from_pem(cert, key, ca_cert) + } + } + } + } + + fn server_certs( + &self, + ) -> Result>, crate::errors::TlsError> { + match self { + Self::Paths { cert, .. } => tls::load_certs(cert.as_ref()), + Self::Pem { cert, .. } => { + tls::load_certs_from_pem(cert.as_bytes(), "server certificate") + } + } + } + + fn ca_certs( + &self, + ) -> Result>, crate::errors::TlsError> { + match self { + Self::Paths { ca_cert, .. } => tls::load_certs(ca_cert.as_ref()), + Self::Pem { ca_cert, .. } => { + tls::load_certs_from_pem(ca_cert.as_bytes(), "CA certificate") + } + } + } +} + +impl ClientTlsMaterial { + pub fn from_paths( + cert: impl Into, + key: impl Into, + ca_cert: impl Into, + ) -> Self { + Self::Paths { + cert: Arc::new(cert.into()), + key: Arc::new(key.into()), + ca_cert: Arc::new(ca_cert.into()), + } + } + + fn build_config( + &self, + insecure: bool, + ) -> Result, crate::errors::TlsError> { + if insecure { + match self { + Self::Paths { cert, key, .. } => { + tls::build_client_config_insecure(cert.as_ref(), key.as_ref()) + } + Self::Pem { cert, key, .. } => { + tls::build_client_config_insecure_from_pem(cert, key) + } + } + } else { + match self { + Self::Paths { cert, key, ca_cert } => { + tls::build_client_config(cert.as_ref(), key.as_ref(), ca_cert.as_ref()) + } + Self::Pem { cert, key, ca_cert } => { + tls::build_client_config_from_pem(cert, key, ca_cert) + } + } + } + } + + fn client_certs( + &self, + ) -> Result>, crate::errors::TlsError> { + match self { + Self::Paths { cert, .. } => tls::load_certs(cert.as_ref()), + Self::Pem { cert, .. } => { + tls::load_certs_from_pem(cert.as_bytes(), "client certificate") + } + } + } + + fn ca_certs( + &self, + ) -> Result>, crate::errors::TlsError> { + match self { + Self::Paths { ca_cert, .. } => tls::load_certs(ca_cert.as_ref()), + Self::Pem { ca_cert, .. } => { + tls::load_certs_from_pem(ca_cert.as_bytes(), "CA certificate") + } + } + } } // --------------------------------------------------------------------------- @@ -245,30 +394,27 @@ where /// Start the HTTPS tunnel listener. pub async fn run_listener(config: ListenerConfig) -> Result<(), TunnelError> { let addr = config.bind_addr; - let server_cert_path = config.server_cert_path.clone(); - let server_key_path = config.server_key_path.clone(); - let ca_cert_path = config.ca_cert_path.clone(); + let tls_material = config.tls.clone(); let auth_token = config.auth_token.clone(); - let tls_config = tls::build_server_config( - server_cert_path.as_ref(), - server_key_path.as_ref(), - ca_cert_path.as_ref(), - ) - .map_err(|e| { - tracing::error!("Failed to build TLS config: {}", e); - e - })?; + let tls_config = tls_material + .build_config(config.insecure_skip_tls_verify) + .map_err(|e| { + tracing::error!("Failed to build TLS config: {}", e); + e + })?; - // Validate server cert identity matches bind address - let server_certs = tls::load_certs(server_cert_path.as_ref()).map_err(TunnelError::Tls)?; + let server_certs = tls_material.server_certs().map_err(TunnelError::Tls)?; + let ca_certs = tls_material.ca_certs().map_err(TunnelError::Tls)?; let bind_host = addr.ip().to_string(); - tls::validate_cert_identity(&server_certs[0], &bind_host).map_err(|e| { - TunnelError::Tls(crate::errors::TlsError::IdentityMismatch(format!( - "server cert does not match bind address {}: {}", - bind_host, e - ))) - })?; + if !addr.ip().is_unspecified() { + tls::validate_cert_identity(&server_certs[0], &bind_host).map_err(|e| { + TunnelError::Tls(crate::errors::TlsError::IdentityMismatch(format!( + "server cert does not match bind address {}: {}", + bind_host, e + ))) + })?; + } let tls_acceptor = tokio_rustls::TlsAcceptor::from(tls_config); let listener = TcpListener::bind(addr).await.map_err(|e| { @@ -288,6 +434,23 @@ pub async fn run_listener(config: ListenerConfig) -> Result<(), TunnelError> { addr, Redacted::new(&*auth_token) ); + tracing::info!( + "TLS material fingerprints: ca={}, server={}", + tls::cert_fingerprint_sha256(&ca_certs[0]), + tls::cert_fingerprint_sha256(&server_certs[0]) + ); + + let (mux_sender, mux_receiver) = watch::channel::>>(None); + let socks_enabled = config.socks_addr.is_some(); + let socks_handle = if let Some(socks_addr) = config.socks_addr { + tracing::info!( + "Server-side SOCKS5 proxy configured on {} for connector-side network access", + socks_addr + ); + Some(tokio::spawn(run_socks5_proxy(socks_addr, mux_receiver))) + } else { + None + }; // Listen for shutdown signal let shutdown = crate::signal::shutdown_signal(); @@ -305,9 +468,15 @@ pub async fn run_listener(config: ListenerConfig) -> Result<(), TunnelError> { tracing::info!("New connection from {}", peer_addr); let acceptor = tls_acceptor.clone(); let token = auth_token.clone(); + let session_mux_sender = if socks_enabled { + Some(mux_sender.clone()) + } else { + None + }; + let insecure = config.insecure_skip_tls_verify; tokio::spawn(async move { - if let Err(e) = handle_listener_connection(stream, peer_addr, acceptor, token).await { + if let Err(e) = handle_listener_connection(stream, peer_addr, acceptor, token, session_mux_sender, insecure).await { tracing::warn!("Connection from {} failed: {}", peer_addr, e); } }); @@ -320,6 +489,10 @@ pub async fn run_listener(config: ListenerConfig) -> Result<(), TunnelError> { } } + if let Some(handle) = socks_handle { + handle.abort(); + } + tracing::info!( "HTTPS tunnel listener shutting down — port {} released", addr.port() @@ -333,6 +506,8 @@ async fn handle_listener_connection( peer_addr: SocketAddr, acceptor: tokio_rustls::TlsAcceptor, auth_token: Arc, + mux_sender: Option>>>, + insecure_skip_tls_verify: bool, ) -> Result<(), TunnelError> { // TLS accept let tls_stream = acceptor.accept(stream).await.map_err(|e| { @@ -340,18 +515,25 @@ async fn handle_listener_connection( TunnelError::Tls(crate::errors::TlsError::VerificationFailed(e.to_string())) })?; - // Verify mTLS — client must present a valid certificate - let (_, server_conn) = tls_stream.get_ref(); - let peer_certs = server_conn.peer_certificates().ok_or_else(|| { - tracing::warn!("No client certificate from {}", peer_addr); - TunnelError::Tls(crate::errors::TlsError::Missing("client certificate")) - })?; - if peer_certs.is_empty() { - return Err(TunnelError::Tls(crate::errors::TlsError::Missing( - "client certificate", - ))); + if insecure_skip_tls_verify { + tracing::info!( + "TLS connection accepted from {} (insecure mode — skipping client certificate verification)", + peer_addr + ); + } else { + // Verify mTLS — client must present a valid certificate + let (_, server_conn) = tls_stream.get_ref(); + let peer_certs = server_conn.peer_certificates().ok_or_else(|| { + tracing::warn!("No client certificate from {}", peer_addr); + TunnelError::Tls(crate::errors::TlsError::Missing("client certificate")) + })?; + if peer_certs.is_empty() { + return Err(TunnelError::Tls(crate::errors::TlsError::Missing( + "client certificate", + ))); + } + tracing::info!("mTLS established with {}", peer_addr); } - tracing::info!("mTLS established with {}", peer_addr); // Pin the stream so we can use it with io::split and generic async functions let mut tls_stream: std::pin::Pin>> = @@ -394,7 +576,7 @@ async fn handle_listener_connection( "Upgraded connection from {} to binary framing protocol", peer_addr ); - handle_framing_server(tls_stream, peer_addr).await + handle_framing_server(tls_stream, peer_addr, mux_sender).await } // --------------------------------------------------------------------------- @@ -408,18 +590,28 @@ struct ServerStreamEntry { } /// Handle the binary framing protocol on the server side. -async fn handle_framing_server(stream: S, peer_addr: SocketAddr) -> Result<(), TunnelError> +async fn handle_framing_server( + stream: S, + peer_addr: SocketAddr, + mux_sender: Option>>>, +) -> Result<(), TunnelError> where S: AsyncReadExt + AsyncWriteExt + Unpin + Send + 'static, { let mut stream = stream; let mut reader = FrameReader::new(); let (outbound_tx, mut outbound_rx) = mpsc::channel::(256); + let mux = mux_sender + .as_ref() + .map(|_| Arc::new(StreamMux::new(outbound_tx.clone()))); // Active streams: stream_id → target write half let mut streams: HashMap = HashMap::new(); tracing::info!("Framing session started with {}", peer_addr); + if let (Some(sender), Some(mux)) = (&mux_sender, &mux) { + let _ = sender.send(Some(mux.clone())); + } loop { tokio::select! { @@ -435,13 +627,32 @@ where result = reader.read_frame(&mut stream) => { match result { Ok(Some(frame)) => { - if let Err(e) = handle_inbound_frame( - &frame, - &mut stream, - &outbound_tx, - &mut streams, - peer_addr, - ).await { + let result = if matches!( + frame.frame_type, + FRAME_CONNECT_REPLY | FRAME_DATA | FRAME_CLOSE | FRAME_ERROR + ) { + if let Some(mux) = &mux { + mux.dispatch_frame(&frame).await; + Ok(()) + } else { + handle_inbound_frame( + &frame, + &mut stream, + &outbound_tx, + &mut streams, + peer_addr, + ).await + } + } else { + handle_inbound_frame( + &frame, + &mut stream, + &outbound_tx, + &mut streams, + peer_addr, + ).await + }; + if let Err(e) = result { tracing::warn!( "Stream {} inbound error: {}", frame.stream_id, @@ -471,6 +682,9 @@ where let _ = entry.target_write.shutdown().await; tracing::debug!("Closed stream {} on shutdown", sid); } + if let Some(sender) = mux_sender { + let _ = sender.send(None); + } tracing::info!("Framing session with {} ended", peer_addr); Ok(()) @@ -783,15 +997,15 @@ pub async fn run_connector_with_socks( config: ConnectorConfig, socks_addr: SocketAddr, ) -> Result<(), TunnelError> { - let client_config = tls::build_client_config( - config.client_cert_path.as_ref(), - config.client_key_path.as_ref(), - config.ca_cert_path.as_ref(), - ) - .map_err(|e| { - tracing::error!("Failed to build client TLS config: {}", e); - TunnelError::Tls(e) - })?; + let client_config = config + .tls + .build_config(config.insecure_skip_tls_verify) + .map_err(|e| { + tracing::error!("Failed to build client TLS config: {}", e); + TunnelError::Tls(e) + })?; + let client_certs = config.tls.client_certs().map_err(TunnelError::Tls)?; + let ca_certs = config.tls.ca_certs().map_err(TunnelError::Tls)?; let target_addr = resolve_target(&config.target_host, config.target_port).await?; @@ -803,6 +1017,11 @@ pub async fn run_connector_with_socks( socks_addr, Redacted::new(&*config.auth_token) ); + tracing::info!( + "TLS material fingerprints: ca={}, client={}", + tls::cert_fingerprint_sha256(&ca_certs[0]), + tls::cert_fingerprint_sha256(&client_certs[0]) + ); // Create a watch channel for the active StreamMux. // Initial value is None — first tunnel session will set it. @@ -887,6 +1106,9 @@ async fn run_reconnect_loop( let tls_stream = match tls_connector.connect(server_name.clone(), stream).await { Ok(s) => s, Err(e) => { + if let Some(hint) = tls_handshake_hint(&e.to_string()) { + tracing::warn!("{}", hint); + } tracing::warn!( "TLS handshake to {}:{} failed: {} — retrying in {:?}", config.target_host, @@ -956,6 +1178,26 @@ async fn run_reconnect_loop( } } +fn tls_handshake_hint(error: &str) -> Option<&'static str> { + if error.contains("BadSignature") { + return Some( + "TLS hint: the listener certificate is not signed by the CA trusted by this connector; use the same connection key on both sides or matching --ca-cert/server cert material", + ); + } + + if error.contains("NotValidForName") + || error.contains("CertNotValidForName") + || error.contains("NotValidForNameContext") + || error.contains("certificate does not match") + { + return Some( + "TLS hint: the listener certificate does not match the target host; generate material with --advertise/--target set to the public listener address", + ); + } + + None +} + /// Result of the authenticate_tunnel function. struct AuthResult { mux: Arc, @@ -1002,10 +1244,11 @@ async fn authenticate_tunnel( let (outbound_tx, mut outbound_rx) = mpsc::channel::(256); let mux = Arc::new(StreamMux::new(outbound_tx)); let mux_dispatch = mux.clone(); + let peer_name = target_host.to_string(); // Spawn the framing loop let framing_task = tokio::spawn(async move { - run_framing_client(stream, mux_dispatch, &mut outbound_rx).await; + run_framing_client(stream, mux_dispatch, &mut outbound_rx, peer_name).await; }); Ok(AuthResult { mux, framing_task }) @@ -1016,10 +1259,13 @@ async fn run_framing_client( mut stream: S, mux: Arc, outbound_rx: &mut mpsc::Receiver, + peer_name: String, ) where S: AsyncReadExt + AsyncWriteExt + Unpin, { let mut reader = FrameReader::new(); + let (target_tx, mut target_rx) = mpsc::channel::(256); + let mut target_streams: HashMap = HashMap::new(); loop { tokio::select! { @@ -1031,11 +1277,40 @@ async fn run_framing_client( } } + Some(frame) = target_rx.recv() => { + if let Err(e) = FrameWriter::write_frame(&mut stream, &frame).await { + tracing::debug!("Client target write error for stream {}: {}", frame.stream_id, e); + break; + } + } + // Read inbound frames and dispatch result = reader.read_frame(&mut stream) => { match result { Ok(Some(frame)) => { - mux.dispatch_frame(&frame).await; + if frame.frame_type == FRAME_CONNECT + || target_streams.contains_key(&frame.stream_id) + { + if let Err(e) = handle_inbound_frame( + &frame, + &mut stream, + &target_tx, + &mut target_streams, + SocketAddr::from(([0, 0, 0, 0], 0)), + ).await { + tracing::warn!( + "Connector-side stream {} inbound error from {}: {}", + frame.stream_id, + peer_name, + e + ); + let _ = target_tx + .send(Frame::error(frame.stream_id, e.to_string())) + .await; + } + } else { + mux.dispatch_frame(&frame).await; + } } Ok(None) => { tracing::info!("Tunnel connection closed (EOF)"); @@ -1050,6 +1325,11 @@ async fn run_framing_client( } } + for (sid, mut entry) in target_streams.drain() { + let _ = entry.target_write.shutdown().await; + tracing::debug!("Closed connector-side target stream {} on shutdown", sid); + } + tracing::info!("Client framing loop ended"); } @@ -1346,15 +1626,13 @@ async fn read_exact_2( /// Used by tests to verify tunnel auth/connect paths. #[cfg(test)] pub async fn connect_tunnel(config: ConnectorConfig) -> Result<(), TunnelError> { - let client_config = tls::build_client_config( - config.client_cert_path.as_ref(), - config.client_key_path.as_ref(), - config.ca_cert_path.as_ref(), - ) - .map_err(|e| { - tracing::error!("Failed to build client TLS config: {}", e); - TunnelError::Tls(e) - })?; + let client_config = config + .tls + .build_config(config.insecure_skip_tls_verify) + .map_err(|e| { + tracing::error!("Failed to build client TLS config: {}", e); + TunnelError::Tls(e) + })?; let tls_connector = tokio_rustls::TlsConnector::from(client_config); let target_addr = resolve_target(&config.target_host, config.target_port).await?; @@ -1455,12 +1733,10 @@ impl SecurityGateState { #[cfg(test)] mod tests { use super::*; - use std::sync::atomic::AtomicU16; - - static PORT_COUNTER: AtomicU16 = AtomicU16::new(50000); fn get_free_port() -> u16 { - PORT_COUNTER.fetch_add(1, Ordering::SeqCst) + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + listener.local_addr().unwrap().port() } fn generate_test_creds() -> tempfile::TempDir { @@ -1470,17 +1746,23 @@ mod tests { dir } - macro_rules! path_arc { - ($e:expr) => { - Arc::new(std::path::PathBuf::from($e)) - }; - } - /// Helper: quick-connect that authenticates then immediately returns. async fn quick_connect(config: &ConnectorConfig) -> Result<(), TunnelError> { connect_tunnel(config.clone()).await } + #[test] + fn tls_handshake_hint_explains_bad_signature() { + let hint = tls_handshake_hint("invalid peer certificate: BadSignature").unwrap(); + assert!(hint.contains("not signed by the CA")); + } + + #[test] + fn tls_handshake_hint_explains_name_mismatch() { + let hint = tls_handshake_hint("invalid peer certificate: NotValidForName").unwrap(); + assert!(hint.contains("does not match the target host")); + } + // ========================================================================= // Auth / mTLS tests // ========================================================================= @@ -1493,10 +1775,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; @@ -1504,11 +1790,14 @@ mod tests { tokio::time::sleep(std::time::Duration::from_millis(200)).await; let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; @@ -1522,6 +1811,100 @@ mod tests { listener_task.abort(); } + /// Test: insecure listener accepts a connector without valid client certificate. + #[tokio::test] + async fn insecure_listener_accepts_no_client_cert() { + let dir1 = generate_test_creds(); + let dir2 = generate_test_creds(); + let token = std::fs::read_to_string(dir1.path().join("token.txt")).unwrap(); + let port = get_free_port(); + let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); + + let listener_config = ListenerConfig { + insecure_skip_tls_verify: true, + bind_addr, + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir1.path().join("server.crt"), + dir1.path().join("server.key"), + dir1.path().join("ca.pem"), + ), + auth_token: Arc::new(token.clone()), + }; + + let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + // Connector uses client cert from a DIFFERENT CA but verifies server with listener CA + let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, + target_host: "127.0.0.1".to_string(), + target_port: port, + tls: ClientTlsMaterial::from_paths( + dir2.path().join("client.crt"), + dir2.path().join("client.key"), + dir1.path().join("ca.pem"), + ), + auth_token: Arc::new(token.clone()), + }; + + let result = quick_connect(&connector_config).await; + assert!( + result.is_ok(), + "Insecure listener should accept connector with untrusted client cert: {:?}", + result + ); + + listener_task.abort(); + } + + /// Test: insecure connector accepts a server certificate signed by an unknown CA. + #[tokio::test] + async fn insecure_connector_accepts_unknown_server_cert() { + let dir1 = generate_test_creds(); + let dir2 = generate_test_creds(); + let token = std::fs::read_to_string(dir1.path().join("token.txt")).unwrap(); + let port = get_free_port(); + let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); + + let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, + bind_addr, + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir1.path().join("server.crt"), + dir1.path().join("server.key"), + dir1.path().join("ca.pem"), + ), + auth_token: Arc::new(token.clone()), + }; + + let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + // Connector trusts a DIFFERENT CA (would normally fail server cert verification) + let connector_config = ConnectorConfig { + insecure_skip_tls_verify: true, + target_host: "127.0.0.1".to_string(), + target_port: port, + tls: ClientTlsMaterial::from_paths( + dir1.path().join("client.crt"), + dir1.path().join("client.key"), + dir2.path().join("ca.pem"), + ), + auth_token: Arc::new(token.clone()), + }; + + let result = quick_connect(&connector_config).await; + assert!( + result.is_ok(), + "Insecure connector should accept server with unknown CA: {:?}", + result + ); + + listener_task.abort(); + } + #[tokio::test] async fn invalid_auth_token_fails() { let dir = generate_test_creds(); @@ -1529,10 +1912,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new("correct-token".to_string()), }; @@ -1540,11 +1927,14 @@ mod tests { tokio::time::sleep(std::time::Duration::from_millis(200)).await; let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new("wrong-token".to_string()), }; @@ -1566,10 +1956,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir1.path().join("server.crt")), - server_key_path: path_arc!(dir1.path().join("server.key")), - ca_cert_path: path_arc!(dir1.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir1.path().join("server.crt"), + dir1.path().join("server.key"), + dir1.path().join("ca.pem"), + ), auth_token: Arc::new("token".to_string()), }; @@ -1577,11 +1971,14 @@ mod tests { tokio::time::sleep(std::time::Duration::from_millis(200)).await; let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir2.path().join("client.crt")), - client_key_path: path_arc!(dir2.path().join("client.key")), - ca_cert_path: path_arc!(dir2.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir2.path().join("client.crt"), + dir2.path().join("client.key"), + dir2.path().join("ca.pem"), + ), auth_token: Arc::new("token".to_string()), }; @@ -1604,10 +2001,14 @@ mod tests { let _listener = TcpListener::bind(bind_addr).await.unwrap(); let config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new("token".to_string()), }; @@ -1621,10 +2022,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: Arc::new(std::path::PathBuf::from("/nonexistent/cert.pem")), - server_key_path: Arc::new(std::path::PathBuf::from("/nonexistent/key.pem")), - ca_cert_path: Arc::new(std::path::PathBuf::from("/nonexistent/ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + "/nonexistent/cert.pem", + "/nonexistent/key.pem", + "/nonexistent/ca.pem", + ), auth_token: Arc::new("token".to_string()), }; @@ -1640,10 +2045,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir1.path().join("server.crt")), - server_key_path: path_arc!(dir1.path().join("server.key")), - ca_cert_path: path_arc!(dir1.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir1.path().join("server.crt"), + dir1.path().join("server.key"), + dir1.path().join("ca.pem"), + ), auth_token: Arc::new("token".to_string()), }; @@ -1651,11 +2060,14 @@ mod tests { tokio::time::sleep(std::time::Duration::from_millis(200)).await; let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir2.path().join("client.crt")), - client_key_path: path_arc!(dir2.path().join("client.key")), - ca_cert_path: path_arc!(dir2.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir2.path().join("client.crt"), + dir2.path().join("client.key"), + dir2.path().join("ca.pem"), + ), auth_token: Arc::new("token".to_string()), }; @@ -1771,10 +2183,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new("correct-token".to_string()), }; @@ -1782,11 +2198,14 @@ mod tests { tokio::time::sleep(std::time::Duration::from_millis(200)).await; let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new("wrong-token".to_string()), }; @@ -1808,10 +2227,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; @@ -1819,11 +2242,14 @@ mod tests { tokio::time::sleep(std::time::Duration::from_millis(200)).await; let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; @@ -1831,11 +2257,14 @@ mod tests { assert!(result.is_ok()); let bad_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new("bad-token".to_string()), }; let result = quick_connect(&bad_config).await; @@ -1863,10 +2292,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; @@ -1878,11 +2311,14 @@ mod tests { assert!(stream_result.is_ok(), "TCP should connect to TLS listener"); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; @@ -1961,10 +2397,14 @@ mod tests { // Start listener let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -1973,11 +2413,14 @@ mod tests { // Start connector with SOCKS5 let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2029,6 +2472,77 @@ mod tests { target_task.abort(); } + /// Test: server-side SOCKS uses the connector to open targets. + #[tokio::test] + async fn e2e_server_side_socks_reaches_connector_side_target() { + let dir = generate_test_creds(); + let token = std::fs::read_to_string(dir.path().join("token.txt")).unwrap(); + let listener_port = get_free_port(); + let socks_port = get_free_port(); + let target_port = get_free_port(); + + let target_listener = TcpListener::bind(format!("127.0.0.1:{}", target_port)) + .await + .unwrap(); + let target_task = tokio::spawn(async move { + if let Ok((stream, _)) = target_listener.accept().await { + let (mut rd, mut wr) = stream.into_split(); + let mut buf = [0u8; 4096]; + if let Ok(n) = rd.read(&mut buf).await + && n > 0 + { + let _ = wr.write_all(&buf[..n]).await; + } + } + }); + + let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); + let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); + let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, + bind_addr, + socks_addr: Some(socks_addr), + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), + auth_token: Arc::new(token.clone()), + }; + let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, + target_host: "127.0.0.1".to_string(), + target_port: listener_port, + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), + auth_token: Arc::new(token.clone()), + }; + let client_socks_addr: SocketAddr = + format!("127.0.0.1:{}", get_free_port()).parse().unwrap(); + let connector_task = tokio::spawn(async move { + run_connector_with_socks(connector_config, client_socks_addr).await + }); + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + + let mut stream = TcpStream::connect(socks_addr).await.unwrap(); + socks5_greeting(&mut stream).await.unwrap(); + socks5_connect(&mut stream, "127.0.0.1", target_port) + .await + .unwrap(); + let echoed = socks5_echo(&mut stream, "reverse-socks").await; + assert_eq!(echoed, "reverse-socks"); + + connector_task.abort(); + listener_task.abort(); + target_task.abort(); + } + /// Test: multiple concurrent streams share one tunnel session. #[tokio::test] async fn e2e_concurrent_streams_isolation() { @@ -2069,10 +2583,14 @@ mod tests { // Start listener let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2081,11 +2599,14 @@ mod tests { // Start connector let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2138,10 +2659,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2149,11 +2674,14 @@ mod tests { let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2216,10 +2744,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2227,11 +2759,14 @@ mod tests { let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2301,10 +2836,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2312,11 +2851,14 @@ mod tests { let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2409,10 +2951,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2420,11 +2966,14 @@ mod tests { let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2554,10 +3103,14 @@ mod tests { // Start listener let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2566,11 +3119,14 @@ mod tests { // Start connector with SOCKS5 (has reconnect loop built-in) let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2599,10 +3155,14 @@ mod tests { // Step 3: Restart listener let bind_addr2: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config2 = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr: bind_addr2, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task2 = tokio::spawn(async move { run_listener(listener_config2).await }); @@ -2645,10 +3205,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; @@ -2686,10 +3250,14 @@ mod tests { // Start listener let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2697,11 +3265,14 @@ mod tests { // Start connector let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2761,10 +3332,14 @@ mod tests { // Start listener let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2773,11 +3348,14 @@ mod tests { // Start connector let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2840,10 +3418,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", listener_port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2851,11 +3433,14 @@ mod tests { let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port).parse().unwrap(); let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: listener_port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(token.clone()), }; let connector_task = @@ -2894,10 +3479,14 @@ mod tests { let bind_addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap(); let listener_config = ListenerConfig { + insecure_skip_tls_verify: false, bind_addr, - server_cert_path: path_arc!(dir.path().join("server.crt")), - server_key_path: path_arc!(dir.path().join("server.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + socks_addr: None, + tls: ServerTlsMaterial::from_paths( + dir.path().join("server.crt"), + dir.path().join("server.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new("secret-token-abc".to_string()), }; let listener_task = tokio::spawn(async move { run_listener(listener_config).await }); @@ -2906,11 +3495,14 @@ mod tests { // Connect with wrong token let bad_token = "wrong-secret-token"; let connector_config = ConnectorConfig { + insecure_skip_tls_verify: false, target_host: "127.0.0.1".to_string(), target_port: port, - client_cert_path: path_arc!(dir.path().join("client.crt")), - client_key_path: path_arc!(dir.path().join("client.key")), - ca_cert_path: path_arc!(dir.path().join("ca.pem")), + tls: ClientTlsMaterial::from_paths( + dir.path().join("client.crt"), + dir.path().join("client.key"), + dir.path().join("ca.pem"), + ), auth_token: Arc::new(bad_token.to_string()), }; let result = connect_tunnel(connector_config).await;