start, reverse guard, cli-frontend for server and client
This commit is contained in:
17
ostp/Cargo.toml
Normal file
17
ostp/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "ostp"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio.workspace = true
|
||||
chacha20poly1305.workspace = true
|
||||
x25519-dalek.workspace = true
|
||||
bytes.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
hmac.workspace = true
|
||||
sha2.workspace = true
|
||||
rand.workspace = true
|
||||
uuid.workspace = true
|
||||
159
ostp/src/client.rs
Normal file
159
ostp/src/client.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
//! OSTP Client - Stealth tunnel initiator
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::crypto::{AeadCipher, KeyExchange, PskValidator};
|
||||
use crate::mimicry::{MimicryEngine, TlsHelloBuilder};
|
||||
use crate::uot::{encode_frame, decode_frame, FrameFlags};
|
||||
use bytes::BytesMut;
|
||||
|
||||
/// Client configuration
|
||||
#[derive(Clone)]
|
||||
pub struct ClientConfig {
|
||||
pub server_addr: SocketAddr,
|
||||
pub psk: [u8; 32],
|
||||
pub country_code: String,
|
||||
}
|
||||
|
||||
impl ClientConfig {
|
||||
pub fn new(server: SocketAddr, psk: [u8; 32], country: impl Into<String>) -> Self {
|
||||
Self {
|
||||
server_addr: server,
|
||||
psk,
|
||||
country_code: country.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OSTP Client with TLS mimicry
|
||||
pub struct OstpClient {
|
||||
config: ClientConfig,
|
||||
psk_validator: PskValidator,
|
||||
mimicry: MimicryEngine,
|
||||
cipher: Option<AeadCipher>,
|
||||
}
|
||||
|
||||
impl OstpClient {
|
||||
pub fn new(config: ClientConfig) -> Self {
|
||||
let psk_validator = PskValidator::new(config.psk);
|
||||
Self {
|
||||
config,
|
||||
psk_validator,
|
||||
mimicry: MimicryEngine::new(),
|
||||
cipher: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to OSTP server with silent handshake
|
||||
pub async fn connect(&mut self) -> anyhow::Result<TcpStream> {
|
||||
let mut stream = TcpStream::connect(self.config.server_addr).await?;
|
||||
tracing::info!("Connected to {}", self.config.server_addr);
|
||||
|
||||
// Phase 1: Generate keypair and send PSK-signed handshake
|
||||
let kex = KeyExchange::new();
|
||||
let client_pubkey = *kex.public_key();
|
||||
|
||||
// Sign the public key with PSK
|
||||
let signature = self.psk_validator.sign(client_pubkey.as_bytes());
|
||||
|
||||
// Send: [32-byte signature][32-byte pubkey]
|
||||
stream.write_all(&signature).await?;
|
||||
stream.write_all(client_pubkey.as_bytes()).await?;
|
||||
|
||||
// Phase 2: Receive server's response
|
||||
let mut buf = [0u8; 64];
|
||||
let n = stream.read_exact(&mut buf).await?;
|
||||
if n < 64 {
|
||||
anyhow::bail!("Invalid server response");
|
||||
}
|
||||
|
||||
let server_sig: [u8; 32] = buf[..32].try_into()?;
|
||||
let server_pubkey_bytes: [u8; 32] = buf[32..64].try_into()?;
|
||||
|
||||
// Verify server's signature
|
||||
if !self.psk_validator.verify(&server_pubkey_bytes, &server_sig) {
|
||||
anyhow::bail!("Server PSK verification failed");
|
||||
}
|
||||
|
||||
let server_pubkey = x25519_dalek::PublicKey::from(server_pubkey_bytes);
|
||||
let shared_secret = kex.derive_shared(&server_pubkey);
|
||||
|
||||
// Derive session key
|
||||
let session_key = crate::crypto::derive_session_key(&shared_secret, b"ostp-session-v1");
|
||||
self.cipher = Some(AeadCipher::new(&session_key));
|
||||
|
||||
tracing::info!("Session established with server");
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Send encrypted data through the tunnel
|
||||
pub async fn send(&mut self, stream: &mut TcpStream, data: &[u8]) -> anyhow::Result<()> {
|
||||
let cipher = self.cipher.as_mut().ok_or_else(|| anyhow::anyhow!("Not connected"))?;
|
||||
|
||||
let encrypted = cipher.encrypt(data).map_err(|_| anyhow::anyhow!("Encryption failed"))?;
|
||||
let padding = rand::random::<usize>() % 64; // Random padding 0-63 bytes
|
||||
let frame = encode_frame(&encrypted, FrameFlags::DATA, padding)?;
|
||||
|
||||
stream.write_all(&frame).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive and decrypt data from the tunnel
|
||||
pub async fn recv(&mut self, stream: &mut TcpStream) -> anyhow::Result<Vec<u8>> {
|
||||
let cipher = self.cipher.as_ref().ok_or_else(|| anyhow::anyhow!("Not connected"))?;
|
||||
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n == 0 {
|
||||
anyhow::bail!("Connection closed");
|
||||
}
|
||||
|
||||
let mut read_buf = BytesMut::from(&buf[..n]);
|
||||
|
||||
if let Some((data, _flags)) = decode_frame(&mut read_buf)? {
|
||||
let plaintext = cipher.decrypt(&data).map_err(|_| anyhow::anyhow!("Decryption failed"))?;
|
||||
Ok(plaintext)
|
||||
} else {
|
||||
anyhow::bail!("Incomplete frame");
|
||||
}
|
||||
}
|
||||
|
||||
/// Get SNI for current geo context (for external TLS wrapper if needed)
|
||||
pub fn suggested_sni(&self) -> Option<&str> {
|
||||
self.mimicry.random_sni(&self.config.country_code)
|
||||
}
|
||||
|
||||
/// Build TLS ClientHello for the selected SNI
|
||||
pub fn build_tls_hello(&self) -> Option<Vec<u8>> {
|
||||
let sni = self.suggested_sni()?;
|
||||
Some(TlsHelloBuilder::new(sni).build())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_client_config() {
|
||||
let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap();
|
||||
let psk = [0x42u8; 32];
|
||||
let config = ClientConfig::new(addr, psk, "RU");
|
||||
assert_eq!(config.country_code, "RU");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sni_selection() {
|
||||
let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap();
|
||||
let config = ClientConfig::new(addr, [0u8; 32], "RU");
|
||||
let client = OstpClient::new(config);
|
||||
|
||||
let sni = client.suggested_sni();
|
||||
assert!(sni.is_some());
|
||||
// Should be one of the Russian domains
|
||||
let sni = sni.unwrap();
|
||||
assert!(sni.ends_with(".ru"));
|
||||
}
|
||||
}
|
||||
149
ostp/src/crypto.rs
Normal file
149
ostp/src/crypto.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
//! OSTP Crypto Module - PSK, X25519, ChaCha20-Poly1305
|
||||
|
||||
use chacha20poly1305::{
|
||||
aead::{Aead, KeyInit, OsRng},
|
||||
ChaCha20Poly1305, Nonce,
|
||||
};
|
||||
use hmac::{digest::KeyInit as HmacKeyInit, Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
|
||||
|
||||
/// AEAD error type re-export
|
||||
pub type AeadError = chacha20poly1305::aead::Error;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
/// PSK-based silent handshake validator
|
||||
pub struct PskValidator {
|
||||
psk: [u8; 32],
|
||||
}
|
||||
|
||||
impl PskValidator {
|
||||
pub fn new(psk: [u8; 32]) -> Self {
|
||||
Self { psk }
|
||||
}
|
||||
|
||||
/// Generate HMAC signature for packet authentication
|
||||
pub fn sign(&self, data: &[u8]) -> [u8; 32] {
|
||||
let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(&self.psk).expect("HMAC key size");
|
||||
mac.update(data);
|
||||
mac.finalize().into_bytes().into()
|
||||
}
|
||||
|
||||
/// Verify packet has valid PSK-derived signature (silent drop if invalid)
|
||||
pub fn verify(&self, data: &[u8], signature: &[u8; 32]) -> bool {
|
||||
let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(&self.psk).expect("HMAC key size");
|
||||
mac.update(data);
|
||||
mac.verify_slice(signature).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// X25519 key exchange
|
||||
pub struct KeyExchange {
|
||||
secret: EphemeralSecret,
|
||||
public: PublicKey,
|
||||
}
|
||||
|
||||
impl KeyExchange {
|
||||
pub fn new() -> Self {
|
||||
let secret = EphemeralSecret::random_from_rng(OsRng);
|
||||
let public = PublicKey::from(&secret);
|
||||
Self { secret, public }
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
&self.public
|
||||
}
|
||||
|
||||
pub fn derive_shared(self, peer_public: &PublicKey) -> SharedSecret {
|
||||
self.secret.diffie_hellman(peer_public)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KeyExchange {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// AEAD cipher for data encryption
|
||||
pub struct AeadCipher {
|
||||
cipher: ChaCha20Poly1305,
|
||||
nonce_counter: u64,
|
||||
}
|
||||
|
||||
impl AeadCipher {
|
||||
pub fn new(key: &[u8; 32]) -> Self {
|
||||
let cipher = ChaCha20Poly1305::new_from_slice(key).expect("key size");
|
||||
Self {
|
||||
cipher,
|
||||
nonce_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_nonce(&mut self) -> Nonce {
|
||||
let mut nonce = [0u8; 12];
|
||||
nonce[4..12].copy_from_slice(&self.nonce_counter.to_le_bytes());
|
||||
self.nonce_counter = self.nonce_counter.wrapping_add(1);
|
||||
Nonce::from(nonce)
|
||||
}
|
||||
|
||||
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, AeadError> {
|
||||
let nonce = self.next_nonce();
|
||||
let mut ciphertext = self.cipher.encrypt(&nonce, plaintext)?;
|
||||
// Prepend nonce for decryption
|
||||
let mut output = nonce.to_vec();
|
||||
output.append(&mut ciphertext);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, AeadError> {
|
||||
if ciphertext.len() < 12 {
|
||||
return Err(chacha20poly1305::aead::Error);
|
||||
}
|
||||
let nonce = Nonce::from_slice(&ciphertext[..12]);
|
||||
self.cipher.decrypt(nonce, &ciphertext[12..])
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive session key from shared secret using HKDF-like expansion
|
||||
pub fn derive_session_key(shared: &SharedSecret, salt: &[u8]) -> [u8; 32] {
|
||||
let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(salt).expect("HMAC key size");
|
||||
mac.update(shared.as_bytes());
|
||||
mac.finalize().into_bytes().into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_psk_sign_verify() {
|
||||
let psk = [0x42u8; 32];
|
||||
let validator = PskValidator::new(psk);
|
||||
let data = b"hello";
|
||||
let sig = validator.sign(data);
|
||||
assert!(validator.verify(data, &sig));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_exchange() {
|
||||
let alice = KeyExchange::new();
|
||||
let bob = KeyExchange::new();
|
||||
let alice_pub = *alice.public_key();
|
||||
let bob_pub = *bob.public_key();
|
||||
let alice_shared = alice.derive_shared(&bob_pub);
|
||||
let bob_shared = bob.derive_shared(&alice_pub);
|
||||
assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aead_encrypt_decrypt() {
|
||||
let key = [0x11u8; 32];
|
||||
let mut cipher = AeadCipher::new(&key);
|
||||
let plaintext = b"secret message";
|
||||
let ciphertext = cipher.encrypt(plaintext).unwrap();
|
||||
let decrypted = cipher.decrypt(&ciphertext).unwrap();
|
||||
assert_eq!(plaintext.as_slice(), decrypted.as_slice());
|
||||
}
|
||||
}
|
||||
11
ostp/src/lib.rs
Normal file
11
ostp/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod client;
|
||||
pub mod crypto;
|
||||
pub mod mimicry;
|
||||
pub mod server;
|
||||
pub mod uot;
|
||||
|
||||
pub use client::{ClientConfig, OstpClient};
|
||||
pub use crypto::{AeadCipher, KeyExchange, PskValidator};
|
||||
pub use mimicry::{MimicryEngine, TlsHelloBuilder};
|
||||
pub use server::{OstpServer, ServerConfig};
|
||||
pub use uot::{decode_frame, encode_frame, FrameFlags, Fragmenter, Reassembler};
|
||||
174
ostp/src/mimicry.rs
Normal file
174
ostp/src/mimicry.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
//! Dynamic SNI & TLS Mimicry Engine
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Geo-based SNI target selection
|
||||
pub struct MimicryEngine {
|
||||
geo_sni_map: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
impl MimicryEngine {
|
||||
pub fn new() -> Self {
|
||||
let mut geo_sni_map = HashMap::new();
|
||||
|
||||
// Default geo-SNI mappings for contextual mimicry
|
||||
geo_sni_map.insert(
|
||||
"RU".into(),
|
||||
vec![
|
||||
"gosuslugi.ru".into(),
|
||||
"sberbank.ru".into(),
|
||||
"yandex.ru".into(),
|
||||
],
|
||||
);
|
||||
geo_sni_map.insert(
|
||||
"NO".into(),
|
||||
vec!["bankid.no".into(), "vipps.no".into(), "altinn.no".into()],
|
||||
);
|
||||
geo_sni_map.insert(
|
||||
"DE".into(),
|
||||
vec![
|
||||
"sparkasse.de".into(),
|
||||
"deutsche-bank.de".into(),
|
||||
"bund.de".into(),
|
||||
],
|
||||
);
|
||||
geo_sni_map.insert(
|
||||
"US".into(),
|
||||
vec![
|
||||
"apple.com".into(),
|
||||
"microsoft.com".into(),
|
||||
"amazon.com".into(),
|
||||
],
|
||||
);
|
||||
geo_sni_map.insert(
|
||||
"CN".into(),
|
||||
vec!["qq.com".into(), "baidu.com".into(), "taobao.com".into()],
|
||||
);
|
||||
|
||||
Self { geo_sni_map }
|
||||
}
|
||||
|
||||
/// Select SNI based on geo-location
|
||||
pub fn select_sni(&self, country_code: &str) -> Option<&str> {
|
||||
self.geo_sni_map
|
||||
.get(country_code)
|
||||
.and_then(|list| list.first().map(|s| s.as_str()))
|
||||
}
|
||||
|
||||
/// Get random SNI from geo list for anti-fingerprinting
|
||||
pub fn random_sni(&self, country_code: &str) -> Option<&str> {
|
||||
self.geo_sni_map.get(country_code).and_then(|list| {
|
||||
if list.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let idx = rand::random::<usize>() % list.len();
|
||||
Some(list[idx].as_str())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Add custom geo-SNI mapping
|
||||
pub fn add_mapping(&mut self, country: String, domains: Vec<String>) {
|
||||
self.geo_sni_map.insert(country, domains);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MimicryEngine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS ClientHello builder for REALITY-like mimicry
|
||||
pub struct TlsHelloBuilder {
|
||||
sni: String,
|
||||
random_session_id: bool,
|
||||
}
|
||||
|
||||
impl TlsHelloBuilder {
|
||||
pub fn new(sni: impl Into<String>) -> Self {
|
||||
Self {
|
||||
sni: sni.into(),
|
||||
random_session_id: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build minimal TLS 1.3 ClientHello-like header
|
||||
pub fn build(&self) -> Vec<u8> {
|
||||
let mut hello = Vec::with_capacity(256);
|
||||
|
||||
// TLS record header
|
||||
hello.push(0x16); // Handshake
|
||||
hello.extend_from_slice(&[0x03, 0x01]); // TLS 1.0 for compat
|
||||
|
||||
// Placeholder for length (will update)
|
||||
let len_pos = hello.len();
|
||||
hello.extend_from_slice(&[0x00, 0x00]);
|
||||
|
||||
// Handshake header
|
||||
hello.push(0x01); // ClientHello
|
||||
let hs_len_pos = hello.len();
|
||||
hello.extend_from_slice(&[0x00, 0x00, 0x00]); // length placeholder
|
||||
|
||||
// Client version (TLS 1.2 presented, 1.3 in extensions)
|
||||
hello.extend_from_slice(&[0x03, 0x03]);
|
||||
|
||||
// Random (32 bytes)
|
||||
let random: [u8; 32] = rand::random();
|
||||
hello.extend_from_slice(&random);
|
||||
|
||||
// Session ID (32 bytes if random)
|
||||
if self.random_session_id {
|
||||
hello.push(32);
|
||||
let session_id: [u8; 32] = rand::random();
|
||||
hello.extend_from_slice(&session_id);
|
||||
} else {
|
||||
hello.push(0);
|
||||
}
|
||||
|
||||
// Cipher suites (TLS 1.3 suites)
|
||||
hello.extend_from_slice(&[0x00, 0x06]); // 3 suites
|
||||
hello.extend_from_slice(&[0x13, 0x01]); // TLS_AES_128_GCM_SHA256
|
||||
hello.extend_from_slice(&[0x13, 0x02]); // TLS_AES_256_GCM_SHA384
|
||||
hello.extend_from_slice(&[0x13, 0x03]); // TLS_CHACHA20_POLY1305_SHA256
|
||||
|
||||
// Compression (null)
|
||||
hello.extend_from_slice(&[0x01, 0x00]);
|
||||
|
||||
// Extensions with SNI
|
||||
let ext_start = hello.len();
|
||||
hello.extend_from_slice(&[0x00, 0x00]); // ext length placeholder
|
||||
|
||||
// SNI extension
|
||||
hello.extend_from_slice(&[0x00, 0x00]); // type: server_name
|
||||
let sni_bytes = self.sni.as_bytes();
|
||||
let sni_ext_len = sni_bytes.len() + 5;
|
||||
hello.extend_from_slice(&(sni_ext_len as u16).to_be_bytes());
|
||||
hello.extend_from_slice(&((sni_bytes.len() + 3) as u16).to_be_bytes());
|
||||
hello.push(0x00); // host_name type
|
||||
hello.extend_from_slice(&(sni_bytes.len() as u16).to_be_bytes());
|
||||
hello.extend_from_slice(sni_bytes);
|
||||
|
||||
// supported_versions extension
|
||||
hello.extend_from_slice(&[0x00, 0x2b]); // type
|
||||
hello.extend_from_slice(&[0x00, 0x03]); // length
|
||||
hello.push(0x02); // versions length
|
||||
hello.extend_from_slice(&[0x03, 0x04]); // TLS 1.3
|
||||
|
||||
// Update lengths
|
||||
let ext_len = hello.len() - ext_start - 2;
|
||||
hello[ext_start] = (ext_len >> 8) as u8;
|
||||
hello[ext_start + 1] = ext_len as u8;
|
||||
|
||||
let total_len = hello.len() - 5;
|
||||
hello[len_pos] = (total_len >> 8) as u8;
|
||||
hello[len_pos + 1] = total_len as u8;
|
||||
|
||||
let hs_len = hello.len() - hs_len_pos - 3;
|
||||
hello[hs_len_pos] = 0;
|
||||
hello[hs_len_pos + 1] = (hs_len >> 8) as u8;
|
||||
hello[hs_len_pos + 2] = hs_len as u8;
|
||||
|
||||
hello
|
||||
}
|
||||
}
|
||||
192
ostp/src/server.rs
Normal file
192
ostp/src/server.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
//! OSTP Server - Stealth relay endpoint
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::crypto::{AeadCipher, KeyExchange, PskValidator};
|
||||
use crate::uot::decode_frame;
|
||||
use bytes::BytesMut;
|
||||
|
||||
/// Server configuration
|
||||
#[derive(Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub listen_addr: SocketAddr,
|
||||
pub psk: [u8; 32],
|
||||
pub max_connections: usize,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
pub fn new(addr: SocketAddr, psk: [u8; 32]) -> Self {
|
||||
Self {
|
||||
listen_addr: addr,
|
||||
psk,
|
||||
max_connections: 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Active connection state
|
||||
#[allow(dead_code)]
|
||||
struct Connection {
|
||||
cipher: AeadCipher,
|
||||
user_id: Option<uuid::Uuid>,
|
||||
bytes_rx: u64,
|
||||
bytes_tx: u64,
|
||||
}
|
||||
|
||||
/// OSTP Server with silent handshake
|
||||
pub struct OstpServer {
|
||||
config: ServerConfig,
|
||||
#[allow(dead_code)]
|
||||
psk_validator: PskValidator,
|
||||
connections: Arc<RwLock<HashMap<SocketAddr, Connection>>>,
|
||||
}
|
||||
|
||||
impl OstpServer {
|
||||
pub fn new(config: ServerConfig) -> Self {
|
||||
let psk_validator = PskValidator::new(config.psk);
|
||||
Self {
|
||||
config,
|
||||
psk_validator,
|
||||
connections: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the server (main async loop)
|
||||
pub async fn run(&self) -> anyhow::Result<()> {
|
||||
let listener = TcpListener::bind(self.config.listen_addr).await?;
|
||||
tracing::info!("OSTP server listening on {}", self.config.listen_addr);
|
||||
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((stream, addr)) => {
|
||||
let psk_validator = PskValidator::new(self.config.psk);
|
||||
let connections = Arc::clone(&self.connections);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::handle_connection(stream, addr, psk_validator, connections).await {
|
||||
tracing::debug!("Connection {} closed: {}", addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Accept error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
mut stream: TcpStream,
|
||||
addr: SocketAddr,
|
||||
psk_validator: PskValidator,
|
||||
connections: Arc<RwLock<HashMap<SocketAddr, Connection>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut buf = [0u8; 4096];
|
||||
|
||||
// Phase 1: Silent PSK handshake (32-byte signature must be first)
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n < 64 {
|
||||
// Silent drop - don't reveal port is open
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let signature: [u8; 32] = buf[..32].try_into()?;
|
||||
let payload = &buf[32..n];
|
||||
|
||||
if !psk_validator.verify(payload, &signature) {
|
||||
// Silent drop on invalid PSK - core anti-probing defense
|
||||
tracing::trace!("Silent drop for {}: invalid PSK", addr);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Phase 2: X25519 key exchange
|
||||
// Payload contains client's public key (32 bytes)
|
||||
if payload.len() < 32 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let client_pubkey_bytes: [u8; 32] = payload[..32].try_into()?;
|
||||
let client_pubkey = x25519_dalek::PublicKey::from(client_pubkey_bytes);
|
||||
|
||||
let server_kex = KeyExchange::new();
|
||||
let server_pubkey = *server_kex.public_key();
|
||||
let shared_secret = server_kex.derive_shared(&client_pubkey);
|
||||
|
||||
// Derive session key
|
||||
let session_key = crate::crypto::derive_session_key(&shared_secret, b"ostp-session-v1");
|
||||
let cipher = AeadCipher::new(&session_key);
|
||||
|
||||
// Send server's public key (signed with PSK)
|
||||
let server_response = server_pubkey.as_bytes();
|
||||
let response_sig = psk_validator.sign(server_response);
|
||||
stream.write_all(&response_sig).await?;
|
||||
stream.write_all(server_response).await?;
|
||||
|
||||
// Store connection
|
||||
{
|
||||
let mut conns = connections.write().await;
|
||||
conns.insert(addr, Connection {
|
||||
cipher,
|
||||
user_id: None,
|
||||
bytes_rx: 0,
|
||||
bytes_tx: 0,
|
||||
});
|
||||
}
|
||||
|
||||
tracing::info!("Session established with {}", addr);
|
||||
|
||||
// Phase 3: Encrypted relay loop
|
||||
let mut read_buf = BytesMut::with_capacity(65536);
|
||||
|
||||
loop {
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
read_buf.extend_from_slice(&buf[..n]);
|
||||
|
||||
// Process frames
|
||||
while let Some((data, flags)) = decode_frame(&mut read_buf)? {
|
||||
let conns = connections.read().await;
|
||||
if let Some(conn) = conns.get(&addr) {
|
||||
match conn.cipher.decrypt(&data) {
|
||||
Ok(plaintext) => {
|
||||
if flags.is_data() {
|
||||
// TODO: Route decrypted data to destination
|
||||
tracing::trace!("Received {} bytes from {}", plaintext.len(), addr);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("Decrypt failed for {}", addr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
connections.write().await.remove(&addr);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_server_config() {
|
||||
let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap();
|
||||
let psk = [0x42u8; 32];
|
||||
let config = ServerConfig::new(addr, psk);
|
||||
assert_eq!(config.listen_addr, addr);
|
||||
assert_eq!(config.max_connections, 1024);
|
||||
}
|
||||
}
|
||||
132
ostp/src/uot.rs
Normal file
132
ostp/src/uot.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! UoT (UDP-over-TCP) framing layer
|
||||
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Maximum UDP datagram size
|
||||
pub const MAX_UDP_SIZE: usize = 65535;
|
||||
/// Frame header: 2 bytes length + 1 byte flags
|
||||
const HEADER_SIZE: usize = 3;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FrameError {
|
||||
#[error("frame too large: {0} bytes")]
|
||||
TooLarge(usize),
|
||||
#[error("incomplete frame")]
|
||||
Incomplete,
|
||||
#[error("invalid frame")]
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct FrameFlags(u8);
|
||||
|
||||
impl FrameFlags {
|
||||
pub const DATA: Self = Self(0x00);
|
||||
pub const CONTROL: Self = Self(0x01);
|
||||
pub const PADDING: Self = Self(0x02);
|
||||
|
||||
pub fn is_data(self) -> bool {
|
||||
self.0 == 0x00
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulate UDP datagram into framed format with random padding
|
||||
pub fn encode_frame(data: &[u8], flags: FrameFlags, padding: usize) -> Result<Bytes, FrameError> {
|
||||
let total_len = data.len() + padding;
|
||||
if total_len > MAX_UDP_SIZE {
|
||||
return Err(FrameError::TooLarge(total_len));
|
||||
}
|
||||
|
||||
let mut buf = BytesMut::with_capacity(HEADER_SIZE + total_len);
|
||||
buf.put_u16(total_len as u16);
|
||||
buf.put_u8(flags.0);
|
||||
buf.put_slice(data);
|
||||
|
||||
// Random padding to avoid fingerprinting
|
||||
if padding > 0 {
|
||||
let pad: Vec<u8> = (0..padding).map(|_| rand::random()).collect();
|
||||
buf.put_slice(&pad);
|
||||
}
|
||||
|
||||
Ok(buf.freeze())
|
||||
}
|
||||
|
||||
/// Decode frame from buffer, returns (payload, data_length, flags)
|
||||
pub fn decode_frame(buf: &mut BytesMut) -> Result<Option<(Bytes, FrameFlags)>, FrameError> {
|
||||
if buf.len() < HEADER_SIZE {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
|
||||
let flags = FrameFlags(buf[2]);
|
||||
|
||||
if buf.len() < HEADER_SIZE + len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
buf.advance(HEADER_SIZE);
|
||||
let data = buf.split_to(len).freeze();
|
||||
|
||||
Ok(Some((data, flags)))
|
||||
}
|
||||
|
||||
/// MTU-aware fragmentation
|
||||
pub struct Fragmenter {
|
||||
mtu: usize,
|
||||
}
|
||||
|
||||
impl Fragmenter {
|
||||
pub fn new(mtu: usize) -> Self {
|
||||
Self { mtu }
|
||||
}
|
||||
|
||||
pub fn fragment(&self, data: &[u8]) -> Vec<Bytes> {
|
||||
data.chunks(self.mtu)
|
||||
.map(|chunk| Bytes::copy_from_slice(chunk))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reassembly buffer for fragmented packets
|
||||
pub struct Reassembler {
|
||||
buffer: BytesMut,
|
||||
expected_len: Option<usize>,
|
||||
}
|
||||
|
||||
impl Reassembler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buffer: BytesMut::new(),
|
||||
expected_len: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, fragment: &[u8], total_len: usize) {
|
||||
if self.expected_len.is_none() {
|
||||
self.expected_len = Some(total_len);
|
||||
}
|
||||
self.buffer.extend_from_slice(fragment);
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.expected_len
|
||||
.map(|len| self.buffer.len() >= len)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> Option<Bytes> {
|
||||
if self.is_complete() {
|
||||
self.expected_len = None;
|
||||
Some(self.buffer.split().freeze())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Reassembler {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user