Compare commits

...

2 Commits

Author SHA1 Message Date
723ed5e390 Can specify custom server root certificate for client 2022-08-31 10:59:07 +02:00
3b2866fa6a Add embedded TLS server 2022-08-31 09:29:22 +02:00
9 changed files with 310 additions and 33 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
target target
.idea .idea
*.crt
*.key
pki

152
Cargo.lock generated
View File

@@ -52,6 +52,7 @@ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-tls",
"actix-utils", "actix-utils",
"ahash", "ahash",
"base64", "base64",
@@ -143,6 +144,24 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "actix-tls"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297"
dependencies = [
"actix-codec",
"actix-rt",
"actix-service",
"actix-utils",
"futures-core",
"log",
"pin-project-lite",
"tokio-rustls",
"tokio-util",
"webpki-roots",
]
[[package]] [[package]]
name = "actix-utils" name = "actix-utils"
version = "3.0.0" version = "3.0.0"
@@ -166,6 +185,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-server", "actix-server",
"actix-service", "actix-service",
"actix-tls",
"actix-utils", "actix-utils",
"actix-web-codegen", "actix-web-codegen",
"ahash", "ahash",
@@ -817,6 +837,21 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
dependencies = [
"http",
"hyper",
"log",
"rustls",
"rustls-native-certs",
"tokio",
"tokio-rustls",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.5.0" version = "0.5.0"
@@ -1259,6 +1294,7 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
"hyper-rustls",
"hyper-tls", "hyper-tls",
"ipnet", "ipnet",
"js-sys", "js-sys",
@@ -1268,19 +1304,38 @@ dependencies = [
"native-tls", "native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls",
"rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots",
"winreg", "winreg",
] ]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.0"
@@ -1290,6 +1345,39 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
dependencies = [
"base64",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.11" version = "1.0.11"
@@ -1312,6 +1400,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.7.0" version = "2.7.0"
@@ -1440,6 +1538,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@@ -1465,8 +1569,11 @@ dependencies = [
"clap", "clap",
"env_logger", "env_logger",
"futures", "futures",
"hyper-rustls",
"log", "log",
"reqwest", "reqwest",
"rustls",
"rustls-pemfile",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"urlencoding", "urlencoding",
@@ -1477,6 +1584,7 @@ name = "tcp_relay_server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix", "actix",
"actix-tls",
"actix-web", "actix-web",
"actix-web-actors", "actix-web-actors",
"base", "base",
@@ -1484,6 +1592,8 @@ dependencies = [
"env_logger", "env_logger",
"futures", "futures",
"log", "log",
"rustls",
"rustls-pemfile",
"serde", "serde",
"tokio", "tokio",
] ]
@@ -1612,6 +1722,17 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls",
"tokio",
"webpki",
]
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.17.2" version = "0.17.2"
@@ -1620,8 +1741,12 @@ checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
"rustls",
"rustls-native-certs",
"tokio", "tokio",
"tokio-rustls",
"tungstenite", "tungstenite",
"webpki",
] ]
[[package]] [[package]]
@@ -1684,10 +1809,12 @@ dependencies = [
"httparse", "httparse",
"log", "log",
"rand", "rand",
"rustls",
"sha-1", "sha-1",
"thiserror", "thiserror",
"url", "url",
"utf-8", "utf-8",
"webpki",
] ]
[[package]] [[package]]
@@ -1717,6 +1844,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.2.2" version = "2.2.2"
@@ -1845,6 +1978,25 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
dependencies = [
"webpki",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@@ -8,8 +8,11 @@ base = { path = "../base" }
clap = { version = "3.2.18", features = ["derive", "env"] } clap = { version = "3.2.18", features = ["derive", "env"] }
log = "0.4.17" log = "0.4.17"
env_logger = "0.9.0" env_logger = "0.9.0"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
futures = "0.3.24" futures = "0.3.24"
tokio-tungstenite = "0.17.2" tokio-tungstenite = { version = "0.17.2", features = ["__rustls-tls", "rustls-tls-native-roots"] }
urlencoding = "2.1.0" urlencoding = "2.1.0"
rustls = { version = "0.20.6" }
hyper-rustls = { version = "0.23.0", features = ["rustls-native-certs"] }
rustls-pemfile = { version = "1.0.1" }

View File

@@ -1,7 +1,9 @@
use std::error::Error;
use std::sync::Arc; use std::sync::Arc;
use clap::Parser; use clap::Parser;
use futures::future::join_all; use futures::future::join_all;
use reqwest::Certificate;
use base::RemoteConfig; use base::RemoteConfig;
use tcp_relay_client::relay_client::relay_client; use tcp_relay_client::relay_client::relay_client;
@@ -21,6 +23,35 @@ pub struct Args {
/// Listen address /// Listen address
#[clap(short, long, default_value = "127.0.0.1")] #[clap(short, long, default_value = "127.0.0.1")]
pub listen_address: String, pub listen_address: String,
/// Optional root certificate to use for server authentication
#[clap(short = 'c', long)]
pub root_certificate: Option<String>,
}
async fn get_server_config(config: &Args, root_cert: &Option<Vec<u8>>) -> Result<RemoteConfig, Box<dyn Error>> {
let url = format!("{}/config", config.relay_url);
log::info!("Retrieving configuration on {}", url);
let mut client = reqwest::Client::builder();
// Specify root certificate, if any was specified in the command line
if let Some(cert) = root_cert {
client = client.add_root_certificate(Certificate::from_pem(cert)?);
}
let client = client.build().expect("Failed to build reqwest client");
let req = client.get(url)
.header("Authorization", format!("Bearer {}", config.token))
.send()
.await?;
if req.status().as_u16() != 200 {
log::error!("Could not retrieve configuration! (got status {})", req.status());
std::process::exit(2);
}
Ok(req.json::<RemoteConfig>().await?)
} }
#[tokio::main] #[tokio::main]
@@ -30,19 +61,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Args = Args::parse(); let args: Args = Args::parse();
let args = Arc::new(args); let args = Arc::new(args);
let root_cert = args.root_certificate.as_ref().map(|c| std::fs::read(c)
.expect("Failed to read root certificate!"));
// Get server relay configuration (fetch the list of port to forward) // Get server relay configuration (fetch the list of port to forward)
let url = format!("{}/config", args.relay_url); let conf = get_server_config(&args, &root_cert).await?;
log::info!("Retrieving configuration on {}", url);
let req = reqwest::Client::new().get(url)
.header("Authorization", format!("Bearer {}", args.token))
.send()
.await?;
if req.status().as_u16() != 200 {
log::error!("Could not retrieve configuration! (got status {})", req.status());
std::process::exit(2);
}
let conf = req.json::<RemoteConfig>()
.await?;
// Start to listen port // Start to listen port
let mut handles = vec![]; let mut handles = vec![];
@@ -54,6 +77,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
args.relay_url, port.id, urlencoding::encode(&args.token)) args.relay_url, port.id, urlencoding::encode(&args.token))
.replace("http", "ws"), .replace("http", "ws"),
listen_address, listen_address,
root_cert.clone(),
)); ));
handles.push(h); handles.push(h);
} }

View File

@@ -1,9 +1,14 @@
use std::io::Cursor;
use std::sync::Arc;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use hyper_rustls::ConfigBuilderExt;
use rustls::{Certificate, RootCertStore};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::Message;
pub async fn relay_client(ws_url: String, listen_address: String) { pub async fn relay_client(ws_url: String, listen_address: String, root_cert: Option<Vec<u8>>) {
log::info!("Start to listen on {}", listen_address); log::info!("Start to listen on {}", listen_address);
let listener = match TcpListener::bind(&listen_address).await { let listener = match TcpListener::bind(&listen_address).await {
Ok(l) => l, Ok(l) => l,
@@ -17,7 +22,7 @@ pub async fn relay_client(ws_url: String, listen_address: String) {
let (socket, _) = listener.accept().await let (socket, _) = listener.accept().await
.expect("Failed to accept new connection!"); .expect("Failed to accept new connection!");
tokio::spawn(relay_connection(ws_url.clone(), socket)); tokio::spawn(relay_connection(ws_url.clone(), socket, root_cert.clone()));
} }
} }
@@ -25,10 +30,44 @@ pub async fn relay_client(ws_url: String, listen_address: String) {
/// ///
/// WS read => TCP write /// WS read => TCP write
/// TCP read => WS write /// TCP read => WS write
async fn relay_connection(ws_url: String, socket: TcpStream) { async fn relay_connection(ws_url: String, socket: TcpStream, root_cert: Option<Vec<u8>>) {
log::debug!("Connecting to {}...", ws_url); log::debug!("Connecting to {}...", ws_url);
let (ws_stream, _) = tokio_tungstenite::connect_async(ws_url)
.await.expect("Failed to connect to server relay!"); let ws_stream = if ws_url.starts_with("wss") {
let config = rustls::ClientConfig::builder()
.with_safe_defaults();
let config = match root_cert {
None => config.with_native_roots(),
Some(cert) => {
log::debug!("Using custom root certificates");
let mut store = RootCertStore::empty();
rustls_pemfile::certs(&mut Cursor::new(cert))
.expect("Failed to parse root certificates!")
.into_iter()
.map(Certificate)
.for_each(|c| store.add(&c).expect("Failed to add certificate to chain!"));
config.with_root_certificates(store)
}
};
let config = config.with_no_client_auth();
let connector = tokio_tungstenite::Connector::Rustls(Arc::new(config));
let (ws_stream, _) = tokio_tungstenite::connect_async_tls_with_config(
ws_url,
None,
Some(connector))
.await.expect("Failed to connect to server relay!");
ws_stream
} else {
let (ws_stream, _) = tokio_tungstenite::connect_async(ws_url)
.await.expect("Failed to connect to server relay!");
ws_stream
};
let (mut tcp_read, mut tcp_write) = socket.into_split(); let (mut tcp_read, mut tcp_write) = socket.into_split();

View File

@@ -9,8 +9,11 @@ clap = { version = "3.2.18", features = ["derive", "env"] }
log = "0.4.17" log = "0.4.17"
env_logger = "0.9.0" env_logger = "0.9.0"
actix = "0.13.0" actix = "0.13.0"
actix-web = "4" actix-web = { version = "4", features = ["rustls"] }
actix-web-actors = "4.1.0" actix-web-actors = "4.1.0"
actix-tls = "3.0.3"
serde = { version = "1.0.144", features = ["derive"] } serde = { version = "1.0.144", features = ["derive"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
futures = "0.3.24" futures = "0.3.24"
rustls = "0.20.6"
rustls-pemfile = "1.0.1"

View File

@@ -2,8 +2,9 @@ use clap::Parser;
/// TCP relay server /// TCP relay server
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about,
pub struct Args { long_about = "TCP-over-HTTP server. This program might be configured behind a reverse-proxy.")]
pub struct ProgramArgs {
/// Access tokens /// Access tokens
#[clap(short, long)] #[clap(short, long)]
pub tokens: Vec<String>, pub tokens: Vec<String>,
@@ -28,4 +29,12 @@ pub struct Args {
/// on the same machine /// on the same machine
#[clap(short, long, default_value_t = 0)] #[clap(short, long, default_value_t = 0)]
pub increment_ports: u16, pub increment_ports: u16,
/// TLS certificate. Specify also private key to use HTTPS/TLS instead of HTTP
#[clap(long)]
pub tls_cert: Option<String>,
/// TLS private key. Specify also certificate to use HTTPS/TLS instead of HTTP
#[clap(long)]
pub tls_key: Option<String>,
} }

View File

@@ -1,18 +1,22 @@
use std::fs::File;
use std::io::BufReader;
use std::sync::Arc; use std::sync::Arc;
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, middleware, Responder, web}; use actix_web::{App, HttpRequest, HttpResponse, HttpServer, middleware, Responder, web};
use actix_web::web::Data; use actix_web::web::Data;
use clap::Parser; use clap::Parser;
use rustls::{Certificate, PrivateKey, ServerConfig};
use rustls_pemfile::{certs, Item, read_one};
use base::RelayedPort; use base::RelayedPort;
use tcp_relay_server::args::Args; use tcp_relay_server::args::ProgramArgs;
use tcp_relay_server::relay_ws::relay_ws; use tcp_relay_server::relay_ws::relay_ws;
pub async fn hello_route() -> &'static str { pub async fn hello_route() -> &'static str {
"Hello world!" "Hello world!"
} }
pub async fn config_route(req: HttpRequest, data: Data<Arc<Args>>) -> impl Responder { pub async fn config_route(req: HttpRequest, data: Data<Arc<ProgramArgs>>) -> impl Responder {
let token = req.headers().get("Authorization") let token = req.headers().get("Authorization")
.map(|t| t.to_str().unwrap_or_default()) .map(|t| t.to_str().unwrap_or_default())
.unwrap_or_default() .unwrap_or_default()
@@ -35,13 +39,49 @@ pub async fn config_route(req: HttpRequest, data: Data<Arc<Args>>) -> impl Respo
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let mut args: Args = Args::parse(); let mut args: ProgramArgs = ProgramArgs::parse();
if args.ports.is_empty() { if args.ports.is_empty() {
log::error!("No port to forward!"); log::error!("No port to forward!");
std::process::exit(2); std::process::exit(2);
} }
// Load TLS configuration, if any
let tls_config = if let (Some(cert), Some(key)) = (&args.tls_cert, &args.tls_key) {
// Load TLS certificate & private key
let cert_file = &mut BufReader::new(File::open(cert).unwrap());
let key_file = &mut BufReader::new(File::open(key).unwrap());
// Get certificates chain
let cert_chain = certs(cert_file).unwrap()
.into_iter()
.map(Certificate)
.collect();
// Get private key
let key = match read_one(key_file).expect("Failed to read private key!") {
None => {
log::error!("Failed to extract private key!");
panic!();
}
Some(Item::PKCS8Key(key)) => key,
Some(Item::RSAKey(key)) => key,
_ => {
log::error!("Unsupported private key type!");
panic!();
}
};
let config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(cert_chain, PrivateKey(key))
.expect("Failed to load TLS certificate!");
Some(config)
} else { None };
// Read tokens from file, if any // Read tokens from file, if any
if let Some(file) = &args.tokens_file { if let Some(file) = &args.tokens_file {
std::fs::read_to_string(file) std::fs::read_to_string(file)
@@ -60,15 +100,19 @@ async fn main() -> std::io::Result<()> {
let args = Arc::new(args); let args = Arc::new(args);
let args_clone = args.clone(); let args_clone = args.clone();
HttpServer::new(move || { let server = HttpServer::new(move || {
App::new() App::new()
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.app_data(Data::new(args_clone.clone())) .app_data(Data::new(args_clone.clone()))
.route("/", web::get().to(hello_route)) .route("/", web::get().to(hello_route))
.route("/config", web::get().to(config_route)) .route("/config", web::get().to(config_route))
.route("/ws", web::get().to(relay_ws)) .route("/ws", web::get().to(relay_ws))
}) });
.bind(&args.listen_address)?
.run() if let Some(tls_conf) = tls_config {
server.bind_rustls(&args.listen_address, tls_conf)?
} else {
server.bind(&args.listen_address)?
}.run()
.await .await
} }

View File

@@ -9,7 +9,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::args::Args; use crate::args::ProgramArgs;
/// How often heartbeat pings are sent /// How often heartbeat pings are sent
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
@@ -153,7 +153,7 @@ pub struct WebSocketQuery {
pub async fn relay_ws(req: HttpRequest, stream: web::Payload, pub async fn relay_ws(req: HttpRequest, stream: web::Payload,
query: web::Query<WebSocketQuery>, query: web::Query<WebSocketQuery>,
conf: web::Data<Arc<Args>>) -> Result<HttpResponse, Error> { conf: web::Data<Arc<ProgramArgs>>) -> Result<HttpResponse, Error> {
if !conf.tokens.contains(&query.token) { if !conf.tokens.contains(&query.token) {
log::error!("Rejected WS request from {:?} due to invalid token!", req.peer_addr()); log::error!("Rejected WS request from {:?} due to invalid token!", req.peer_addr());
return Ok(HttpResponse::Unauthorized().json("Invalid / missing token!")); return Ok(HttpResponse::Unauthorized().json("Invalid / missing token!"));