Compare commits

..

10 Commits

20 changed files with 954 additions and 140 deletions

3
.gitignore vendored
View File

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

335
Cargo.lock generated
View File

@@ -52,6 +52,7 @@ dependencies = [
"actix-codec",
"actix-rt",
"actix-service",
"actix-tls",
"actix-utils",
"ahash",
"base64",
@@ -143,6 +144,24 @@ dependencies = [
"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]]
name = "actix-utils"
version = "3.0.0"
@@ -166,6 +185,7 @@ dependencies = [
"actix-rt",
"actix-server",
"actix-service",
"actix-tls",
"actix-utils",
"actix-web-codegen",
"ahash",
@@ -274,6 +294,45 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "asn1-rs"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf6690c370453db30743b373a60ba498fc0d6d83b11f4abfd87a84a075db5dd4"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror",
"time",
]
[[package]]
name = "asn1-rs-derive"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atty"
version = "0.2.14"
@@ -295,6 +354,8 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
name = "base"
version = "0.1.0"
dependencies = [
"rustls",
"rustls-pemfile",
"serde",
]
@@ -502,6 +563,26 @@ dependencies = [
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "der-parser"
version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d4bc9b0db0a0df9ae64634ac5bdefb7afcb534e182275ca0beadbe486701c1"
dependencies = [
"asn1-rs",
"displaydoc",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@@ -525,6 +606,17 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "encoding_rs"
version = "0.8.31"
@@ -817,6 +909,21 @@ dependencies = [
"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]]
name = "hyper-tls"
version = "0.5.0"
@@ -963,6 +1070,12 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.5.3"
@@ -1002,6 +1115,46 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nom"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.13.1"
@@ -1021,6 +1174,15 @@ dependencies = [
"libc",
]
[[package]]
name = "oid-registry"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d4bda43fd1b844cbc6e6e54b5444e2b1bc7838bce59ad205902cccbb26d6761"
dependencies = [
"asn1-rs",
]
[[package]]
name = "once_cell"
version = "1.13.1"
@@ -1107,6 +1269,15 @@ version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22"
[[package]]
name = "pem"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
dependencies = [
"base64",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@@ -1259,6 +1430,7 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
@@ -1268,19 +1440,38 @@ dependencies = [
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"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]]
name = "rustc_version"
version = "0.4.0"
@@ -1290,6 +1481,48 @@ dependencies = [
"semver",
]
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom",
]
[[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]]
name = "ryu"
version = "1.0.11"
@@ -1312,6 +1545,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "security-framework"
version = "2.7.0"
@@ -1440,6 +1683,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "strsim"
version = "0.10.0"
@@ -1457,16 +1706,31 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "tcp_relay_client"
version = "0.1.0"
dependencies = [
"base",
"bytes",
"clap",
"env_logger",
"futures",
"hyper-rustls",
"log",
"reqwest",
"rustls",
"tokio",
"tokio-tungstenite",
"urlencoding",
@@ -1477,6 +1741,7 @@ name = "tcp_relay_server"
version = "0.1.0"
dependencies = [
"actix",
"actix-tls",
"actix-web",
"actix-web-actors",
"base",
@@ -1484,8 +1749,12 @@ dependencies = [
"env_logger",
"futures",
"log",
"pem",
"rustls",
"serde",
"tokio",
"webpki",
"x509-parser",
]
[[package]]
@@ -1612,6 +1881,17 @@ dependencies = [
"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]]
name = "tokio-tungstenite"
version = "0.17.2"
@@ -1620,8 +1900,12 @@ checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-native-certs",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki",
]
[[package]]
@@ -1684,10 +1968,12 @@ dependencies = [
"httparse",
"log",
"rand",
"rustls",
"sha-1",
"thiserror",
"url",
"utf-8",
"webpki",
]
[[package]]
@@ -1717,6 +2003,18 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-xid"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.2.2"
@@ -1845,6 +2143,25 @@ dependencies = [
"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]]
name = "winapi"
version = "0.3.9"
@@ -1928,6 +2245,24 @@ dependencies = [
"winapi",
]
[[package]]
name = "x509-parser"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8"
dependencies = [
"asn1-rs",
"base64",
"data-encoding",
"der-parser",
"lazy_static",
"nom",
"oid-registry",
"rusticata-macros",
"thiserror",
"time",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"

View File

@@ -6,17 +6,24 @@ This project aims to provide an easy-to-setup TCP forwarding solution:
| | | Client | | Server | | |
| Client | -- TCP xx -- | | -- HTTP 80 / 443 -- | | -- TCP xx -- | Server |
| | | Relay | | Relay | | |
|--------|   |--------| |--------| |--------|
|--------| |--------| |--------| |--------|
```
This project can be used especially to bypass firewalls that blocks traffics
from ports others than the 80 / 443 duo.
from ports others than the HTTP / HTTPS ports.
## Authentication
The client can authenticate agains the server relays through two different means:
* Using a token
* Using a client TLS certificate. In this case, the server relay must act as a HTTPS server, and you must provide the
server the required certificates / key files in PEM format. It is also possible to provide the server a CRL file.
## Binaries
This repository contains two binaries:
* `tpc_relay_server`: The server relay
* `tpc_relay_server`: The server relay. In case of token authentication, it can be put behind a reverse proxy.
* `tcp_relay_client`: The client relay
The clients relay authenticates itself to the server using a token.
A single server - client relay pair can relay multiple ports simultaneously from the same machine.

View File

@@ -4,4 +4,6 @@ version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.144", features = ["derive"] }
serde = { version = "1.0.144", features = ["derive"] }
rustls-pemfile = "1.0.1"
rustls = "0.20.6"

37
base/src/cert_utils.rs Normal file
View File

@@ -0,0 +1,37 @@
use std::error::Error;
use std::io::{Cursor, ErrorKind};
use rustls::{Certificate, PrivateKey};
use rustls_pemfile::{read_one, Item};
/// Parse PEM certificates bytes into a [`rustls::Certificate`] structure
pub fn parse_pem_certificates(certs: &[u8]) -> Result<Vec<Certificate>, Box<dyn Error>> {
Ok(rustls_pemfile::certs(&mut Cursor::new(certs))?
.into_iter()
.map(Certificate)
.collect())
}
/// Parse PEM private key bytes into a [`rustls::PrivateKey`] structure
pub fn parse_pem_private_key(privkey: &[u8]) -> Result<PrivateKey, Box<dyn Error>> {
let key = match read_one(&mut Cursor::new(privkey))? {
None => {
Err(std::io::Error::new(
ErrorKind::Other,
"Failed to extract private key!",
))?;
unreachable!()
}
Some(Item::PKCS8Key(key)) => key,
Some(Item::RSAKey(key)) => key,
_ => {
Err(std::io::Error::new(
ErrorKind::Other,
"Unsupported private key type!",
))?;
unreachable!();
}
};
Ok(PrivateKey(key))
}

View File

@@ -1,7 +1,4 @@
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)]
pub struct RelayedPort {
pub id: usize,
pub port: u16,
}
pub mod cert_utils;
mod structs;
pub type RemoteConfig = Vec<RelayedPort>;
pub use structs::{RelayedPort, RemoteConfig};

7
base/src/structs.rs Normal file
View File

@@ -0,0 +1,7 @@
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)]
pub struct RelayedPort {
pub id: usize,
pub port: u16,
}
pub type RemoteConfig = Vec<RelayedPort>;

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

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

View File

@@ -0,0 +1,100 @@
use bytes::BufMut;
use clap::Parser;
/// TCP relay client
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
pub struct ClientConfig {
/// Access token
#[clap(short, long)]
pub token: Option<String>,
/// Relay server
#[clap(short, long, default_value = "http://127.0.0.1:8000")]
pub relay_url: String,
/// Listen address
#[clap(short, long, default_value = "127.0.0.1")]
pub listen_address: String,
/// Alternative root certificate to use for server authentication
#[clap(short = 'c', long)]
pub root_certificate: Option<String>,
#[clap(skip)]
_root_certificate_cache: Option<Vec<u8>>,
/// TLS certificate for TLS authentication.
#[clap(long)]
pub tls_cert: Option<String>,
#[clap(skip)]
_tls_cert_cache: Option<Vec<u8>>,
/// TLS key for TLS authentication.
#[clap(long)]
pub tls_key: Option<String>,
#[clap(skip)]
_tls_key_cache: Option<Vec<u8>>,
}
impl ClientConfig {
/// Load certificates and put them in cache
pub fn load_certificates(&mut self) {
self._root_certificate_cache = self
.root_certificate
.as_ref()
.map(|c| std::fs::read(c).expect("Failed to read root certificate!"));
self._tls_cert_cache = self
.tls_cert
.as_ref()
.map(|c| std::fs::read(c).expect("Failed to read client certificate!"));
self._tls_key_cache = self
.tls_key
.as_ref()
.map(|c| std::fs::read(c).expect("Failed to read client key!"));
}
/// Get client token, returning a dummy token if none was specified
pub fn get_auth_token(&self) -> &str {
self.token.as_deref().unwrap_or("none")
}
/// Get root certificate content
pub fn get_root_certificate(&self) -> Option<Vec<u8>> {
self._root_certificate_cache.clone()
}
/// Get client certificate & key pair, if available
pub fn get_client_keypair(&self) -> Option<(&Vec<u8>, &Vec<u8>)> {
if let (Some(cert), Some(key)) = (&self._tls_cert_cache, &self._tls_key_cache) {
Some((cert, key))
} else {
None
}
}
/// Get client certificate & key pair, in a single memory buffer
pub fn get_merged_client_keypair(&self) -> Option<Vec<u8>> {
self.get_client_keypair().map(|(c, k)| {
let mut out = k.to_vec();
out.put_slice("\n".as_bytes());
out.put_slice(c);
out
})
}
}
#[cfg(test)]
mod test {
use crate::client_config::ClientConfig;
#[test]
fn verify_cli() {
use clap::CommandFactory;
ClientConfig::command().debug_assert()
}
}

View File

@@ -1 +1,2 @@
pub mod relay_client;
pub mod client_config;
pub mod relay_client;

View File

@@ -1,48 +1,76 @@
extern crate core;
use std::error::Error;
use std::sync::Arc;
use clap::Parser;
use futures::future::join_all;
use reqwest::{Certificate, Identity};
use base::RemoteConfig;
use tcp_relay_client::client_config::ClientConfig;
use tcp_relay_client::relay_client::relay_client;
/// TCP relay client
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
/// Access token
#[clap(short, long)]
pub token: String,
/// Relay server
#[clap(short, long, default_value = "http://127.0.0.1:8000")]
pub relay_url: String,
/// Listen address
#[clap(short, long, default_value = "127.0.0.1")]
pub listen_address: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let args: Args = Args::parse();
let args = Arc::new(args);
// Get server relay configuration (fetch the list of port to forward)
let url = format!("{}/config", args.relay_url);
async fn get_server_config(config: &ClientConfig) -> Result<RemoteConfig, Box<dyn Error>> {
let url = format!("{}/config", config.relay_url);
log::info!("Retrieving configuration on {}", url);
let req = reqwest::Client::new().get(url)
.header("Authorization", format!("Bearer {}", args.token))
let mut client = reqwest::Client::builder();
// Specify root certificate, if any was specified in the command line
if let Some(cert) = config.get_root_certificate() {
client = client.add_root_certificate(Certificate::from_pem(&cert)?);
}
// Specify client certificate, if any
if let Some(kp) = config.get_merged_client_keypair() {
let identity = Identity::from_pem(&kp).expect("Failed to load certificates for reqwest!");
client = client.identity(identity).use_rustls_tls();
}
let client = client.build().expect("Failed to build reqwest client");
let req = client
.get(url)
.header(
"Authorization",
format!("Bearer {}", config.get_auth_token()),
)
.send()
.await?;
if req.status().as_u16() != 200 {
log::error!("Could not retrieve configuration! (got status {})", req.status());
log::error!(
"Could not retrieve configuration! (got status {})",
req.status()
);
std::process::exit(2);
}
let conf = req.json::<RemoteConfig>()
.await?;
Ok(req.json::<RemoteConfig>().await?)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let mut args: ClientConfig = ClientConfig::parse();
args.load_certificates();
let args = Arc::new(args);
// Check arguments coherence
if args.tls_cert.is_some() != args.tls_key.is_some() {
log::error!(
"If you specify one of TLS certificate / key, you must then specify the other!"
);
panic!();
}
if args.get_client_keypair().is_some() {
log::info!("Using client-side authentication");
}
// Get server relay configuration (fetch the list of port to forward)
let conf = get_server_config(&args).await?;
// Start to listen port
let mut handles = vec![];
@@ -50,10 +78,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listen_address = format!("{}:{}", args.listen_address, port.port);
let h = tokio::spawn(relay_client(
format!("{}/ws?id={}&token={}",
args.relay_url, port.id, urlencoding::encode(&args.token))
.replace("http", "ws"),
format!(
"{}/ws?id={}&token={}",
args.relay_url,
port.id,
urlencoding::encode(args.get_auth_token())
)
.replace("http", "ws"),
listen_address,
args.clone(),
));
handles.push(h);
}
@@ -61,4 +94,4 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
join_all(handles).await;
Ok(())
}
}

View File

@@ -1,9 +1,17 @@
use std::sync::Arc;
use futures::{SinkExt, StreamExt};
use hyper_rustls::ConfigBuilderExt;
use rustls::RootCertStore;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::tungstenite::Message;
pub async fn relay_client(ws_url: String, listen_address: String) {
use base::cert_utils;
use crate::client_config::ClientConfig;
pub async fn relay_client(ws_url: String, listen_address: String, config: Arc<ClientConfig>) {
log::info!("Start to listen on {}", listen_address);
let listener = match TcpListener::bind(&listen_address).await {
Ok(l) => l,
@@ -14,10 +22,12 @@ pub async fn relay_client(ws_url: String, listen_address: String) {
};
loop {
let (socket, _) = listener.accept().await
let (socket, _) = listener
.accept()
.await
.expect("Failed to accept new connection!");
tokio::spawn(relay_connection(ws_url.clone(), socket));
tokio::spawn(relay_connection(ws_url.clone(), socket, config.clone()));
}
}
@@ -25,15 +35,58 @@ pub async fn relay_client(ws_url: String, listen_address: String) {
///
/// WS read => TCP write
/// TCP read => WS write
async fn relay_connection(ws_url: String, socket: TcpStream) {
async fn relay_connection(ws_url: String, socket: TcpStream, conf: Arc<ClientConfig>) {
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 conf.get_root_certificate() {
None => config.with_native_roots(),
Some(cert) => {
log::debug!("Using custom root certificates");
let mut store = RootCertStore::empty();
cert_utils::parse_pem_certificates(&cert)
.unwrap()
.iter()
.for_each(|c| store.add(c).expect("Failed to add certificate to chain!"));
config.with_root_certificates(store)
}
};
let config = match conf.get_client_keypair() {
None => config.with_no_client_auth(),
Some((certs, key)) => {
let certs = cert_utils::parse_pem_certificates(certs)
.expect("Failed to parse client certificate!");
let key = cert_utils::parse_pem_private_key(key)
.expect("Failed to parse client auth private key!");
config
.with_single_cert(certs, key)
.expect("Failed to set client certificate!")
}
};
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 ws_write, mut ws_read) =
ws_stream.split();
let (mut ws_write, mut ws_read) = ws_stream.split();
// TCP read -> WS write
let future = async move {
@@ -68,12 +121,18 @@ async fn relay_connection(ws_url: String, socket: TcpStream) {
while let Some(m) = ws_read.next().await {
match m {
Err(e) => {
log::error!("Failed to read from WebSocket. Breaking read loop... {:?}", e);
log::error!(
"Failed to read from WebSocket. Breaking read loop... {:?}",
e
);
break;
}
Ok(Message::Binary(b)) => {
if let Err(e) = tcp_write.write_all(&b).await {
log::error!("Failed to forward message to websocket. Closing reading end... {:?}", e);
log::error!(
"Failed to forward message to websocket. Closing reading end... {:?}",
e
);
break;
};
}
@@ -81,7 +140,7 @@ async fn relay_connection(ws_url: String, socket: TcpStream) {
log::info!("Server asked to close this WebSocket connection");
break;
}
Ok(m) => log::info!("{:?}", m)
Ok(m) => log::info!("{:?}", m),
}
}
}
}

View File

@@ -9,8 +9,13 @@ clap = { version = "3.2.18", features = ["derive", "env"] }
log = "0.4.17"
env_logger = "0.9.0"
actix = "0.13.0"
actix-web = "4"
actix-web = { version = "4", features = ["rustls"] }
actix-web-actors = "4.1.0"
actix-tls = "3.0.3"
serde = { version = "1.0.144", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
futures = "0.3.24"
futures = "0.3.24"
rustls = "0.20.6"
webpki = "0.22.0"
x509-parser = "0.14.0"
pem = "1.1.0"

View File

@@ -1,31 +0,0 @@
use clap::Parser;
/// TCP relay server
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
/// Access tokens
#[clap(short, long)]
pub tokens: Vec<String>,
/// Access tokens stored in a file, one token per line
#[clap(long)]
pub tokens_file: Option<String>,
/// Forwarded ports
#[clap(short, long)]
pub ports: Vec<u16>,
/// Upstream server
#[clap(short, long, default_value = "127.0.0.1")]
pub upstream_server: String,
/// HTTP server listen address
#[clap(short, long, default_value = "0.0.0.0:8000")]
pub listen_address: String,
/// Increment ports on client. Useful for debugging and running both client and server
/// on the same machine
#[clap(short, long, default_value_t = 0)]
pub increment_ports: u16,
}

View File

@@ -1,2 +1,3 @@
pub mod args;
pub mod relay_ws;
pub mod relay_ws;
pub mod server_config;
pub mod tls_cert_client_verifier;

View File

@@ -1,33 +1,42 @@
use std::sync::Arc;
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, middleware, Responder, web};
use actix_web::web::Data;
use actix_web::{middleware, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use clap::Parser;
use base::RelayedPort;
use tcp_relay_server::args::Args;
use base::{cert_utils, RelayedPort};
use tcp_relay_server::relay_ws::relay_ws;
use tcp_relay_server::server_config::ServerConfig;
use tcp_relay_server::tls_cert_client_verifier::CustomCertClientVerifier;
pub async fn hello_route() -> &'static str {
"Hello world!"
}
pub async fn config_route(req: HttpRequest, data: Data<Arc<Args>>) -> impl Responder {
let token = req.headers().get("Authorization")
.map(|t| t.to_str().unwrap_or_default())
.unwrap_or_default()
.strip_prefix("Bearer ")
.unwrap_or_default();
pub async fn config_route(req: HttpRequest, data: Data<Arc<ServerConfig>>) -> impl Responder {
if data.has_token_auth() {
let token = req
.headers()
.get("Authorization")
.map(|t| t.to_str().unwrap_or_default())
.unwrap_or_default()
.strip_prefix("Bearer ")
.unwrap_or_default();
if !data.tokens.iter().any(|t| t.eq(token)) {
return HttpResponse::Unauthorized().json("Missing / invalid token");
if !data.tokens.iter().any(|t| t.eq(token)) {
return HttpResponse::Unauthorized().json("Missing / invalid token");
}
}
HttpResponse::Ok().json(
data.ports.iter()
data.ports
.iter()
.enumerate()
.map(|(id, port)| RelayedPort { id, port: port + data.increment_ports })
.collect::<Vec<_>>()
.map(|(id, port)| RelayedPort {
id,
port: port + data.increment_ports,
})
.collect::<Vec<_>>(),
)
}
@@ -35,8 +44,9 @@ pub async fn config_route(req: HttpRequest, data: Data<Arc<Args>>) -> impl Respo
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let mut args: Args = Args::parse();
let mut args: ServerConfig = ServerConfig::parse();
// Check if no port are to be forwarded
if args.ports.is_empty() {
log::error!("No port to forward!");
std::process::exit(2);
@@ -51,24 +61,66 @@ async fn main() -> std::io::Result<()> {
.for_each(|t| args.tokens.push(t.to_string()));
}
if args.tokens.is_empty() {
log::error!("No tokens specified!");
if !args.has_auth() {
log::error!("No authentication method specified!");
std::process::exit(3);
}
log::info!("Starting relay on http://{}", args.listen_address);
if args.has_tls_client_auth() && !args.has_tls_config() {
log::error!("Cannot provide client auth without TLS configuration!");
panic!();
}
let args = Arc::new(args);
// 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 = std::fs::read(cert).expect("Failed to read certificate file");
let key_file = std::fs::read(key).expect("Failed to read server private key");
// Get certificates chain
let cert_chain =
cert_utils::parse_pem_certificates(&cert_file).expect("Failed to extract certificates");
// Get private key
let key =
cert_utils::parse_pem_private_key(&key_file).expect("Failed to extract private key!");
let config = rustls::ServerConfig::builder().with_safe_defaults();
let config = match args.has_tls_client_auth() {
true => config
.with_client_cert_verifier(Arc::new(CustomCertClientVerifier::new(args.clone()))),
false => config.with_no_client_auth(),
};
let config = config
.with_single_cert(cert_chain, key)
.expect("Failed to load TLS certificate!");
Some(config)
} else {
None
};
log::info!("Starting relay on http://{}", args.listen_address);
let args_clone = args.clone();
HttpServer::new(move || {
let server = HttpServer::new(move || {
App::new()
.wrap(middleware::Logger::default())
.app_data(Data::new(args_clone.clone()))
.route("/", web::get().to(hello_route))
.route("/config", web::get().to(config_route))
.route("/ws", web::get().to(relay_ws))
})
.bind(&args.listen_address)?
.run()
.await
}
});
if let Some(tls_conf) = tls_config {
server.bind_rustls(&args.listen_address, tls_conf)?
} else {
server.bind(&args.listen_address)?
}
.run()
.await
}

View File

@@ -2,14 +2,14 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use actix::{Actor, ActorContext, AsyncContext, Handler, Message, StreamHandler};
use actix_web::{Error, HttpRequest, HttpResponse, web};
use actix_web::{web, Error, HttpRequest, HttpResponse};
use actix_web_actors::ws;
use actix_web_actors::ws::{CloseCode, CloseReason};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::TcpStream;
use crate::args::Args;
use crate::server_config::ServerConfig;
/// How often heartbeat pings are sent
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
@@ -32,7 +32,6 @@ struct RelayWS {
// Client must respond to ping at a specific interval, otherwise we drop connection
hb: Instant,
// TODO : handle socket close
}
@@ -94,7 +93,6 @@ impl Actor for RelayWS {
}
log::info!("Exited read loop");
// TODO : notify context
};
tokio::spawn(future);
@@ -110,7 +108,9 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for RelayWS {
Ok(ws::Message::Text(text)) => ctx.text(text),
Ok(ws::Message::Close(_reason)) => ctx.stop(),
Ok(ws::Message::Binary(data)) => {
if let Err(e) = futures::executor::block_on(self.tcp_write.write_all(&data.to_vec())) {
if let Err(e) =
futures::executor::block_on(self.tcp_write.write_all(&data.to_vec()))
{
log::error!("Failed to forward some data, closing connection! {:?}", e);
ctx.stop();
}
@@ -148,19 +148,33 @@ impl Handler<TCPReadEndClosed> for RelayWS {
#[derive(serde::Deserialize)]
pub struct WebSocketQuery {
id: usize,
token: String,
token: Option<String>,
}
pub async fn relay_ws(req: HttpRequest, stream: web::Payload,
query: web::Query<WebSocketQuery>,
conf: web::Data<Arc<Args>>) -> Result<HttpResponse, Error> {
if !conf.tokens.contains(&query.token) {
log::error!("Rejected WS request from {:?} due to invalid token!", req.peer_addr());
pub async fn relay_ws(
req: HttpRequest,
stream: web::Payload,
query: web::Query<WebSocketQuery>,
conf: web::Data<Arc<ServerConfig>>,
) -> Result<HttpResponse, Error> {
if conf.has_token_auth()
&& !conf
.tokens
.iter()
.any(|t| t == query.token.as_deref().unwrap_or_default())
{
log::error!(
"Rejected WS request from {:?} due to invalid token!",
req.peer_addr()
);
return Ok(HttpResponse::Unauthorized().json("Invalid / missing token!"));
}
if conf.ports.len() <= query.id {
log::error!("Rejected WS request from {:?} due to invalid port number!", req.peer_addr());
log::error!(
"Rejected WS request from {:?} due to invalid port number!",
req.peer_addr()
);
return Ok(HttpResponse::BadRequest().json("Invalid port number!"));
}
@@ -169,14 +183,24 @@ pub async fn relay_ws(req: HttpRequest, stream: web::Payload,
let (tcp_read, tcp_write) = match TcpStream::connect(&upstream_addr).await {
Ok(s) => s.into_split(),
Err(e) => {
log::error!("Failed to establish connection with upstream server! {:?}", e);
return Ok(HttpResponse::InternalServerError()
.json("Failed to establish connection!"));
log::error!(
"Failed to establish connection with upstream server! {:?}",
e
);
return Ok(HttpResponse::InternalServerError().json("Failed to establish connection!"));
}
};
let relay = RelayWS { tcp_read: Some(tcp_read), tcp_write, hb: Instant::now() };
let relay = RelayWS {
tcp_read: Some(tcp_read),
tcp_write,
hb: Instant::now(),
};
let resp = ws::start(relay, &req, stream);
log::info!("Opening new WS connection for {:?} to {}", req.peer_addr(), upstream_addr);
log::info!(
"Opening new WS connection for {:?} to {}",
req.peer_addr(),
upstream_addr
);
resp
}
}

View File

@@ -0,0 +1,73 @@
use clap::Parser;
/// TCP relay server
#[derive(Parser, Debug, Clone)]
#[clap(
author,
version,
about,
long_about = "TCP-over-HTTP server. This program might be configured behind a reverse-proxy."
)]
pub struct ServerConfig {
/// Access tokens
#[clap(short, long)]
pub tokens: Vec<String>,
/// Access tokens stored in a file, one token per line
#[clap(long)]
pub tokens_file: Option<String>,
/// Forwarded ports
#[clap(short, long)]
pub ports: Vec<u16>,
/// Upstream server
#[clap(short, long, default_value = "127.0.0.1")]
pub upstream_server: String,
/// HTTP server listen address
#[clap(short, long, default_value = "0.0.0.0:8000")]
pub listen_address: String,
/// Increment ports on client. Useful for debugging and running both client and server
/// on the same machine
#[clap(short, long, default_value_t = 0)]
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>,
/// Restrict TLS client authentication to certificates signed directly or indirectly by the
/// provided root certificates
///
/// This option automatically enable TLS client authentication
#[clap(long)]
pub tls_client_auth_root_cert: Option<String>,
/// TLS client authentication revocation list (CRL file)
#[clap(long)]
pub tls_revocation_list: Option<String>,
}
impl ServerConfig {
pub fn has_token_auth(&self) -> bool {
!self.tokens.is_empty()
}
pub fn has_tls_config(&self) -> bool {
self.tls_cert.is_some() && self.tls_key.is_some()
}
pub fn has_tls_client_auth(&self) -> bool {
self.tls_client_auth_root_cert.is_some()
}
pub fn has_auth(&self) -> bool {
self.has_token_auth() || self.has_tls_client_auth()
}
}

View File

@@ -0,0 +1,103 @@
use std::sync::Arc;
use std::time::SystemTime;
use rustls::internal::msgs::enums::AlertDescription;
use rustls::server::{AllowAnyAuthenticatedClient, ClientCertVerified, ClientCertVerifier};
use rustls::{Certificate, DistinguishedNames, Error, RootCertStore};
use x509_parser::prelude::{CertificateRevocationList, FromDer, X509Certificate};
use base::cert_utils::parse_pem_certificates;
use crate::server_config::ServerConfig;
pub struct CustomCertClientVerifier {
upstream_cert_verifier: Box<Arc<dyn ClientCertVerifier>>,
crl: Option<Vec<u8>>,
}
impl CustomCertClientVerifier {
pub fn new(conf: Arc<ServerConfig>) -> Self {
// Build root certifications list
let cert_path = conf
.tls_client_auth_root_cert
.as_deref()
.expect("No root certificates for client authentication provided!");
let cert_file = std::fs::read(cert_path)
.expect("Failed to read root certificates for client authentication!");
let root_certs = parse_pem_certificates(&cert_file)
.expect("Failed to read root certificates for server authentication!");
if root_certs.is_empty() {
log::error!("No certificates found for client authentication!");
panic!();
}
let mut store = RootCertStore::empty();
for cert in root_certs {
store
.add(&cert)
.expect("Failed to add certificate to root store");
}
// Parse CRL file (if any)
let crl = if let Some(crl_file) = &conf.tls_revocation_list {
let crl_file = std::fs::read(crl_file).expect("Failed to open / read CRL file!");
let parsed_crl = pem::parse(crl_file).expect("Failed to decode CRL file!");
Some(parsed_crl.contents)
} else {
None
};
Self {
upstream_cert_verifier: Box::new(AllowAnyAuthenticatedClient::new(store)),
crl,
}
}
}
impl ClientCertVerifier for CustomCertClientVerifier {
fn offer_client_auth(&self) -> bool {
true
}
fn client_auth_mandatory(&self) -> Option<bool> {
Some(true)
}
fn client_auth_root_subjects(&self) -> Option<DistinguishedNames> {
Some(vec![])
}
fn verify_client_cert(
&self,
end_entity: &Certificate,
intermediates: &[Certificate],
now: SystemTime,
) -> Result<ClientCertVerified, Error> {
// Check the certificates sent by the client has been revoked
if let Some(crl) = &self.crl {
let (_rem, crl) =
CertificateRevocationList::from_der(crl).expect("Failed to read CRL!");
let (_rem, cert) =
X509Certificate::from_der(&end_entity.0).expect("Failed to read certificate!");
for revoked in crl.iter_revoked_certificates() {
if revoked.user_certificate == cert.serial {
log::error!(
"Client attempted to use a revoked certificate! Serial={:?} Subject={}",
cert.serial,
cert.subject
);
return Err(Error::AlertReceived(AlertDescription::CertificateRevoked));
}
}
}
self.upstream_cert_verifier
.verify_client_cert(end_entity, intermediates, now)
}
}