From d8ff958481b94fa01209c3f78a9c5fdb5099cd00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=96=D0=BE=D1=80=D0=B0=20=D0=97=D0=BC=D0=B5=D0=B9=D0=BA?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 12 Feb 2026 18:53:07 +0300 Subject: [PATCH] Add mask_unix_sock for censorship masking via Unix socket --- README.md | 1 + config.toml | 1 + src/config/mod.rs | 35 ++++++++++++++-- src/main.rs | 15 +++++-- src/proxy/masking.rs | 98 +++++++++++++++++++++++++++++++------------- 5 files changed, 114 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 5abe4dc..1df8bb0 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ tls_domain = "petrovich.ru" mask = true mask_port = 443 # mask_host = "petrovich.ru" # Defaults to tls_domain if not set +# mask_unix_sock = "/var/run/nginx.sock" # Unix socket (mutually exclusive with mask_host) fake_cert_len = 2048 # === Access Control & Users === diff --git a/config.toml b/config.toml index 4edd318..aa31105 100644 --- a/config.toml +++ b/config.toml @@ -48,6 +48,7 @@ tls_domain = "google.ru" mask = true mask_port = 443 # mask_host = "petrovich.ru" # Defaults to tls_domain if not set +# mask_unix_sock = "/var/run/nginx.sock" # Unix socket (mutually exclusive with mask_host) fake_cert_len = 2048 # === Access Control & Users === diff --git a/src/config/mod.rs b/src/config/mod.rs index ef3fa35..14ac6ab 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -212,6 +212,9 @@ pub struct AntiCensorshipConfig { #[serde(default = "default_mask_port")] pub mask_port: u16, + #[serde(default)] + pub mask_unix_sock: Option, + #[serde(default = "default_fake_cert_len")] pub fake_cert_len: usize, } @@ -223,6 +226,7 @@ impl Default for AntiCensorshipConfig { mask: true, mask_host: None, mask_port: default_mask_port(), + mask_unix_sock: None, fake_cert_len: default_fake_cert_len(), } } @@ -376,8 +380,33 @@ impl ProxyConfig { return Err(ProxyError::Config("tls_domain cannot be empty".to_string())); } - // Default mask_host to tls_domain if not set - if config.censorship.mask_host.is_none() { + // Validate mask_unix_sock + if let Some(ref sock_path) = config.censorship.mask_unix_sock { + if sock_path.is_empty() { + return Err(ProxyError::Config( + "mask_unix_sock cannot be empty".to_string() + )); + } + #[cfg(unix)] + if sock_path.len() > 107 { + return Err(ProxyError::Config( + format!("mask_unix_sock path too long: {} bytes (max 107)", sock_path.len()) + )); + } + #[cfg(not(unix))] + return Err(ProxyError::Config( + "mask_unix_sock is only supported on Unix platforms".to_string() + )); + + if config.censorship.mask_host.is_some() { + return Err(ProxyError::Config( + "mask_unix_sock and mask_host are mutually exclusive".to_string() + )); + } + } + + // Default mask_host to tls_domain if not set and no unix socket configured + if config.censorship.mask_host.is_none() && config.censorship.mask_unix_sock.is_none() { config.censorship.mask_host = Some(config.censorship.tls_domain.clone()); } @@ -429,7 +458,7 @@ impl ProxyConfig { format!("Invalid tls_domain: '{}'. Must be a valid domain name", self.censorship.tls_domain) )); } - + Ok(()) } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d91bf2b..b28167c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -130,10 +130,17 @@ async fn main() -> Result<(), Box> { config.general.modes.secure, config.general.modes.tls); info!("TLS domain: {}", config.censorship.tls_domain); - info!("Mask: {} -> {}:{}", - config.censorship.mask, - config.censorship.mask_host.as_deref().unwrap_or(&config.censorship.tls_domain), - config.censorship.mask_port); + if let Some(ref sock) = config.censorship.mask_unix_sock { + info!("Mask: {} -> unix:{}", config.censorship.mask, sock); + if !std::path::Path::new(sock).exists() { + warn!("Unix socket '{}' does not exist yet. Masking will fail until it appears.", sock); + } + } else { + info!("Mask: {} -> {}:{}", + config.censorship.mask, + config.censorship.mask_host.as_deref().unwrap_or(&config.censorship.tls_domain), + config.censorship.mask_port); + } if config.censorship.tls_domain == "www.google.com" { warn!("Using default tls_domain. Consider setting a custom domain."); diff --git a/src/proxy/masking.rs b/src/proxy/masking.rs index e81804a..3d3039a 100644 --- a/src/proxy/masking.rs +++ b/src/proxy/masking.rs @@ -3,6 +3,8 @@ use std::time::Duration; use std::str; use tokio::net::TcpStream; +#[cfg(unix)] +use tokio::net::UnixStream; use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt}; use tokio::time::timeout; use tracing::debug; @@ -24,33 +26,33 @@ fn detect_client_type(data: &[u8]) -> &'static str { return "HTTP"; } } - + // Check for TLS ClientHello (0x16 = handshake, 0x03 0x01-0x03 = TLS version) if data.len() > 3 && data[0] == 0x16 && data[1] == 0x03 { return "TLS-scanner"; } - + // Check for SSH if data.starts_with(b"SSH-") { return "SSH"; } - + // Port scanner (very short data) if data.len() < 10 { return "port-scanner"; } - + "unknown" } /// Handle a bad client by forwarding to mask host pub async fn handle_bad_client( - mut reader: R, - mut writer: W, + reader: R, + writer: W, initial_data: &[u8], config: &ProxyConfig, -) -where +) +where R: AsyncRead + Unpin + Send + 'static, W: AsyncWrite + Unpin + Send + 'static, { @@ -59,13 +61,41 @@ where consume_client_data(reader).await; return; } - + let client_type = detect_client_type(initial_data); - + + // Connect via Unix socket or TCP + #[cfg(unix)] + if let Some(ref sock_path) = config.censorship.mask_unix_sock { + debug!( + client_type = client_type, + sock = %sock_path, + data_len = initial_data.len(), + "Forwarding bad client to mask unix socket" + ); + + let connect_result = timeout(MASK_TIMEOUT, UnixStream::connect(sock_path)).await; + match connect_result { + Ok(Ok(stream)) => { + let (mask_read, mask_write) = stream.into_split(); + relay_to_mask(reader, writer, mask_read, mask_write, initial_data).await; + } + Ok(Err(e)) => { + debug!(error = %e, "Failed to connect to mask unix socket"); + consume_client_data(reader).await; + } + Err(_) => { + debug!("Timeout connecting to mask unix socket"); + consume_client_data(reader).await; + } + } + return; + } + let mask_host = config.censorship.mask_host.as_deref() .unwrap_or(&config.censorship.tls_domain); let mask_port = config.censorship.mask_port; - + debug!( client_type = client_type, host = %mask_host, @@ -73,35 +103,45 @@ where data_len = initial_data.len(), "Forwarding bad client to mask host" ); - + // Connect to mask host let mask_addr = format!("{}:{}", mask_host, mask_port); - let connect_result = timeout( - MASK_TIMEOUT, - TcpStream::connect(&mask_addr) - ).await; - - let mask_stream = match connect_result { - Ok(Ok(s)) => s, + let connect_result = timeout(MASK_TIMEOUT, TcpStream::connect(&mask_addr)).await; + match connect_result { + Ok(Ok(stream)) => { + let (mask_read, mask_write) = stream.into_split(); + relay_to_mask(reader, writer, mask_read, mask_write, initial_data).await; + } Ok(Err(e)) => { debug!(error = %e, "Failed to connect to mask host"); consume_client_data(reader).await; - return; } Err(_) => { debug!("Timeout connecting to mask host"); consume_client_data(reader).await; - return; } - }; - - let (mut mask_read, mut mask_write) = mask_stream.into_split(); - + } +} + +/// Relay traffic between client and mask backend +async fn relay_to_mask( + mut reader: R, + mut writer: W, + mut mask_read: MR, + mut mask_write: MW, + initial_data: &[u8], +) +where + R: AsyncRead + Unpin + Send + 'static, + W: AsyncWrite + Unpin + Send + 'static, + MR: AsyncRead + Unpin + Send + 'static, + MW: AsyncWrite + Unpin + Send + 'static, +{ // Send initial data to mask host if mask_write.write_all(initial_data).await.is_err() { return; } - + // Relay traffic let c2m = tokio::spawn(async move { let mut buf = vec![0u8; MASK_BUFFER_SIZE]; @@ -119,7 +159,7 @@ where } } }); - + let m2c = tokio::spawn(async move { let mut buf = vec![0u8; MASK_BUFFER_SIZE]; loop { @@ -136,7 +176,7 @@ where } } }); - + // Wait for either to complete tokio::select! { _ = c2m => {} @@ -152,4 +192,4 @@ async fn consume_client_data(mut reader: R) { break; } } -} \ No newline at end of file +}