feat: implement HTTPS mTLS tunnel with auth handshake

Add HTTPS default tunnel endpoint and outbound connector using Rustls
with mTLS and an additional auth/session token.

- Add rustls, tokio-rustls, hyper, and related dependencies
- Implement TLS config builders (server/client) with cert validation
- Implement HTTPS tunnel listener with mTLS accept and auth handshake
- Implement HTTPS tunnel connector with server cert validation
- Add auth token loading from CLI value or file
- Add security gate state machine for tracking auth progression
- Fail closed for missing/invalid TLS config, cert mismatch, auth failures
- Port conflicts fail without fallback
- Reconnect revalidates all security gates
- 67 tests covering all validation assertions
This commit is contained in:
c4ch3c4d3
2026-06-03 18:29:15 -06:00
parent 03dab7c2c8
commit 393100de5a
7 changed files with 1955 additions and 19 deletions
+89 -16
View File
@@ -1,13 +1,20 @@
mod cli;
mod config;
mod errors;
mod generate;
mod redact;
mod tls;
mod tunnel;
use std::net::SocketAddr;
use std::path::Path;
use std::process;
use std::sync::Arc;
use clap::Parser;
use crate::cli::Cli;
use crate::tunnel::{ConnectorConfig, ListenerConfig};
fn main() {
// Initialize tracing/logger — secrets are never printed by design
@@ -28,7 +35,8 @@ fn main() {
key,
ca_cert,
auth_token,
} => run_listen(listen, cert, key, ca_cert, auth_token),
auth_token_file,
} => run_listen(listen, cert, key, ca_cert, auth_token, auth_token_file),
crate::cli::Commands::Connect {
target,
socks,
@@ -36,7 +44,16 @@ fn main() {
key,
ca_cert,
auth_token,
} => run_connect(target, socks, cert, key, ca_cert, auth_token),
auth_token_file,
} => run_connect(
target,
socks,
cert,
key,
ca_cert,
auth_token,
auth_token_file,
),
crate::cli::Commands::Generate {
out,
ca_name,
@@ -55,6 +72,7 @@ fn run_listen(
key: String,
ca_cert: String,
auth_token: String,
auth_token_file: Option<String>,
) -> i32 {
// Validate all inputs before opening any sockets
if cert.is_empty() {
@@ -69,10 +87,6 @@ fn run_listen(
eprintln!("Error: --ca-cert is required for listen command");
return 1;
}
if auth_token.is_empty() {
eprintln!("Error: --auth-token is required for listen command");
return 1;
}
let (host, port) = match cli::parse_host_port(&listen) {
Ok(v) => v,
@@ -96,6 +110,21 @@ fn run_listen(
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
} 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;
}
};
tracing::info!(
"Starting HTTPS tunnel listener on {}:{} (HTTPS default transport)",
host,
@@ -109,8 +138,24 @@ fn run_listen(
redact::Redacted::new(&auth_token)
);
// TODO: Implement HTTPS mTLS listener
tracing::info!("Listener implementation pending (Milestone 2)");
let bind_addr: SocketAddr = format!("{}:{}", host, port).parse().unwrap();
let config = ListenerConfig {
bind_addr,
server_cert_path: Arc::from(Path::new(&cert)),
server_key_path: Arc::from(Path::new(&key)),
ca_cert_path: Arc::from(Path::new(&ca_cert)),
auth_token: Arc::new(auth_token),
};
// Run the async listener
if let Err(e) = tokio::runtime::Runtime::new()
.unwrap()
.block_on(tunnel::run_listener(config))
{
eprintln!("Error: {}", e);
return 1;
}
0
}
@@ -121,6 +166,7 @@ fn run_connect(
key: String,
ca_cert: String,
auth_token: String,
auth_token_file: Option<String>,
) -> i32 {
// Validate all inputs before opening any sockets
if cert.is_empty() {
@@ -135,12 +181,8 @@ fn run_connect(
eprintln!("Error: --ca-cert is required for connect command");
return 1;
}
if auth_token.is_empty() {
eprintln!("Error: --auth-token is required for connect command");
return 1;
}
let (_target_host, target_port) = match cli::parse_host_port(&target) {
let (target_host, target_port) = match cli::parse_host_port(&target) {
Ok(v) => v,
Err(e) => {
eprintln!("Error: {}", e);
@@ -170,9 +212,24 @@ fn run_connect(
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
} 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;
}
};
tracing::info!(
"Connecting to HTTPS tunnel at {}:{} (HTTPS default transport)",
_target_host,
target_host,
target_port
);
tracing::info!("SOCKS5 proxy will listen on {}:{} ", socks_host, socks_port);
@@ -184,8 +241,24 @@ fn run_connect(
redact::Redacted::new(&auth_token)
);
// TODO: Implement HTTPS mTLS connector and SOCKS5 proxy
tracing::info!("Connector and SOCKS5 implementation pending (Milestone 2-3)");
let target_addr: SocketAddr = format!("{}:{}", target_host, target_port).parse().unwrap();
let config = ConnectorConfig {
target_addr,
client_cert_path: Arc::from(Path::new(&cert)),
client_key_path: Arc::from(Path::new(&key)),
ca_cert_path: Arc::from(Path::new(&ca_cert)),
auth_token: Arc::new(auth_token),
};
// Run the async connector
if let Err(e) = tokio::runtime::Runtime::new()
.unwrap()
.block_on(tunnel::connect_tunnel(config))
{
eprintln!("Error: {}", e);
return 1;
}
0
}