127 lines
3.8 KiB
Rust
127 lines
3.8 KiB
Rust
//! Socket handler relay for connecting client connections to a TypeScript handler
|
|
//! via a Unix domain socket.
|
|
//!
|
|
//! Protocol: Send a JSON metadata line terminated by `\n`, then bidirectional relay.
|
|
|
|
use tokio::net::UnixStream;
|
|
use tokio::io::{AsyncWriteExt, AsyncReadExt};
|
|
use tokio::net::TcpStream;
|
|
use serde::Serialize;
|
|
use tracing::debug;
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RelayMetadata {
|
|
connection_id: u64,
|
|
remote_ip: String,
|
|
remote_port: u16,
|
|
local_port: u16,
|
|
sni: Option<String>,
|
|
route_name: String,
|
|
initial_data_base64: Option<String>,
|
|
}
|
|
|
|
/// Relay a client connection to a TypeScript handler via Unix domain socket.
|
|
///
|
|
/// Protocol: Send a JSON metadata line terminated by `\n`, then bidirectional relay.
|
|
pub async fn relay_to_handler(
|
|
client: TcpStream,
|
|
relay_socket_path: &str,
|
|
connection_id: u64,
|
|
remote_ip: String,
|
|
remote_port: u16,
|
|
local_port: u16,
|
|
sni: Option<String>,
|
|
route_name: String,
|
|
initial_data: Option<&[u8]>,
|
|
) -> std::io::Result<()> {
|
|
debug!(
|
|
"Relaying connection {} to handler socket {}",
|
|
connection_id, relay_socket_path
|
|
);
|
|
|
|
// Connect to TypeScript handler Unix socket
|
|
let mut handler = UnixStream::connect(relay_socket_path).await?;
|
|
|
|
// Build and send metadata header
|
|
let initial_data_base64 = initial_data.map(base64_encode);
|
|
|
|
let metadata = RelayMetadata {
|
|
connection_id,
|
|
remote_ip,
|
|
remote_port,
|
|
local_port,
|
|
sni,
|
|
route_name,
|
|
initial_data_base64,
|
|
};
|
|
|
|
let metadata_json = serde_json::to_string(&metadata)
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
|
|
|
handler.write_all(metadata_json.as_bytes()).await?;
|
|
handler.write_all(b"\n").await?;
|
|
|
|
// Bidirectional relay between client and handler
|
|
let (mut client_read, mut client_write) = client.into_split();
|
|
let (mut handler_read, mut handler_write) = handler.into_split();
|
|
|
|
let c2h = tokio::spawn(async move {
|
|
let mut buf = vec![0u8; 65536];
|
|
loop {
|
|
let n = match client_read.read(&mut buf).await {
|
|
Ok(0) | Err(_) => break,
|
|
Ok(n) => n,
|
|
};
|
|
if handler_write.write_all(&buf[..n]).await.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
let _ = handler_write.shutdown().await;
|
|
});
|
|
|
|
let h2c = tokio::spawn(async move {
|
|
let mut buf = vec![0u8; 65536];
|
|
loop {
|
|
let n = match handler_read.read(&mut buf).await {
|
|
Ok(0) | Err(_) => break,
|
|
Ok(n) => n,
|
|
};
|
|
if client_write.write_all(&buf[..n]).await.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
let _ = client_write.shutdown().await;
|
|
});
|
|
|
|
let _ = tokio::join!(c2h, h2c);
|
|
|
|
debug!("Relay connection {} completed", connection_id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Simple base64 encoding without external dependency.
|
|
fn base64_encode(data: &[u8]) -> String {
|
|
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
let mut result = String::new();
|
|
for chunk in data.chunks(3) {
|
|
let b0 = chunk[0] as u32;
|
|
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
|
|
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
|
|
let n = (b0 << 16) | (b1 << 8) | b2;
|
|
result.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
|
|
result.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
|
|
if chunk.len() > 1 {
|
|
result.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
|
|
} else {
|
|
result.push('=');
|
|
}
|
|
if chunk.len() > 2 {
|
|
result.push(CHARS[(n & 0x3F) as usize] as char);
|
|
} else {
|
|
result.push('=');
|
|
}
|
|
}
|
|
result
|
|
}
|