diff options
| -rw-r--r-- | src/log.rs | 29 | ||||
| -rw-r--r-- | src/main.rs | 217 | ||||
| -rw-r--r-- | src/opt.rs | 165 | ||||
| -rw-r--r-- | src/pkt.rs | 112 | ||||
| -rw-r--r-- | src/platform/linux.rs | 29 |
5 files changed, 283 insertions, 269 deletions
@@ -1,4 +1,4 @@ -// Copyright 2025 Dillution <[email protected]>. +// Copyright 2025-2026 Dillution <[email protected]>. // // This file is part of DPIBreak. // @@ -16,7 +16,6 @@ // along with DPIBreak. If not, see <https://www.gnu.org/licenses/>. use std::fmt; -use std::sync::OnceLock; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum LogLevel { @@ -26,18 +25,6 @@ pub enum LogLevel { Error, // Unrecoverable } -const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Warning; - -static LOG_LEVEL_OVERRIDE: OnceLock<LogLevel> = OnceLock::new(); - -pub fn set_log_level(level: LogLevel) -> Result<(), &'static str> { - LOG_LEVEL_OVERRIDE.set(level).map_err(|_| "LOG_LEVEL already initialized") -} - -pub fn current_log_level() -> LogLevel { - *LOG_LEVEL_OVERRIDE.get().unwrap_or(&DEFAULT_LOG_LEVEL) -} - impl fmt::Display for LogLevel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let p = match self { @@ -74,20 +61,10 @@ impl std::str::FromStr for LogLevel { } } -static NO_SPLASH: OnceLock<bool> = OnceLock::new(); - -pub fn set_no_splash(no_splash: bool) -> Result<(), &'static str> { - NO_SPLASH.set(no_splash).map_err(|_| "NO_SPLASH already initialized") -} - -pub fn no_splash() -> bool { - *NO_SPLASH.get().unwrap_or(&false) -} - #[macro_export] macro_rules! log_println { ($level:expr, $($arg:tt)*) => {{ - if $level >= crate::log::current_log_level() { + if $level >= crate::opt::log_level() { println!("{} {}", $level, format_args!($($arg)*)); } }}; @@ -96,7 +73,7 @@ macro_rules! log_println { #[macro_export] macro_rules! splash { ($($arg:tt)*) => {{ - if !crate::log::no_splash() { + if !crate::opt::no_splash() { println!($($arg)*); } }}; diff --git a/src/main.rs b/src/main.rs index ab7becc..a36bd48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,16 +15,16 @@ // You should have received a copy of the GNU General Public License // along with DPIBreak. If not, see <https://www.gnu.org/licenses/>. -use anyhow::{Result, anyhow, Context}; +use anyhow::{Result, Context}; use std::sync::{ atomic::{Ordering, AtomicBool}, - OnceLock, }; mod platform; mod pkt; mod tls; mod log; +mod opt; use log::LogLevel; @@ -36,211 +36,6 @@ const MESSAGE_AT_RUN: &str = r#"DPIBreak is now running. Press Ctrl+C or close this window to stop. "#; static RUNNING: AtomicBool = AtomicBool::new(true); -static OPT_DELAY_MS: OnceLock<u64> = OnceLock::new(); - -static OPT_FAKE: OnceLock<bool> = OnceLock::new(); - -fn opt_fake() -> bool { - *crate::OPT_FAKE.get().expect("OPT_FAKE not initialized") -} - -fn delay_ms() -> u64 { - *OPT_DELAY_MS.get().expect("OPT_DELAY_MS not initialized") -} - -fn split_packet( - view: &pkt::PktView, - start: u32, - end: Option<u32>, - out_buf: &mut Vec<u8> -) -> Result<()> { - pkt::split_packet_0(view, start, end, out_buf, None, None, None) -} - -fn send_segment( - view: &pkt::PktView, - start: u32, - end: Option<u32>, - buf: &mut Vec<u8> -) -> Result<()> { - use platform::send_to_raw; - - if opt_fake() { - pkt::fake_clienthello(view, start, end, buf)?; - send_to_raw(buf)?; - } - split_packet(view, start, end, buf)?; - send_to_raw(buf)?; - - Ok(()) -} - -fn split_packet_1(view: &pkt::PktView, order: &[u32], buf: &mut Vec<u8>) -> Result<()> { - let mut it = order.iter().copied(); - - let Some(mut first) = it.next() else { - return Err(anyhow!("split_packet_1: invalid order array")); - }; - - for next in it { - send_segment(view, first, Some(next), buf)?; - std::thread::sleep(std::time::Duration::from_millis(delay_ms())); - first = next; - } - - send_segment(view, first, None, buf)?; - - Ok(()) -} - -/// Return Ok(true) if packet is handled -fn handle_packet(pkt: &[u8], buf: &mut Vec::<u8>) -> Result<bool> { - #[cfg(target_os = "linux")] - let is_filtered = platform::IS_U32_SUPPORTED.load(Ordering::Relaxed); - - #[cfg(windows)] - let is_filtered = true; - - let view = pkt::PktView::from_raw(pkt)?; - - if !is_filtered && !tls::is_client_hello(view.tcp.payload()) { - return Ok(false); - } - - // TODO: if clienthello packet has been (unlikely) fragmented, - // we should find the second part and drop, reassemble it here. - - split_packet_1(&view, &[0, 1], buf)?; - - #[cfg(debug_assertions)] - log_println!(LogLevel::Debug, "packet is handled, len={}", pkt.len()); - - Ok(true) -} - -#[macro_export] -macro_rules! handle_packet { - ($bytes:expr, $buf:expr, handled => $on_handled:expr, rejected => $on_rejected:expr $(,)?) => {{ - match handle_packet($bytes, $buf) { - Ok(true) => { $on_handled } - Ok(false) => { $on_rejected } - Err(e) => { - log_println!(LogLevel::Warning, "handle_packet: {e}"); - $on_rejected - } - } - }}; -} - -fn take_value<T, I>(args: &mut I, arg_name: &str) -> Result<T> -where - T: std::str::FromStr, - T::Err: std::error::Error + Send + Sync + 'static, - I: Iterator<Item = String>, -{ - let raw = args - .next() - .ok_or_else(|| anyhow!("argument: missing value after {}", arg_name))?; - raw.parse::<T>() - .with_context(|| format!("argument {}: invalid value '{}'", arg_name, raw)) -} - -fn usage() { - println!( - r#"Usage: dpibreak [OPTIONS] - -Options: - --delay-ms <u64> (default: 0) - --queue-num <u16> (linux only, default: 1) - --nft-command <string> (linux only, default: nft) - --loglevel <debug|info|warning|error> (default: warning) - --no-splash Do not print splash messages - - --fake Enable fake clienthello injection - --fake-ttl <u8> Override ttl of fake clienthello (default: 8) - --fake-badsum Modifies the TCP checksum of the fake packet to an invalid value. - - -h, --help Show this help"# - ); -} - -fn set_opt<T: std::fmt::Display>( - name: &str, - cell: &OnceLock<T>, - value: T, -) -> Result<()> { - log_println!(LogLevel::Info, "{name}: {value}"); - cell.set(value).map_err(|_| anyhow!("{name} already initialized")) -} - -fn splash_banner() { - splash!("{PROJECT_NAME} v{PKG_VERSION} - {PKG_DESCRIPTION}"); - splash!("{PKG_HOMEPAGE}"); - splash!(""); -} - -fn parse_args_1() -> Result<()> { - let mut delay_ms: u64 = 0; - let mut no_splash: bool = false; - let mut fake: bool = false; - let mut fake_ttl: u8 = 8; - let mut fake_badsum: bool = false; - - #[cfg(debug_assertions)] - let mut log_level: log::LogLevel = LogLevel::Debug; - #[cfg(not(debug_assertions))] - let mut log_level: log::LogLevel = LogLevel::Warning; - #[cfg(target_os = "linux")] - let mut queue_num: u16 = 1; - #[cfg(target_os = "linux")] - let mut nft_command = String::from("nft"); - - let mut args = std::env::args().skip(1); // program name - - while let Some(arg) = args.next() { - let argv = arg.as_str(); - - match argv { - "-h" | "--help" => { usage(); std::process::exit(0); } - "--delay-ms" => { delay_ms = take_value(&mut args, argv)?; } - "--loglevel" => { log_level = take_value(&mut args, argv)?; } - "--no-splash" => { no_splash = true; } - - "--fake" => { fake = true; } - "--fake-ttl" => { fake_ttl = take_value(&mut args, argv)?; } - "--fake-badsum" => { fake_badsum = true } - - #[cfg(target_os = "linux")] - "--queue-num" => { queue_num = take_value(&mut args, argv)?; } - - #[cfg(target_os = "linux")] - "--nft-command" => { nft_command = take_value(&mut args, argv)?; } - - _ => { return Err(anyhow!("argument: unknown: {}", arg)); } - } - } - - log::set_no_splash(no_splash).map_err(|e| anyhow!("{e}"))?; - log::set_log_level(log_level).map_err(|e| anyhow!("{e}"))?; - - set_opt("OPT_DELAY_MS", &OPT_DELAY_MS, delay_ms)?; - set_opt("OPT_FAKE", &OPT_FAKE, fake)?; - set_opt("OPT_FAKE_TTL", &pkt::OPT_FAKE_TTL, fake_ttl)?; - set_opt("OPT_FAKE_BADSUM", &pkt::OPT_FAKE_BADSUM, fake_badsum)?; - - #[cfg(target_os = "linux")] set_opt("OPT_QUEUE_NUM", &platform::OPT_QUEUE_NUM, queue_num)?; - #[cfg(target_os = "linux")] set_opt("OPT_NFT_COMMAND", &platform::OPT_NFT_COMMAND, nft_command)?; - - Ok(()) -} - -fn parse_args() { - if let Err(e) = parse_args_1() { - log_println!(LogLevel::Error, "{e}"); - usage(); - std::process::exit(1); - } -} fn trap_exit() -> Result<()> { ctrlc::set_handler(|| { @@ -261,9 +56,15 @@ impl Drop for EnsureCleanup { } } +fn splash_banner() { + splash!("{PROJECT_NAME} v{PKG_VERSION} - {PKG_DESCRIPTION}"); + splash!("{PKG_HOMEPAGE}"); + splash!(""); +} + fn main_0() -> Result<()> { trap_exit()?; - parse_args(); + opt::parse_args(); splash_banner(); let _guard = EnsureCleanup; diff --git a/src/opt.rs b/src/opt.rs new file mode 100644 index 0000000..287f1d7 --- /dev/null +++ b/src/opt.rs @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> +// SPDX-License-Identifier: GPL-3.0-or-later + +use anyhow::{Result, anyhow, Context}; +use std::sync::OnceLock; + +use crate::log_println; + +use crate::log; + +use log::LogLevel; + +static OPT_NO_SPLASH: OnceLock<bool> = OnceLock::new(); +static OPT_LOG_LEVEL: OnceLock<LogLevel> = OnceLock::new(); + +static OPT_FAKE_TTL: OnceLock<u8> = OnceLock::new(); +static OPT_FAKE_BADSUM: OnceLock<bool> = OnceLock::new(); +static OPT_FAKE: OnceLock<bool> = OnceLock::new(); + +static OPT_DELAY_MS: OnceLock<u64> = OnceLock::new(); + +#[cfg(target_os = "linux")] static OPT_QUEUE_NUM: OnceLock<u16> = OnceLock::new(); +#[cfg(target_os = "linux")] static OPT_NFT_COMMAND: OnceLock<String> = OnceLock::new(); + +pub fn no_splash() -> bool { + *OPT_NO_SPLASH.get().expect("OPT_NO_SPLASH not initialized") +} + +pub fn log_level() -> LogLevel { + *OPT_LOG_LEVEL.get().expect("OPT_LOG_LEVEL not initialized") +} + +pub fn fake() -> bool { + *OPT_FAKE.get().expect("OPT_FAKE not initialized") +} + +pub fn fake_ttl() -> u8 { + *OPT_FAKE_TTL.get().expect("OPT_FAKE_TTL not initialized") +} + +pub fn fake_badsum() -> bool { + *OPT_FAKE_BADSUM.get().expect("OPT_FAKE_BADSUM not initialized") +} + +pub fn delay_ms() -> u64 { + *OPT_DELAY_MS.get().expect("OPT_DELAY_MS not initialized") +} + +#[cfg(target_os = "linux")] +pub fn queue_num() -> u16 { + *OPT_QUEUE_NUM.get().expect("OPT_QUEUE_NUM not initialized") +} + +#[cfg(target_os = "linux")] +pub fn nft_command() -> &'static str { + OPT_NFT_COMMAND.get().expect("OPT_NFT_COMMAND not initialized").as_str() +} + +fn take_value<T, I>(args: &mut I, arg_name: &str) -> Result<T> +where + T: std::str::FromStr, + T::Err: std::error::Error + Send + Sync + 'static, + I: Iterator<Item = String>, +{ + let raw = args + .next() + .ok_or_else(|| anyhow!("argument: missing value after {}", arg_name))?; + raw.parse::<T>() + .with_context(|| format!("argument {}: invalid value '{}'", arg_name, raw)) +} + +fn usage() { + println!( + r#"Usage: dpibreak [OPTIONS] + +Options: + --delay-ms <u64> (default: 0) + --queue-num <u16> (linux only, default: 1) + --nft-command <string> (linux only, default: nft) + --loglevel <debug|info|warning|error> (default: warning) + --no-splash Do not print splash messages + + --fake Enable fake clienthello injection + --fake-ttl <u8> Override ttl of fake clienthello (default: 8) + --fake-badsum Modifies the TCP checksum of the fake packet to an invalid value. + + -h, --help Show this help"# + ); +} + +fn set_opt<T: std::fmt::Display>( + name: &str, + cell: &OnceLock<T>, + value: T, +) -> Result<()> { + cell.set(value).map_err(|_| anyhow!("{name} already initialized"))?; + + let v = cell.get().expect("just set; qed"); + log_println!(LogLevel::Info, "{name}: {v}"); + + Ok(()) +} + +fn parse_args_1() -> Result<()> { + let mut delay_ms: u64 = 0; + let mut no_splash: bool = false; + let mut fake: bool = false; + let mut fake_ttl: u8 = 8; + let mut fake_badsum: bool = false; + + #[cfg(debug_assertions)] + let mut log_level: log::LogLevel = LogLevel::Debug; + #[cfg(not(debug_assertions))] + let mut log_level: log::LogLevel = LogLevel::Warning; + #[cfg(target_os = "linux")] + let mut queue_num: u16 = 1; + #[cfg(target_os = "linux")] + let mut nft_command = String::from("nft"); + + let mut args = std::env::args().skip(1); // program name + + while let Some(arg) = args.next() { + let argv = arg.as_str(); + + match argv { + "-h" | "--help" => { usage(); std::process::exit(0); } + "--delay-ms" => { delay_ms = take_value(&mut args, argv)?; } + "--loglevel" => { log_level = take_value(&mut args, argv)?; } + "--no-splash" => { no_splash = true; } + + "--fake" => { fake = true; } + "--fake-ttl" => { fake_ttl = take_value(&mut args, argv)?; } + "--fake-badsum" => { fake_badsum = true } + + #[cfg(target_os = "linux")] + "--queue-num" => { queue_num = take_value(&mut args, argv)?; } + + #[cfg(target_os = "linux")] + "--nft-command" => { nft_command = take_value(&mut args, argv)?; } + + _ => { return Err(anyhow!("argument: unknown: {}", arg)); } + } + } + + set_opt("OPT_LOG_LEVEL", &OPT_LOG_LEVEL, log_level)?; + set_opt("OPT_NO_SPLASH", &OPT_NO_SPLASH, no_splash)?; + + set_opt("OPT_DELAY_MS", &OPT_DELAY_MS, delay_ms)?; + set_opt("OPT_FAKE", &OPT_FAKE, fake)?; + set_opt("OPT_FAKE_TTL", &OPT_FAKE_TTL, fake_ttl)?; + set_opt("OPT_FAKE_BADSUM", &OPT_FAKE_BADSUM, fake_badsum)?; + + #[cfg(target_os = "linux")] set_opt("OPT_QUEUE_NUM", &OPT_QUEUE_NUM, queue_num)?; + #[cfg(target_os = "linux")] set_opt("OPT_NFT_COMMAND", &OPT_NFT_COMMAND, nft_command)?; + + Ok(()) +} + +pub fn parse_args() { + if let Err(e) = parse_args_1() { + log_println!(LogLevel::Error, "{e}"); + usage(); + std::process::exit(1); + } +} @@ -18,7 +18,16 @@ use anyhow::Result; use etherparse::{IpSlice, TcpSlice}; use anyhow::anyhow; -use std::sync::OnceLock; +use std::sync::atomic::Ordering; + +use crate::log_println; + +use crate::log; +use crate::opt; +use crate::platform; +use crate::tls; + +use log::LogLevel; /// www.microsoft.com /// Stolen from github.com/bol-van/zapret/blob/master/nfq/desync.c @@ -82,17 +91,6 @@ const DEFAULT_FAKE_TLS_CLIENTHELLO: &'static [u8] = &[ 0x84, 0x4f, 0x78, 0x64, 0x30, 0x69, 0xe2, 0x1b ]; -pub static OPT_FAKE_TTL: OnceLock<u8> = OnceLock::new(); -pub static OPT_FAKE_BADSUM: OnceLock<bool> = OnceLock::new(); - -fn fake_ttl() -> u8 { - *OPT_FAKE_TTL.get().expect("OPT_FAKE_TTL not initialized") -} - -fn fake_badsum() -> bool { - *OPT_FAKE_BADSUM.get().expect("OPT_FAKE_BADSUM not initialized") -} - pub struct PktView<'a> { pub ip: IpSlice<'a>, pub tcp: TcpSlice<'a> @@ -183,14 +181,14 @@ pub fn split_packet_0( Ok(()) } -pub fn fake_clienthello( +fn fake_clienthello( view: &PktView, start: u32, end: Option<u32>, out_buf: &mut Vec<u8> ) -> Result<()> { - let tcp_checksum = if fake_badsum() { + let tcp_checksum = if opt::fake_badsum() { Some(0) } else { None @@ -198,6 +196,90 @@ pub fn fake_clienthello( split_packet_0(view, start, end, out_buf, Some(DEFAULT_FAKE_TLS_CLIENTHELLO), - Some(fake_ttl()), + Some(opt::fake_ttl()), tcp_checksum) } + +fn split_packet( + view: &PktView, + start: u32, + end: Option<u32>, + out_buf: &mut Vec<u8> +) -> Result<()> { + split_packet_0(view, start, end, out_buf, None, None, None) +} + +fn send_segment( + view: &PktView, + start: u32, + end: Option<u32>, + buf: &mut Vec<u8> +) -> Result<()> { + use platform::send_to_raw; + + if opt::fake() { + fake_clienthello(view, start, end, buf)?; + send_to_raw(buf)?; + } + split_packet(view, start, end, buf)?; + send_to_raw(buf)?; + + Ok(()) +} + +fn split_packet_1(view: &PktView, order: &[u32], buf: &mut Vec<u8>) -> Result<()> { + let mut it = order.iter().copied(); + + let Some(mut first) = it.next() else { + return Err(anyhow!("split_packet_1: invalid order array")); + }; + + for next in it { + send_segment(view, first, Some(next), buf)?; + std::thread::sleep(std::time::Duration::from_millis(opt::delay_ms())); + first = next; + } + + send_segment(view, first, None, buf)?; + + Ok(()) +} + +/// Return Ok(true) if packet is handled +pub fn handle_packet(pkt: &[u8], buf: &mut Vec::<u8>) -> Result<bool> { + #[cfg(target_os = "linux")] + let is_filtered = platform::IS_U32_SUPPORTED.load(Ordering::Relaxed); + + #[cfg(windows)] + let is_filtered = true; + + let view = PktView::from_raw(pkt)?; + + if !is_filtered && !tls::is_client_hello(view.tcp.payload()) { + return Ok(false); + } + + // TODO: if clienthello packet has been (unlikely) fragmented, + // we should find the second part and drop, reassemble it here. + + split_packet_1(&view, &[0, 1], buf)?; + + #[cfg(debug_assertions)] + log_println!(LogLevel::Debug, "packet is handled, len={}", pkt.len()); + + Ok(true) +} + +#[macro_export] +macro_rules! handle_packet { + ($bytes:expr, $buf:expr, handled => $on_handled:expr, rejected => $on_rejected:expr $(,)?) => {{ + match crate::pkt::handle_packet($bytes, $buf) { + Ok(true) => { $on_handled } + Ok(false) => { $on_rejected } + Err(e) => { + log_println!(LogLevel::Warning, "handle_packet: {e}"); + $on_rejected + } + } + }}; +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index a86daaa..5b413b4 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,4 +1,4 @@ -// Copyright 2025 Dillution <[email protected]>. +// Copyright 2025-2026 Dillution <[email protected]>. // // This file is part of DPIBreak. // @@ -19,7 +19,6 @@ use iptables::IPTables; use std::sync::{ atomic::{AtomicBool, Ordering}, Mutex, - OnceLock, LazyLock }; use std::process::{Command, Stdio}; @@ -32,20 +31,8 @@ pub static IS_XT_U32_LOADED_BY_US: AtomicBool = AtomicBool::new(false); static IS_NFT_NOT_SUPPORTED: AtomicBool = AtomicBool::new(false); const DPIBREAK_CHAIN: &str = "DPIBREAK"; - -pub static OPT_QUEUE_NUM: OnceLock<u16> = OnceLock::new(); -pub static OPT_NFT_COMMAND: OnceLock<String> = OnceLock::new(); - const INJECT_MARK: u32 = 0xD001; -fn queue_num() -> u16 { - *OPT_QUEUE_NUM.get().expect("OPT_QUEUE_NUM not initialized") -} - -fn nft_command() -> &'static str { - OPT_NFT_COMMAND.get().expect("OPT_NFT_COMMAND not initialized").as_str() -} - fn exec_process(args: &[&str], input: Option<&str>) -> Result<()> { if args.is_empty() { return Err(anyhow!("command args cannot be empty")); @@ -82,7 +69,7 @@ fn exec_process(args: &[&str], input: Option<&str>) -> Result<()> { /// Apply json format nft rules with `nft_command() -j -f -`. fn apply_nft_rules(rule: &str) -> Result<()> { - exec_process(&[nft_command(), "-j", "-f", "-"], Some(rule)) + exec_process(&[crate::opt::nft_command(), "-j", "-f", "-"], Some(rule)) } fn is_xt_u32_loaded() -> bool { @@ -132,7 +119,8 @@ fn iptables_err(e: impl ToString) -> Error { } fn install_iptables_rules(ipt: &IPTables) -> Result<()> { - let base = format!("-p tcp --dport 443 -j NFQUEUE --queue-num {} --queue-bypass", queue_num()); + let base = format!("-p tcp --dport 443 -j NFQUEUE --queue-num {} --queue-bypass", + crate::opt::queue_num()); let rule = if is_u32_supported(ipt) { const U32: &str = "-m u32 --u32 \ @@ -272,7 +260,7 @@ fn install_nft_rules() -> Result<()> { }, { "queue": { - "num": queue_num(), + "num": crate::opt::queue_num(), "flags": [ "bypass" ] } } @@ -421,8 +409,9 @@ pub fn run() -> Result<()> { use super::PACKET_SIZE_CAP; let mut q = Queue::open()?; - q.bind(queue_num())?; - log_println!(LogLevel::Info, "nfqueue: bound to queue number {}", queue_num()); + q.bind(crate::opt::queue_num())?; + log_println!(LogLevel::Info, "nfqueue: bound to queue number {}", + crate::opt::queue_num()); { // to check inturrupts let raw_fd = q.as_raw_fd(); @@ -463,7 +452,7 @@ pub fn run() -> Result<()> { q.verdict(msg)?; } } - q.unbind(queue_num())?; + q.unbind(crate::opt::queue_num())?; Ok(()) } |
