feat: add connection key encoding and full tunnel implementation

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
root
2026-06-04 13:54:55 -06:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent cc1eb55c58
commit a758e8e0bc
10 changed files with 1677 additions and 390 deletions
+367 -90
View File
@@ -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<String>,
connection_key: Option<String>,
socks: Option<String>,
cert: String,
key: String,
ca_cert: String,
auth_token: String,
auth_token_file: Option<String>,
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<String>,
target: Option<String>,
connection_key: Option<String>,
socks: String,
cert: String,
key: String,
ca_cert: String,
auth_token: String,
auth_token_file: Option<String>,
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));
}
}