mirror of
https://github.com/egor-white/zaprett.git
synced 2025-12-10 13:30:23 +05:00
refactor
This commit is contained in:
35
rust/Cargo.lock
generated
35
rust/Cargo.lock
generated
@@ -285,6 +285,18 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getset"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -519,6 +531,28 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr2"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.103"
|
version = "1.0.103"
|
||||||
@@ -916,6 +950,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"daemonize",
|
"daemonize",
|
||||||
|
"getset",
|
||||||
"iptables",
|
"iptables",
|
||||||
"libc",
|
"libc",
|
||||||
"libnfqws",
|
"libnfqws",
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ log = "0.4.28"
|
|||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
nix = { version = "0.30.1", features = ["signal"] }
|
nix = { version = "0.30.1", features = ["signal"] }
|
||||||
iptables = { git = "https://github.com/sqlerrorthing/rust-iptables.git", branch = "feat/add-android" }
|
iptables = { git = "https://github.com/sqlerrorthing/rust-iptables.git", branch = "feat/add-android" }
|
||||||
|
getset = "0.1.6"
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ fn main() {
|
|||||||
println!("cargo:rustc-link-lib=nfnetlink");
|
println!("cargo:rustc-link-lib=nfnetlink");
|
||||||
println!("cargo:rustc-link-lib=mnl");
|
println!("cargo:rustc-link-lib=mnl");
|
||||||
|
|
||||||
if let Ok(link_libs) = env::var("NETFILTER_LIBS") {
|
let _ = env::var("NETFILTER_LIBS")
|
||||||
println!("cargo:rustc-link-search=native={link_libs}/lib");
|
.map(|libs| println!("cargo:rustc-link-search=native={libs}/lib"));
|
||||||
}
|
|
||||||
|
|
||||||
println!("cargo:rustc-link-lib=static=nfqws");
|
println!("cargo:rustc-link-lib=static=nfqws");
|
||||||
println!("cargo:rerun-if-changed={}", NFQ.display());
|
println!("cargo:rerun-if-changed={}", NFQ.display());
|
||||||
println!("cargo:rerun-if-changed={}", NFQ_CRYPTO.display());
|
println!("cargo:rerun-if-changed={}", NFQ_CRYPTO.display());
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
|
||||||
let mut builder = bindgen::Builder::default();
|
let mut builder = bindgen::Builder::default();
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ pretty_env_logger = { workspace = true }
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
nix = { workspace = true, features = ["user"] }
|
nix = { workspace = true, features = ["user"] }
|
||||||
iptables = { workspace = true }
|
iptables = { workspace = true }
|
||||||
|
getset = { workspace = true }
|
||||||
12
rust/crates/zaprett/src/cli.rs
Normal file
12
rust/crates/zaprett/src/cli.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use getset::Getters;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::commands::Command;
|
||||||
|
|
||||||
|
#[derive(Parser, Getters)]
|
||||||
|
#[command(version)]
|
||||||
|
#[getset(get = "pub")]
|
||||||
|
pub struct CliApp {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: Option<Command>,
|
||||||
|
}
|
||||||
66
rust/crates/zaprett/src/commands.rs
Normal file
66
rust/crates/zaprett/src/commands.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::{bin_version, get_autostart, module_version, restart_service, service_status, set_autostart, start_service, stop_service};
|
||||||
|
use clap::Subcommand;
|
||||||
|
use log::error;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Start the service
|
||||||
|
Start,
|
||||||
|
|
||||||
|
/// Stop the service
|
||||||
|
Stop,
|
||||||
|
|
||||||
|
/// Restart the service
|
||||||
|
Restart,
|
||||||
|
|
||||||
|
/// Show the current service status
|
||||||
|
Status,
|
||||||
|
|
||||||
|
/// Enable or disable automatic restart
|
||||||
|
SetAutostart {
|
||||||
|
/// Whether to enable (true) or disable (false) autostart
|
||||||
|
#[arg(value_parser = clap::value_parser!(bool))]
|
||||||
|
autostart: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Show whether autostart is enabled
|
||||||
|
GetAutostart,
|
||||||
|
|
||||||
|
/// Show the module version
|
||||||
|
ModuleVersion,
|
||||||
|
|
||||||
|
/// Show the nfqws binary version
|
||||||
|
BinaryVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub async fn exec(&self) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
Command::Start => return start_service().await,
|
||||||
|
Command::Stop => {
|
||||||
|
let _ = stop_service().await;
|
||||||
|
},
|
||||||
|
Command::Restart => return restart_service().await,
|
||||||
|
Command::Status => {
|
||||||
|
println!(
|
||||||
|
"zaprett is {}",
|
||||||
|
if service_status().await {
|
||||||
|
"working"
|
||||||
|
} else {
|
||||||
|
"stopped"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Command::SetAutostart { autostart } => {
|
||||||
|
if let Err(err) = set_autostart(autostart).await {
|
||||||
|
error!("Failed to set auto start: {err}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::GetAutostart => get_autostart(),
|
||||||
|
Command::ModuleVersion => module_version(),
|
||||||
|
Command::BinaryVersion => bin_version(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
65
rust/crates/zaprett/src/config.rs
Normal file
65
rust/crates/zaprett/src/config.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use getset::Getters;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::{merge_files, MODULE_PATH};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ListType {
|
||||||
|
Whitelist,
|
||||||
|
Blacklist,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Getters)]
|
||||||
|
#[getset(get = "pub")]
|
||||||
|
pub struct Config {
|
||||||
|
active_lists: Vec<String>,
|
||||||
|
active_ipsets: Vec<String>,
|
||||||
|
active_exclude_lists: Vec<String>,
|
||||||
|
active_exclude_ipsets: Vec<String>,
|
||||||
|
list_type: ListType,
|
||||||
|
strategy: String,
|
||||||
|
app_list: String,
|
||||||
|
whitelist: Vec<String>,
|
||||||
|
blacklist: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListType {
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// (hostlist arg, ipset arg)
|
||||||
|
pub async fn merge(&self, config: &Config) -> (String, String) {
|
||||||
|
let module_path_str = MODULE_PATH.to_str().unwrap();
|
||||||
|
|
||||||
|
let (host_files, ipset_files, host_suffix, ipset_suffix) = match self {
|
||||||
|
ListType::Whitelist => (
|
||||||
|
&config.active_lists,
|
||||||
|
&config.active_ipsets,
|
||||||
|
"hostlist",
|
||||||
|
"ipset",
|
||||||
|
),
|
||||||
|
ListType::Blacklist => (
|
||||||
|
&config.active_exclude_lists,
|
||||||
|
&config.active_exclude_ipsets,
|
||||||
|
"hostlist-exclude",
|
||||||
|
"ipset-exclude",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let host_path = MODULE_PATH.join(format!("tmp/{host_suffix}"));
|
||||||
|
let ipset_path = MODULE_PATH.join(format!("tmp/{ipset_suffix}"));
|
||||||
|
|
||||||
|
merge_files(host_files, host_path).await.unwrap();
|
||||||
|
merge_files(ipset_files, ipset_path).await.unwrap();
|
||||||
|
|
||||||
|
let exclude = if matches!(self, ListType::Blacklist) {
|
||||||
|
"-exclude"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
format!("--hostlist{exclude}={module_path_str}/tmp/{host_suffix}"),
|
||||||
|
format!("--ipset{exclude}={module_path_str}/tmp/{ipset_suffix}"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
268
rust/crates/zaprett/src/lib.rs
Normal file
268
rust/crates/zaprett/src/lib.rs
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
pub mod commands;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
use std::error;
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::os::raw::c_char;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use anyhow::bail;
|
||||||
|
use daemonize::Daemonize;
|
||||||
|
use ini::Ini;
|
||||||
|
use log::{error, info};
|
||||||
|
use nix::sys::signal::{kill, Signal};
|
||||||
|
use nix::unistd::{Pid, Uid};
|
||||||
|
use regex::Regex;
|
||||||
|
use sysctl::Sysctl;
|
||||||
|
use tokio::{fs, task};
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::io::{copy, AsyncReadExt, AsyncWriteExt};
|
||||||
|
use libnfqws::nfqws_main;
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
pub static MODULE_PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("/data/adb/modules/zaprett"));
|
||||||
|
pub static ZAPRETT_DIR_PATH: LazyLock<&Path> =
|
||||||
|
LazyLock::new(|| Path::new("/storage/emulated/0/zaprett"));
|
||||||
|
|
||||||
|
async fn daemonize_nfqws(args: &str) {
|
||||||
|
info!("Starting nfqws as a daemon");
|
||||||
|
let daemonize = Daemonize::new()
|
||||||
|
.pid_file(MODULE_PATH.join("tmp/pid.lock").as_path())
|
||||||
|
.working_directory("/tmp")
|
||||||
|
.group("daemon")
|
||||||
|
.privileged_action(|| "Executed before drop privileges");
|
||||||
|
|
||||||
|
match daemonize.start() {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Success, daemonized");
|
||||||
|
run_nfqws(args).await.unwrap()
|
||||||
|
}
|
||||||
|
Err(e) => error!("Error while starting nfqws daemon: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_service() -> anyhow::Result<()> {
|
||||||
|
if !Uid::effective().is_root() {
|
||||||
|
bail!("Running not from root, exiting");
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Starting zaprett service...");
|
||||||
|
|
||||||
|
let tmp_dir = MODULE_PATH.join("/tmp");
|
||||||
|
if tmp_dir.exists() {
|
||||||
|
fs::remove_dir_all(&tmp_dir).await?;
|
||||||
|
fs::create_dir_all(&tmp_dir).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut config_contents = String::new();
|
||||||
|
File::open(ZAPRETT_DIR_PATH.join("config.json"))
|
||||||
|
.await
|
||||||
|
.expect("cannot open config.json")
|
||||||
|
.read_to_string(&mut config_contents).await?;
|
||||||
|
|
||||||
|
let config: Config = serde_json::from_str(&config_contents).expect("invalid json");
|
||||||
|
|
||||||
|
let def_strat = String::from("
|
||||||
|
--filter-tcp=80 --dpi-desync=fake,split2 --dpi-desync-autottl=2 --dpi-desync-fooling=md5sig,badsum $hostlist --new
|
||||||
|
--filter-tcp=443 $hostlist --dpi-desync=fake,split2 --dpi-desync-repeats=6 --dpi-desync-fooling=md5sig,badsum --dpi-desync-fake-tls=${zaprettdir}/bin/tls_clienthello_www_google_com.bin --new
|
||||||
|
--filter-tcp=80,443 --dpi-desync=fake,disorder2 --dpi-desync-repeats=6 --dpi-desync-autottl=2 --dpi-desync-fooling=md5sig,badsum $hostlist --new
|
||||||
|
--filter-udp=50000-50100 --dpi-desync=fake --dpi-desync-any-protocol --dpi-desync-fake-quic=0xC30000000108 --new
|
||||||
|
--filter-udp=443 $hostlist --dpi-desync=fake --dpi-desync-repeats=6 --dpi-desync-fake-quic=${zaprettdir}/bin/quic_initial_www_google_com.bin --new
|
||||||
|
--filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=6 $hostlist
|
||||||
|
");
|
||||||
|
|
||||||
|
let start = fs::read_to_string(config.strategy())
|
||||||
|
.await
|
||||||
|
.unwrap_or(def_strat);
|
||||||
|
|
||||||
|
let regex_hostlist = Regex::new(r"\$hostlist")?;
|
||||||
|
let regex_ipsets = Regex::new(r"\$ipset")?;
|
||||||
|
let regex_zaprettdir = Regex::new(r"\$\{?zaprettdir}?")?;
|
||||||
|
|
||||||
|
let mut strat_modified;
|
||||||
|
let (hosts, ipsets) = config.list_type().merge(&config).await;
|
||||||
|
|
||||||
|
strat_modified = regex_hostlist.replace_all(&start, &hosts).into_owned();
|
||||||
|
strat_modified = regex_ipsets
|
||||||
|
.replace_all(&strat_modified, &ipsets)
|
||||||
|
.into_owned();
|
||||||
|
strat_modified = regex_zaprettdir
|
||||||
|
.replace_all(&strat_modified, ZAPRETT_DIR_PATH.to_str().unwrap())
|
||||||
|
.into_owned();
|
||||||
|
|
||||||
|
let ctl = sysctl::Ctl::new("net.netfilter.nf_conntrack_tcp_be_liberal")?;
|
||||||
|
ctl.set_value(sysctl::CtlValue::String("1".into()))?;
|
||||||
|
|
||||||
|
setup_iptables_rules().expect("setup iptables rules");
|
||||||
|
|
||||||
|
daemonize_nfqws(&strat_modified).await;
|
||||||
|
info!("zaprett service started!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop_service() -> anyhow::Result<()> {
|
||||||
|
if !Uid::effective().is_root() {
|
||||||
|
bail!("Running not from root, exiting");
|
||||||
|
};
|
||||||
|
|
||||||
|
clear_iptables_rules().expect("clear iptables rules");
|
||||||
|
|
||||||
|
let pid_str = fs::read_to_string(MODULE_PATH.join("tmp/pid.lock")).await?;
|
||||||
|
let pid = pid_str.trim().parse::<i32>()?;
|
||||||
|
|
||||||
|
kill(Pid::from_raw(pid), Signal::SIGKILL)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restart_service() -> anyhow::Result<()> {
|
||||||
|
stop_service().await?;
|
||||||
|
start_service().await?;
|
||||||
|
info!("zaprett service restarted!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_autostart(autostart: &bool) -> Result<(), anyhow::Error> {
|
||||||
|
if *autostart {
|
||||||
|
File::create(MODULE_PATH.join("autostart")).await?;
|
||||||
|
} else {
|
||||||
|
fs::remove_file(MODULE_PATH.join("autostart")).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_autostart() {
|
||||||
|
let file = MODULE_PATH.join("autostart");
|
||||||
|
println!("{}", file.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn service_status() -> bool {
|
||||||
|
fs::read_to_string(MODULE_PATH.join("tmp/pid.lock"))
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|pid_str| pid_str.trim().parse::<i32>().ok())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn module_version() {
|
||||||
|
if let Ok(prop) = Ini::load_from_file(MODULE_PATH.join("module.prop"))
|
||||||
|
&& let Some(props) = prop.section::<String>(None)
|
||||||
|
&& let Some(version) = props.get("version")
|
||||||
|
{
|
||||||
|
println!("{version}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bin_version() {
|
||||||
|
println!("{}", env!("ZAPRET_VERSION"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn merge_files(
|
||||||
|
input_paths: &[impl AsRef<Path>],
|
||||||
|
output_path: impl AsRef<Path>,
|
||||||
|
) -> Result<(), Box<dyn error::Error>> {
|
||||||
|
let output_path = output_path.as_ref();
|
||||||
|
let mut output_file = File::create(output_path).await?;
|
||||||
|
|
||||||
|
for input in input_paths {
|
||||||
|
let input_path = input.as_ref();
|
||||||
|
let mut input_file = File::open(input_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to open {}: {}", input_path.display(), e))?;
|
||||||
|
|
||||||
|
copy(&mut input_file, &mut output_file)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"Failed to write contents of {}: {}",
|
||||||
|
input_path.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
output_file.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_iptables_rules() -> Result<(), Box<dyn error::Error>> {
|
||||||
|
let ipt = iptables::new(false)?;
|
||||||
|
|
||||||
|
ipt.insert(
|
||||||
|
"mangle",
|
||||||
|
"POSTROUTING",
|
||||||
|
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
||||||
|
1,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
ipt.insert(
|
||||||
|
"mangle",
|
||||||
|
"PREROUTING",
|
||||||
|
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
||||||
|
1,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
ipt.append(
|
||||||
|
"filter",
|
||||||
|
"FORWARD",
|
||||||
|
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_iptables_rules() -> Result<(), Box<dyn error::Error>> {
|
||||||
|
let ipt = iptables::new(false)?;
|
||||||
|
|
||||||
|
ipt.delete(
|
||||||
|
"mangle",
|
||||||
|
"POSTROUTING",
|
||||||
|
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
ipt.delete(
|
||||||
|
"mangle",
|
||||||
|
"PREROUTING",
|
||||||
|
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
ipt.delete(
|
||||||
|
"filter",
|
||||||
|
"FORWARD",
|
||||||
|
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_nfqws(args_str: &str) -> anyhow::Result<()> {
|
||||||
|
if service_status().await {
|
||||||
|
bail!("nfqws already started!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut args = vec!["nfqws".to_string()];
|
||||||
|
|
||||||
|
if args_str.trim().is_empty() {
|
||||||
|
args.push("-v".to_string());
|
||||||
|
} else {
|
||||||
|
args.extend(args_str.split_whitespace().map(String::from));
|
||||||
|
}
|
||||||
|
|
||||||
|
task::spawn_blocking(move || {
|
||||||
|
let c_args: Vec<CString> = args
|
||||||
|
.into_iter()
|
||||||
|
.map(|arg| CString::new(arg).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut ptrs: Vec<*const c_char> = c_args.iter().map(|arg| arg.as_ptr()).collect();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
nfqws_main(c_args.len() as libc::c_int, ptrs.as_mut_ptr() as *mut _);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,403 +1,16 @@
|
|||||||
use anyhow::bail;
|
use clap::Parser;
|
||||||
use clap::{ArgAction, Parser, Subcommand, builder::BoolishValueParser};
|
use log::info;
|
||||||
use daemonize::Daemonize;
|
use zaprett::cli::CliApp;
|
||||||
use ini::Ini;
|
|
||||||
use libnfqws::nfqws_main;
|
|
||||||
use log::{error, info};
|
|
||||||
use nix::sys::signal::{Signal, kill};
|
|
||||||
use nix::unistd::{Pid, Uid};
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::ffi::CString;
|
|
||||||
use tokio::fs;
|
|
||||||
use std::os::raw::c_char;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
use std::{error};
|
|
||||||
use std::{path::Path};
|
|
||||||
use sysctl::Sysctl;
|
|
||||||
use tokio::io::{copy, AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::fs::File;
|
|
||||||
use tokio::task;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(version)]
|
|
||||||
struct Cli {
|
|
||||||
#[command(subcommand)]
|
|
||||||
cmd: Option<Commands>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Commands {
|
|
||||||
#[clap(about = "Start service")]
|
|
||||||
Start,
|
|
||||||
|
|
||||||
#[clap(about = "Stop service")]
|
|
||||||
Stop,
|
|
||||||
|
|
||||||
#[clap(about = "Restart service")]
|
|
||||||
Restart,
|
|
||||||
|
|
||||||
#[clap(about = "Show service status")]
|
|
||||||
Status,
|
|
||||||
|
|
||||||
#[clap(about = "Enable/disable autorestart")]
|
|
||||||
SetAutostart {
|
|
||||||
#[arg(
|
|
||||||
value_name = "boolean",
|
|
||||||
action = ArgAction::Set,
|
|
||||||
value_parser = BoolishValueParser::new()
|
|
||||||
)]
|
|
||||||
autostart: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[clap(about = "Get autorestart state")]
|
|
||||||
GetAutostart,
|
|
||||||
|
|
||||||
#[clap(about = "Get module version")]
|
|
||||||
ModuleVer,
|
|
||||||
|
|
||||||
#[clap(about = "Get nfqws binary version")]
|
|
||||||
BinVer,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
enum ListType {
|
|
||||||
Whitelist,
|
|
||||||
Blacklist,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ListType {
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// (hostlist arg, ipset arg)
|
|
||||||
async fn merge(&self, config: &Config) -> (String, String) {
|
|
||||||
let module_path_str = MODULE_PATH.to_str().unwrap();
|
|
||||||
|
|
||||||
let (host_files, ipset_files, host_suffix, ipset_suffix) = match self {
|
|
||||||
ListType::Whitelist => (
|
|
||||||
&config.active_lists,
|
|
||||||
&config.active_ipsets,
|
|
||||||
"hostlist",
|
|
||||||
"ipset",
|
|
||||||
),
|
|
||||||
ListType::Blacklist => (
|
|
||||||
&config.active_exclude_lists,
|
|
||||||
&config.active_exclude_ipsets,
|
|
||||||
"hostlist-exclude",
|
|
||||||
"ipset-exclude",
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let host_path = MODULE_PATH.join(format!("tmp/{host_suffix}"));
|
|
||||||
let ipset_path = MODULE_PATH.join(format!("tmp/{ipset_suffix}"));
|
|
||||||
|
|
||||||
merge_files(host_files, host_path).await.unwrap();
|
|
||||||
merge_files(ipset_files, ipset_path).await.unwrap();
|
|
||||||
|
|
||||||
let exclude = if matches!(self, ListType::Blacklist) {
|
|
||||||
"-exclude"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
format!("--hostlist{exclude}={module_path_str}/tmp/{host_suffix}"),
|
|
||||||
format!("--ipset{exclude}={module_path_str}/tmp/{ipset_suffix}"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct Config {
|
|
||||||
active_lists: Vec<String>,
|
|
||||||
active_ipsets: Vec<String>,
|
|
||||||
active_exclude_lists: Vec<String>,
|
|
||||||
active_exclude_ipsets: Vec<String>,
|
|
||||||
list_type: ListType,
|
|
||||||
strategy: String,
|
|
||||||
app_list: String,
|
|
||||||
whitelist: Vec<String>,
|
|
||||||
blacklist: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static MODULE_PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("/data/adb/modules/zaprett"));
|
|
||||||
pub static ZAPRETT_DIR_PATH: LazyLock<&Path> =
|
|
||||||
LazyLock::new(|| Path::new("/storage/emulated/0/zaprett"));
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = CliApp::parse();
|
||||||
match &cli.cmd {
|
match &cli.cmd() {
|
||||||
Some(Commands::Start) => return start_service().await,
|
Some(cmd) => cmd.exec().await?,
|
||||||
Some(Commands::Stop) => {
|
None => info!("zaprett installed. Join us: t.me/zaprett_module")
|
||||||
let _ = stop_service().await;
|
|
||||||
},
|
|
||||||
Some(Commands::Restart) => return restart_service().await,
|
|
||||||
Some(Commands::Status) => {
|
|
||||||
println!(
|
|
||||||
"zaprett is {}",
|
|
||||||
if service_status().await {
|
|
||||||
"working"
|
|
||||||
} else {
|
|
||||||
"stopped"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Some(Commands::SetAutostart { autostart }) => {
|
|
||||||
if let Err(err) = set_autostart(autostart).await {
|
|
||||||
error!("Failed to set auto start: {err}")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Commands::GetAutostart) => get_autostart(),
|
|
||||||
Some(Commands::ModuleVer) => module_version(),
|
|
||||||
Some(Commands::BinVer) => bin_version(),
|
|
||||||
None => info!("zaprett installed. Join us: t.me/zaprett_module"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn daemonize_nfqws(args: &str) {
|
|
||||||
info!("Starting nfqws as a daemon");
|
|
||||||
let daemonize = Daemonize::new()
|
|
||||||
.pid_file(MODULE_PATH.join("tmp/pid.lock").as_path())
|
|
||||||
.working_directory("/tmp")
|
|
||||||
.group("daemon")
|
|
||||||
.privileged_action(|| "Executed before drop privileges");
|
|
||||||
|
|
||||||
match daemonize.start() {
|
|
||||||
Ok(_) => {
|
|
||||||
info!("Success, daemonized");
|
|
||||||
run_nfqws(args).await.unwrap()
|
|
||||||
}
|
|
||||||
Err(e) => error!("Error while starting nfqws daemon: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_service() -> anyhow::Result<()> {
|
|
||||||
if !Uid::effective().is_root() {
|
|
||||||
bail!("Running not from root, exiting");
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("Starting zaprett service...");
|
|
||||||
|
|
||||||
let tmp_dir = MODULE_PATH.join("/tmp");
|
|
||||||
if tmp_dir.exists() {
|
|
||||||
fs::remove_dir_all(&tmp_dir).await?;
|
|
||||||
fs::create_dir_all(&tmp_dir).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut config_contents = String::new();
|
|
||||||
File::open(ZAPRETT_DIR_PATH.join("config.json"))
|
|
||||||
.await
|
|
||||||
.expect("cannot open config.json")
|
|
||||||
.read_to_string(&mut config_contents).await?;
|
|
||||||
|
|
||||||
let config: Config = serde_json::from_str(&config_contents).expect("invalid json");
|
|
||||||
|
|
||||||
let def_strat = String::from("
|
|
||||||
--filter-tcp=80 --dpi-desync=fake,split2 --dpi-desync-autottl=2 --dpi-desync-fooling=md5sig,badsum $hostlist --new
|
|
||||||
--filter-tcp=443 $hostlist --dpi-desync=fake,split2 --dpi-desync-repeats=6 --dpi-desync-fooling=md5sig,badsum --dpi-desync-fake-tls=${zaprettdir}/bin/tls_clienthello_www_google_com.bin --new
|
|
||||||
--filter-tcp=80,443 --dpi-desync=fake,disorder2 --dpi-desync-repeats=6 --dpi-desync-autottl=2 --dpi-desync-fooling=md5sig,badsum $hostlist --new
|
|
||||||
--filter-udp=50000-50100 --dpi-desync=fake --dpi-desync-any-protocol --dpi-desync-fake-quic=0xC30000000108 --new
|
|
||||||
--filter-udp=443 $hostlist --dpi-desync=fake --dpi-desync-repeats=6 --dpi-desync-fake-quic=${zaprettdir}/bin/quic_initial_www_google_com.bin --new
|
|
||||||
--filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=6 $hostlist
|
|
||||||
");
|
|
||||||
|
|
||||||
let start = fs::read_to_string(&config.strategy)
|
|
||||||
.await
|
|
||||||
.unwrap_or(def_strat);
|
|
||||||
|
|
||||||
let regex_hostlist = Regex::new(r"\$hostlist")?;
|
|
||||||
let regex_ipsets = Regex::new(r"\$ipset")?;
|
|
||||||
let regex_zaprettdir = Regex::new(r"\$\{?zaprettdir}?")?;
|
|
||||||
|
|
||||||
let mut strat_modified;
|
|
||||||
let (hosts, ipsets) = config.list_type.merge(&config).await;
|
|
||||||
|
|
||||||
strat_modified = regex_hostlist.replace_all(&start, &hosts).into_owned();
|
|
||||||
strat_modified = regex_ipsets
|
|
||||||
.replace_all(&strat_modified, &ipsets)
|
|
||||||
.into_owned();
|
|
||||||
strat_modified = regex_zaprettdir
|
|
||||||
.replace_all(&strat_modified, ZAPRETT_DIR_PATH.to_str().unwrap())
|
|
||||||
.into_owned();
|
|
||||||
|
|
||||||
let ctl = sysctl::Ctl::new("net.netfilter.nf_conntrack_tcp_be_liberal")?;
|
|
||||||
ctl.set_value(sysctl::CtlValue::String("1".into()))?;
|
|
||||||
|
|
||||||
setup_iptables_rules().expect("setup iptables rules");
|
|
||||||
|
|
||||||
daemonize_nfqws(&strat_modified).await;
|
|
||||||
info!("zaprett service started!");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stop_service() -> anyhow::Result<()> {
|
|
||||||
if !Uid::effective().is_root() {
|
|
||||||
bail!("Running not from root, exiting");
|
|
||||||
};
|
|
||||||
|
|
||||||
clear_iptables_rules().expect("clear iptables rules");
|
|
||||||
|
|
||||||
let pid_str = fs::read_to_string(MODULE_PATH.join("tmp/pid.lock")).await?;
|
|
||||||
let pid = pid_str.trim().parse::<i32>()?;
|
|
||||||
|
|
||||||
kill(Pid::from_raw(pid), Signal::SIGKILL)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restart_service() -> anyhow::Result<()> {
|
|
||||||
stop_service().await?;
|
|
||||||
start_service().await?;
|
|
||||||
info!("zaprett service restarted!");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_autostart(autostart: &bool) -> Result<(), anyhow::Error> {
|
|
||||||
if *autostart {
|
|
||||||
File::create(MODULE_PATH.join("autostart")).await?;
|
|
||||||
} else {
|
|
||||||
fs::remove_file(MODULE_PATH.join("autostart")).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_autostart() {
|
|
||||||
let file = MODULE_PATH.join("autostart");
|
|
||||||
println!("{}", file.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn service_status() -> bool {
|
|
||||||
fs::read_to_string(MODULE_PATH.join("tmp/pid.lock"))
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|pid_str| pid_str.trim().parse::<i32>().ok())
|
|
||||||
.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn module_version() {
|
|
||||||
if let Ok(prop) = Ini::load_from_file(MODULE_PATH.join("module.prop"))
|
|
||||||
&& let Some(props) = prop.section::<String>(None)
|
|
||||||
&& let Some(version) = props.get("version")
|
|
||||||
{
|
|
||||||
println!("{version}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bin_version() {
|
|
||||||
println!("{}", env!("ZAPRET_VERSION"));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn merge_files(
|
|
||||||
input_paths: &[impl AsRef<Path>],
|
|
||||||
output_path: impl AsRef<Path>,
|
|
||||||
) -> Result<(), Box<dyn error::Error>> {
|
|
||||||
let output_path = output_path.as_ref();
|
|
||||||
let mut output_file = File::create(output_path).await?;
|
|
||||||
|
|
||||||
for input in input_paths {
|
|
||||||
let input_path = input.as_ref();
|
|
||||||
let mut input_file = File::open(input_path)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to open {}: {}", input_path.display(), e))?;
|
|
||||||
|
|
||||||
copy(&mut input_file, &mut output_file)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"Failed to write contents of {}: {}",
|
|
||||||
input_path.display(),
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
output_file.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_iptables_rules() -> Result<(), Box<dyn error::Error>> {
|
|
||||||
let ipt = iptables::new(false)?;
|
|
||||||
|
|
||||||
ipt.insert(
|
|
||||||
"mangle",
|
|
||||||
"POSTROUTING",
|
|
||||||
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
|
||||||
1,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
ipt.insert(
|
|
||||||
"mangle",
|
|
||||||
"PREROUTING",
|
|
||||||
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
|
||||||
1,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
ipt.append(
|
|
||||||
"filter",
|
|
||||||
"FORWARD",
|
|
||||||
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_iptables_rules() -> Result<(), Box<dyn error::Error>> {
|
|
||||||
let ipt = iptables::new(false)?;
|
|
||||||
|
|
||||||
ipt.delete(
|
|
||||||
"mangle",
|
|
||||||
"POSTROUTING",
|
|
||||||
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
ipt.delete(
|
|
||||||
"mangle",
|
|
||||||
"PREROUTING",
|
|
||||||
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
ipt.delete(
|
|
||||||
"filter",
|
|
||||||
"FORWARD",
|
|
||||||
"-j NFQUEUE --queue-num 200 --queue-bypass",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_nfqws(args_str: &str) -> anyhow::Result<()> {
|
|
||||||
if service_status().await {
|
|
||||||
bail!("nfqws already started!");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut args = vec!["nfqws".to_string()];
|
|
||||||
|
|
||||||
if args_str.trim().is_empty() {
|
|
||||||
args.push("-v".to_string());
|
|
||||||
} else {
|
|
||||||
args.extend(args_str.split_whitespace().map(String::from));
|
|
||||||
}
|
|
||||||
|
|
||||||
task::spawn_blocking(move || {
|
|
||||||
let c_args: Vec<CString> = args
|
|
||||||
.into_iter()
|
|
||||||
.map(|arg| CString::new(arg).unwrap())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut ptrs: Vec<*const c_char> = c_args.iter().map(|arg| arg.as_ptr()).collect();
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
nfqws_main(c_args.len() as libc::c_int, ptrs.as_mut_ptr() as *mut _);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user