diff options
Diffstat (limited to 'src/platform/linux')
| -rw-r--r-- | src/platform/linux/iptables.rs | 173 | ||||
| -rw-r--r-- | src/platform/linux/nftables.rs | 124 |
2 files changed, 297 insertions, 0 deletions
diff --git a/src/platform/linux/iptables.rs b/src/platform/linux/iptables.rs new file mode 100644 index 0000000..91fa1da --- /dev/null +++ b/src/platform/linux/iptables.rs @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> +// SPDX-License-Identifier: GPL-3.0-or-later + +use anyhow::{Result, Error}; + +use std::sync::{ + atomic::{AtomicBool, Ordering} +}; + +use crate::{log::LogLevel, log_println, opt}; +use super::{exec_process, INJECT_MARK, 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, +} + +impl IPTables { + pub fn new(is_ipv6: bool) -> Result<Self> { + Ok(Self { + cmd: if is_ipv6 { "ip6tables" } else { "iptables" }, + }) + } + + fn run(&self, args: &[&str]) -> Result<()> { + let mut full_args = Vec::with_capacity(args.len() + 1); + + full_args.push(self.cmd); + full_args.extend_from_slice(args); + + exec_process(&full_args, None) + } + + pub fn new_chain(&self, table: &str, chain: &str) -> Result<()> { + self.run(&["-t", table, "-N", chain]) + } + + pub fn flush_chain(&self, table: &str, chain: &str) -> Result<()> { + self.run(&["-t", table, "-F", chain]) + } + + pub fn delete_chain(&self, table: &str, chain: &str) -> Result<()> { + self.run(&["-t", table, "-X", chain]) + } + + pub fn insert(&self, table: &str, chain: &str, rule: &[&str], pos: i32) -> Result<()> { + let pos_str = pos.to_string(); + let mut args = vec!["-t", table, "-I", chain, &pos_str]; + args.extend_from_slice(rule); + self.run(&args) + } + + pub fn append(&self, table: &str, chain: &str, rule: &[&str]) -> Result<()> { + let mut args = vec!["-t", table, "-A", chain]; + args.extend_from_slice(rule); + self.run(&args) + } + + pub fn delete(&self, table: &str, chain: &str, rule: &[&str]) -> Result<()> { + let mut args = vec!["-t", table, "-D", chain]; + args.extend_from_slice(rule); + self.run(&args) + } +} + +fn is_xt_u32_loaded() -> bool { + std::fs::read_to_string("/proc/modules") + .map(|s| s.lines().any(|l| l.starts_with("xt_u32 "))) + .unwrap_or(false) +} + +fn ensure_xt_u32() -> Result<()> { + let before = is_xt_u32_loaded(); + _ = exec_process(&["modprobe", "-q", "xt_u32"], None); + let after = is_xt_u32_loaded(); + + if !before && after { + IS_XT_U32_LOADED_BY_US.store(true, Ordering::Relaxed); + } + Ok(()) +} + +fn is_u32_supported(ipt: &IPTables) -> bool { + if IS_U32_SUPPORTED.load(Ordering::Relaxed) { + return true; + } + + if ensure_xt_u32().is_err() { + log_println!(LogLevel::Warning, "xt_u32 not supported"); + return false; + } + + log_println!(LogLevel::Info, "xt_u32 loaded"); + + let rule = ["-m", "u32", "--u32", "0x0=0x0", "-j", "RETURN"]; + + match ipt.insert("raw", "PREROUTING", &rule, 1) { + Ok(_) => { + _ = ipt.delete("raw", "PREROUTING", &rule); + IS_U32_SUPPORTED.store(true, Ordering::Relaxed); + true + } + + Err(_) => false + } +} + +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)?; + log_println!(LogLevel::Info, "{}: new chain {} on table mangle", ipt.cmd, DPIBREAK_CHAIN); + + ipt.insert("mangle", "POSTROUTING", &["-j", DPIBREAK_CHAIN], 1).map_err(iptables_err)?; + log_println!(LogLevel::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() { + log_println!(LogLevel::Info, "{}: delete jump to {} from mangle/POSTROUTING", ipt.cmd, DPIBREAK_CHAIN); + } + + if ipt.flush_chain("mangle", DPIBREAK_CHAIN).is_ok() { + log_println!(LogLevel::Info, "{}: flush chain {}", ipt.cmd, DPIBREAK_CHAIN); + } + + if ipt.delete_chain("mangle", DPIBREAK_CHAIN).is_ok() { + log_println!(LogLevel::Info, "{}: delete chain {}", ipt.cmd, DPIBREAK_CHAIN); + } + + Ok(()) +} + +pub fn cleanup_xt_u32() -> Result<()> { + if IS_XT_U32_LOADED_BY_US.load(Ordering::Relaxed) { + exec_process(&["modprobe", "-q", "-r", "xt_u32"], None)?; + log_println!(LogLevel::Info, "cleanup: unload xt_u32"); + } + + Ok(()) +} diff --git a/src/platform/linux/nftables.rs b/src/platform/linux/nftables.rs new file mode 100644 index 0000000..ccb918e --- /dev/null +++ b/src/platform/linux/nftables.rs @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::sync::atomic::Ordering; +use anyhow::Result; + +use crate::{log::LogLevel, log_println, opt}; +use super::{exec_process, INJECT_MARK, IS_U32_SUPPORTED}; + +const DPIBREAK_TABLE: &str = "dpibreak"; + +/// Apply json format nft rules with `nft_command() -j -f -`. +fn apply_nft_rules(rule: &str) -> Result<()> { + exec_process(&[crate::opt::nft_command(), "-j", "-f", "-"], Some(rule)) +} + +pub fn install_nft_rules() -> Result<()> { + let rule = serde_json::json!( + { + "nftables": [ + {"add": {"table": {"family": "inet", "name": DPIBREAK_TABLE}}}, + // Clienthello + { + "add": { + "chain": { + "family": "inet", + "table": DPIBREAK_TABLE, + "name": "OUTPUT", + "type": "filter", + "hook": "output", + "prio": 0, + "policy": "accept", + } + } + }, + { + "add": { + "rule": { + "family": "inet", + "table": DPIBREAK_TABLE, + "chain": "OUTPUT", + "expr": [ + { + "match": { + "left": { "meta": { "key": "mark" }}, + "op": "==", + "right": INJECT_MARK + } + }, + { "return": null } + ] + } + } + }, + { + "add": { + "rule": { + "family": "inet", + "table": DPIBREAK_TABLE, + "chain": "OUTPUT", + "expr": [ + { + "match": { + "left": {"payload": { "protocol": "tcp", "field": "dport" }}, + "op": "==", + "right": 443 + } + }, + // TLS ContentType == 0x16 (Handshake) + { + "match": { + "left": { "payload": { "base": "ih", "offset": 0, "len": 8 } }, + "op": "==", + "right": 0x16 + } + }, + // HandshakeType == 0x01 (ClientHello) + { + "match": { + // Note: offset and len are both "bit" unit not byte + "left": { "payload": { "base": "ih", "offset": 40, "len": 8 } }, + "op": "==", + "right": 0x01 + } + }, + { + "queue": { + "num": crate::opt::queue_num(), + "flags": [ "bypass" ] + } + } + ] + } + } + } + ] + } + ); + + apply_nft_rules(&serde_json::to_string(&rule)?)?; + log_println!(LogLevel::Info, + "nftables: add chain OUTPUT, match ClientHello -> queue {})", + opt::queue_num()); + log_println!(LogLevel::Debug, "nftables: rule json={}", rule); + + // clienthello filtered by nft + IS_U32_SUPPORTED.store(true, Ordering::Relaxed); + log_println!(LogLevel::Info, "nftables: create table inet {DPIBREAK_TABLE}"); + + Ok(()) +} + +pub fn cleanup_nftables_rules() -> Result<()> { + // nft delete table inet dpibreak + let rule = serde_json::json!({ + "nftables": [ + {"delete": {"table": {"family": "inet", "name": DPIBREAK_TABLE}}} + ] + }); + apply_nft_rules(&serde_json::to_string(&rule)?)?; + log_println!(LogLevel::Info, "cleanup: nftables: delete table inet {}", DPIBREAK_TABLE); + + Ok(()) +} |
