diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/pkt.rs | 4 | ||||
| -rw-r--r-- | src/platform.rs | 2 | ||||
| -rw-r--r-- | src/platform/linux.rs | 116 | ||||
| -rw-r--r-- | src/platform/linux/nftables.rs | 39 | ||||
| -rw-r--r-- | src/platform/linux/rules.rs | 191 | ||||
| -rw-r--r-- | src/platform/linux/rules/iptables.rs (renamed from src/platform/linux/iptables.rs) | 77 |
6 files changed, 211 insertions, 218 deletions
@@ -18,8 +18,6 @@ use anyhow::Result; use etherparse::{IpSlice, TcpSlice}; use anyhow::anyhow; -#[cfg(target_os = "linux")] -use std::sync::atomic::Ordering; use crate::opt; use crate::platform; @@ -236,7 +234,7 @@ pub fn put_hop(pkt: &[u8]) { /// 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); + let is_filtered = platform::is_kernel_filtered_clienthello(); #[cfg(windows)] let is_filtered = true; diff --git a/src/platform.rs b/src/platform.rs index 5b74eb9..1ec9b39 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -30,7 +30,7 @@ pub use windows::{bootstrap, run, local_time, send_to_raw}; pub mod linux; #[cfg(target_os = "linux")] -pub use linux::{bootstrap, run, local_time, send_to_raw, IS_U32_SUPPORTED}; +pub use linux::{bootstrap, run, local_time, send_to_raw, is_kernel_filtered_clienthello}; /// pause before exit on windows to print information in console before it is closed. pub fn paexit(code: i32) { diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 03dd94e..1db2e09 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -3,118 +3,25 @@ use std::{ os::fd::{AsRawFd, OwnedFd}, - sync::{LazyLock, atomic::AtomicBool} + sync::{LazyLock, atomic} }; use std::fs::OpenOptions; -use std::process::{Command, Stdio}; use std::io::Write; -use anyhow::{Result, Context, anyhow}; +use anyhow::{Result, Context}; use socket2::{Domain, Protocol, Socket, Type}; -mod iptables; -mod nftables; +mod rules; mod rxring; #[macro_use] mod libc_s; -use iptables::*; -use nftables::*; - use crate::pkt; use crate::opt; -pub static IS_U32_SUPPORTED: AtomicBool = AtomicBool::new(false); - const INJECT_MARK: u32 = 0xD001; const PID_FILE: &str = "/run/dpibreak.pid"; // TODO: unmagic this const PKG_NAME: &str = env!("CARGO_PKG_NAME"); -fn exec_process(args: &[&str], input: Option<&str>) -> Result<()> { - if args.is_empty() { - return Err(anyhow!("command args cannot be empty")); - } - - let program = args[0]; - let stdin_mode = if input.is_some() { Stdio::piped() } else { Stdio::null() }; - - let mut child = Command::new(program) - .args(&args[1..]) - .stdin(stdin_mode) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .spawn() - .with_context(|| format!("failed to spawn {}", program))?; - - if let Some(data) = input { - if let Some(mut stdin) = child.stdin.take() { - stdin.write_all(data.as_bytes()) - .with_context(|| format!("failed to write input to {}", program))?; - } - } - - let output = child.wait_with_output() - .with_context(|| format!("failed to wait for {}", program))?; - - match output.status.code() { - Some(0) => Ok(()), - Some(code) => Err(anyhow!("{} exited with status {}: {}", program, code, - String::from_utf8_lossy(&output.stderr))), - None => Err(anyhow!("{} terminated by signal", program)) - } -} - -struct InstalledRules { - is_nft_not_supported: bool, - ipt: Option<IPTables>, - ip6: Option<IPTables> -} - -fn install_ipt6(is_ipv6: bool) -> Option<IPTables> { - let ipt = IPTables::new(is_ipv6).map_err(|e| crate::warn!("iptables: {e}")).ok(); - if let Some(ref ipt) = ipt { - ipt.install().map_err(|e| crate::warn!("iptables: {e}")).ok(); - } - - ipt -} - -fn install_rules() -> Result<InstalledRules> { - let mut is_nft_not_supported = false; - let mut ipt = None; - let mut ip6 = None; - - if let Err(e) = install_nft_rules() { - is_nft_not_supported = true; - crate::warn!("nftables: {}", e.to_string()); - crate::warn!("fallback to iptables"); - - ipt = install_ipt6(false); - ip6 = install_ipt6(true); - } - - Ok(InstalledRules{ - is_nft_not_supported, - ipt, - ip6 - }) -} - -impl Drop for InstalledRules { - fn drop(&mut self) { - if self.is_nft_not_supported { - if let Some(ipt) = &self.ipt { - ipt.cleanup().map_err(|e| crate::warn!("fail to cleanup iptables rules: {e}")).ok(); - } - if let Some(ipt) = &self.ip6 { - ipt.cleanup().map_err(|e| crate::warn!("fail to cleanup ip6tables rules: {e}")).ok(); - } - cleanup_xt_u32().map_err(|e| crate::warn!("fail to cleanup xt_u32: {e}")).ok(); - } else { - cleanup_nftables_rules().map_err(|e| crate::warn!("fail to cleanup nftables rules: {e}")).ok(); - } - } -} - fn lock_pid_file() -> Result<()> { use libc_s::flock; @@ -279,21 +186,16 @@ fn open_signalfd() -> Result<OwnedFd> { } } -fn ipt6_cleanup(is_ipv6: bool) -> Result<()> { - let ipt6 = IPTables::new(is_ipv6)?; - ipt6.cleanup() -} - pub fn run() -> Result<()> { use crate::handle_packet; use super::PACKET_SIZE_CAP; // In case the previous execution was not cleaned properly - _ = cleanup_nftables_rules(); - _ = ipt6_cleanup(false); - _ = ipt6_cleanup(true); + _ = rules::nft_cleanup(); + _ = rules::ipt6_cleanup(false); + _ = rules::ipt6_cleanup(true); - let _rule = install_rules()?; + let _rule = rules::install()?; let sfd = open_signalfd()?; let mut q = open_nfqueue()?; @@ -400,3 +302,7 @@ pub fn local_time() -> (i32, u8, u8, u8, u8, u8) { tm.tm_hour as u8, tm.tm_min as u8, tm.tm_sec as u8) } } + +pub fn is_kernel_filtered_clienthello() -> bool { + rules::IS_U32_SUPPORTED.load(atomic::Ordering::Relaxed) +} diff --git a/src/platform/linux/nftables.rs b/src/platform/linux/nftables.rs deleted file mode 100644 index 4809000..0000000 --- a/src/platform/linux/nftables.rs +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> -// SPDX-License-Identifier: GPL-3.0-or-later - -use std::sync::atomic::Ordering; -use anyhow::Result; - -use crate::opt; -use super::{exec_process, INJECT_MARK, IS_U32_SUPPORTED}; - -const DPIBREAK_TABLE: &str = "dpibreak"; - -/// Apply nft rules with `nft_command() -f -`. -fn nft(rule: &str) -> Result<()> { - crate::info!("nft: {rule}"); - exec_process(&[opt::nft_command(), "-f", "-"], Some(rule)) -} - -pub fn install_nft_rules() -> Result<()> { - let queue_num = opt::queue_num(); - let rule = format!( - r#"add table inet {DPIBREAK_TABLE} -add chain inet {DPIBREAK_TABLE} OUTPUT {{ type filter hook output priority 0; policy accept; }} -add rule inet {DPIBREAK_TABLE} OUTPUT meta mark {INJECT_MARK} return -add rule inet {DPIBREAK_TABLE} OUTPUT tcp dport 443 @ih,0,8 0x16 @ih,40,8 0x01 queue num {queue_num} bypass"# - ); - nft(&rule)?; - - // clienthello filtered by nft - IS_U32_SUPPORTED.store(true, Ordering::Relaxed); - - Ok(()) -} - -pub fn cleanup_nftables_rules() -> Result<()> { - let rule = format!("delete table inet {DPIBREAK_TABLE}"); - nft(&rule)?; - - Ok(()) -} diff --git a/src/platform/linux/rules.rs b/src/platform/linux/rules.rs new file mode 100644 index 0000000..7dcdc36 --- /dev/null +++ b/src/platform/linux/rules.rs @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::sync::atomic; +use std::process::{Command, Stdio}; +use std::io::Write; +use anyhow::{Result, Context, anyhow}; + +mod iptables; + +use iptables::{IPTables, cleanup_xt_u32}; + +use crate::opt; +use super::INJECT_MARK; + +const DPIBREAK_CHAIN: &str = "DPIBREAK"; +const DPIBREAK_TABLE: &str = "dpibreak"; +pub static IS_U32_SUPPORTED: atomic::AtomicBool = atomic::AtomicBool::new(false); + +fn exec_process(args: &[&str], input: Option<&str>) -> Result<()> { + if args.is_empty() { + return Err(anyhow!("command args cannot be empty")); + } + + let program = args[0]; + let stdin_mode = if input.is_some() { Stdio::piped() } else { Stdio::null() }; + + let mut child = Command::new(program) + .args(&args[1..]) + .stdin(stdin_mode) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .with_context(|| format!("failed to spawn {}", program))?; + + if let Some(data) = input { + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(data.as_bytes()) + .with_context(|| format!("failed to write input to {}", program))?; + } + } + + let output = child.wait_with_output() + .with_context(|| format!("failed to wait for {}", program))?; + + match output.status.code() { + Some(0) => Ok(()), + Some(code) => Err(anyhow!("{} exited with status {}: {}", program, code, + String::from_utf8_lossy(&output.stderr))), + None => Err(anyhow!("{} terminated by signal", program)) + } +} + +/// Apply nft rules with `nft_command() -f -`. +fn nft(rule: &str) -> Result<()> { + crate::info!("nft: {rule}"); + exec_process(&[opt::nft_command(), "-f", "-"], Some(rule)) +} + +pub struct InstalledRules { + is_nft_not_supported: bool, + ipt: Option<IPTables>, + ip6: Option<IPTables> +} + +fn install_ipt6(is_ipv6: bool) -> Option<IPTables> { + let ipt = IPTables::new(is_ipv6).map_err(|e| crate::warn!("iptables: {e}")).ok(); + if let Some(ref ipt) = ipt { + ipt.install().map_err(|e| crate::warn!("iptables: {e}")).ok(); + } + + ipt +} + +pub fn install() -> Result<InstalledRules> { + let mut is_nft_not_supported = false; + let mut ipt = None; + let mut ip6 = None; + + if let Err(e) = install_nft_rules() { + is_nft_not_supported = true; + crate::warn!("nftables: {}", e.to_string()); + crate::warn!("fallback to iptables"); + + ipt = install_ipt6(false); + ip6 = install_ipt6(true); + } + + Ok(InstalledRules{ + is_nft_not_supported, + ipt, + ip6 + }) +} + +impl Drop for InstalledRules { + fn drop(&mut self) { + if self.is_nft_not_supported { + if let Some(ipt) = &self.ipt { + ipt.cleanup().map_err(|e| crate::warn!("fail to cleanup iptables rules: {e}")).ok(); + } + if let Some(ipt) = &self.ip6 { + ipt.cleanup().map_err(|e| crate::warn!("fail to cleanup ip6tables rules: {e}")).ok(); + } + cleanup_xt_u32().map_err(|e| crate::warn!("fail to cleanup xt_u32: {e}")).ok(); + } else { + nft_cleanup().map_err(|e| crate::warn!("fail to cleanup nftables rules: {e}")).ok(); + } + } +} + +pub fn ipt6_cleanup(is_ipv6: bool) -> Result<()> { + let ipt6 = IPTables::new(is_ipv6)?; + ipt6.cleanup() +} + +pub fn nft_cleanup() -> Result<()> { + let rule = format!("delete table inet {DPIBREAK_TABLE}"); + nft(&rule)?; + + Ok(()) +} + +fn install_nft_rules() -> Result<()> { + let queue_num = opt::queue_num(); + let rule = format!( + r#"add table inet {DPIBREAK_TABLE} +add chain inet {DPIBREAK_TABLE} OUTPUT {{ type filter hook output priority 0; policy accept; }} +add rule inet {DPIBREAK_TABLE} OUTPUT meta mark {INJECT_MARK} return +add rule inet {DPIBREAK_TABLE} OUTPUT tcp dport 443 @ih,0,8 0x16 @ih,40,8 0x01 queue num {queue_num} bypass"# + ); + nft(&rule)?; + + // clienthello filtered by nft + IS_U32_SUPPORTED.store(true, atomic::Ordering::Relaxed); + + Ok(()) +} + +impl IPTables { + fn install(&self) -> Result<()> { + let q_num = crate::opt::queue_num().to_string(); + // prevent inf loop + let mark = format!("{:#x}", INJECT_MARK); + + let mut rule = vec![ + "-p", "tcp", "--dport", "443", + "-j", "NFQUEUE", "--queue-num", &q_num, "--queue-bypass" + ]; + + if iptables::is_u32_supported(self) { + const U32: &str = "0>>22&0x3C @ 12>>26&0x3C @ 0>>24&0xFF=0x16 && \ + 0>>22&0x3C @ 12>>26&0x3C @ 2>>24&0xFF=0x01"; + + rule.extend_from_slice(&["-m", "u32", "--u32", U32]); + } + + self.new_chain("mangle", DPIBREAK_CHAIN)?; + + self.insert( + "mangle", + DPIBREAK_CHAIN, + &["-m", "mark", "--mark", &mark, "-j", "RETURN"], + 1 + )?; + + self.append("mangle", DPIBREAK_CHAIN, &rule)?; + crate::info!("{}: new chain {} on table mangle", self.cmd(), DPIBREAK_CHAIN); + + self.insert("mangle", "POSTROUTING", &["-j", DPIBREAK_CHAIN], 1)?; + crate::info!("{}: add jump to {} chain on POSTROUTING", self.cmd(), DPIBREAK_CHAIN); + + Ok(()) + } + + fn cleanup(&self) -> Result<()> { + if self.delete("mangle", "POSTROUTING", &["-j", DPIBREAK_CHAIN]).is_ok() { + crate::info!("{}: delete jump to {} from mangle/POSTROUTING", self.cmd(), DPIBREAK_CHAIN); + } + + if self.flush_chain("mangle", DPIBREAK_CHAIN).is_ok() { + crate::info!("{}: flush chain {}", self.cmd(), DPIBREAK_CHAIN); + } + + if self.delete_chain("mangle", DPIBREAK_CHAIN).is_ok() { + crate::info!("{}: delete chain {}", self.cmd(), DPIBREAK_CHAIN); + } + + Ok(()) + } +} diff --git a/src/platform/linux/iptables.rs b/src/platform/linux/rules/iptables.rs index af8957f..c8eed2d 100644 --- a/src/platform/linux/iptables.rs +++ b/src/platform/linux/rules/iptables.rs @@ -1,18 +1,16 @@ // SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> // SPDX-License-Identifier: GPL-3.0-or-later -use anyhow::{Result, Error}; +use anyhow::{Result}; use std::sync::{ atomic::{AtomicBool, Ordering} }; -use super::{exec_process, INJECT_MARK, IS_U32_SUPPORTED}; +use super::{exec_process, IS_U32_SUPPORTED}; static IS_XT_U32_LOADED_BY_US: AtomicBool = AtomicBool::new(false); -const DPIBREAK_CHAIN: &str = "DPIBREAK"; - pub struct IPTables { cmd: &'static str, } @@ -63,6 +61,10 @@ impl IPTables { args.extend_from_slice(rule); self.run(&args) } + + pub fn cmd(&self) -> &'static str { + self.cmd + } } fn is_xt_u32_loaded() -> bool { @@ -82,7 +84,7 @@ fn ensure_xt_u32() -> Result<()> { Ok(()) } -fn is_u32_supported(ipt: &IPTables) -> bool { +pub fn is_u32_supported(ipt: &IPTables) -> bool { if IS_U32_SUPPORTED.load(Ordering::Relaxed) { return true; } @@ -107,71 +109,6 @@ fn is_u32_supported(ipt: &IPTables) -> bool { } } -pub fn iptables_err(e: impl ToString) -> Error { - Error::msg(format!("iptables: {}", e.to_string())) -} - -pub fn install_iptables_rules(ipt: &IPTables) -> Result<()> { - let q_num = crate::opt::queue_num().to_string(); - // prevent inf loop - let mark = format!("{:#x}", INJECT_MARK); - - let mut rule = vec![ - "-p", "tcp", "--dport", "443", - "-j", "NFQUEUE", "--queue-num", &q_num, "--queue-bypass" - ]; - - if is_u32_supported(ipt) { - const U32: &str = "0>>22&0x3C @ 12>>26&0x3C @ 0>>24&0xFF=0x16 && \ - 0>>22&0x3C @ 12>>26&0x3C @ 2>>24&0xFF=0x01"; - - rule.extend_from_slice(&["-m", "u32", "--u32", U32]); - } - - ipt.new_chain("mangle", DPIBREAK_CHAIN).map_err(iptables_err)?; - - ipt.insert( - "mangle", - DPIBREAK_CHAIN, - &["-m", "mark", "--mark", &mark, "-j", "RETURN"], - 1 - ).map_err(iptables_err)?; - - ipt.append("mangle", DPIBREAK_CHAIN, &rule).map_err(iptables_err)?; - crate::info!("{}: new chain {} on table mangle", ipt.cmd, DPIBREAK_CHAIN); - - ipt.insert("mangle", "POSTROUTING", &["-j", DPIBREAK_CHAIN], 1).map_err(iptables_err)?; - crate::info!("{}: add jump to {} chain on POSTROUTING", ipt.cmd, DPIBREAK_CHAIN); - - Ok(()) -} - -pub fn cleanup_iptables_rules(ipt: &IPTables) -> Result<()> { - if ipt.delete("mangle", "POSTROUTING", &["-j", DPIBREAK_CHAIN]).is_ok() { - crate::info!("{}: delete jump to {} from mangle/POSTROUTING", ipt.cmd, DPIBREAK_CHAIN); - } - - if ipt.flush_chain("mangle", DPIBREAK_CHAIN).is_ok() { - crate::info!("{}: flush chain {}", ipt.cmd, DPIBREAK_CHAIN); - } - - if ipt.delete_chain("mangle", DPIBREAK_CHAIN).is_ok() { - crate::info!("{}: delete chain {}", ipt.cmd, DPIBREAK_CHAIN); - } - - Ok(()) -} - -impl IPTables { - pub fn install(&self) -> Result<()> { - install_iptables_rules(&self) - } - - pub fn cleanup(&self) -> Result<()> { - cleanup_iptables_rules(&self) - } -} - pub fn cleanup_xt_u32() -> Result<()> { if IS_XT_U32_LOADED_BY_US.load(Ordering::Relaxed) { exec_process(&["modprobe", "-q", "-r", "xt_u32"], None)?; |
