1.0 能用
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/.cargo
|
||||
/.idea
|
||||
Generated
+1966
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "core_vpn"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
ipstack = "1.0.1"
|
||||
tun-rs = { version = "2.8.13", features = ["async_tokio"] }
|
||||
tokio = { version = "1.47.5", features = ["full"] }
|
||||
tokio-rustls = "0.26"
|
||||
rustls-pemfile = "2"
|
||||
jni = "0.22.4"
|
||||
rcgen = "0.14"
|
||||
anyhow = "1"
|
||||
bytes = "1"
|
||||
tokio-util = "0.7.20"
|
||||
rustls = "0.23.46"
|
||||
rustls-native-certs = "0.8.5"
|
||||
webpki-roots = "1"
|
||||
regex = "1.13.1"
|
||||
hyper = { version = "1.0", features = ["server", "client", "http1"] }
|
||||
hyper-util = { version = "0.1", features = ["tokio", "server", "client"] }
|
||||
http-body-util = "0.1"
|
||||
http = "1.5.0"
|
||||
|
||||
[build-dependencies]
|
||||
rcgen = { version = "0.14", features = ["x509-parser"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
@@ -0,0 +1,156 @@
|
||||
use rcgen::{
|
||||
CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair,
|
||||
KeyUsagePurpose, SanType,
|
||||
};
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
const CA_KEY: &str = "-----BEGIN PRIVATE KEY-----
|
||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAShZANiAAQTglHNUqyS
|
||||
mMHIqtl3Mh3rl+cJvQtMoKylXcitUdz8nRWJoVl+OlEg4e/WMcY+GDXKyuKYaaYu
|
||||
FjHoooGBq1ZhbcRdkYq8CfOrDmPPeSqk3O1zh743u6VpVJ8cArJw7Wc=
|
||||
-----END PRIVATE KEY-----";
|
||||
|
||||
const CA_CHAIN: &str = "-----BEGIN PRIVATE KEY-----
|
||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAShZANiAAQTglHNUqyS
|
||||
mMHIqtl3Mh3rl+cJvQtMoKylXcitUdz8nRWJoVl+OlEg4e/WMcY+GDXKyuKYaaYu
|
||||
FjHoooGBq1ZhbcRdkYq8CfOrDmPPeSqk3O1zh743u6VpVJ8cArJw7Wc=
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDaDCCAu6gAwIBAgIPKWkmwiejoVJjkrGUgqUEMAoGCCqGSM49BAMDMFYxCzAJ
|
||||
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
|
||||
MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM3
|
||||
NDFaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk
|
||||
aHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C
|
||||
AQYFK4EEACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZ
|
||||
fjpRIOHv1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+
|
||||
N7ulaVSfHAKycO1no4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
|
||||
WUIgkFQwHQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMB0GA1UdJQQWMBQG
|
||||
CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
|
||||
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
|
||||
BAGDjiEBAm4wCAYGZ4EMAQICMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
|
||||
aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
|
||||
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNT1ZUTFNDQUczQy5wN2MwNwYDVR0f
|
||||
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
|
||||
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
|
||||
AwMDaAAwZQIxAMSZt2xsPeumRdeSMSQ3CbzIZeQ/q7GDCWfeE1Pq+mhgA6gRM44c
|
||||
NVF3xJ+wsxJk+wIwFJbaDMokD8Ei6eNOiA8yUMYvfzQyngmrhZQMdPCK1SewrR79
|
||||
4rSb9vGqxZzelQth
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
|
||||
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
|
||||
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
|
||||
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
|
||||
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
|
||||
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
|
||||
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
|
||||
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
|
||||
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
|
||||
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
|
||||
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
|
||||
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
|
||||
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
|
||||
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
|
||||
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
|
||||
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
|
||||
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
|
||||
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDzzCCAregAwIBAgIRAI1aSBGTnvRH13ERzdmEmukwDQYJKoZIhvcNAQELBQAw
|
||||
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
|
||||
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
|
||||
ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu
|
||||
YzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE
|
||||
ACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZfjpRIOHv
|
||||
1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+N7ulaVSf
|
||||
HAKycO1no4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw
|
||||
HQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMA4GA1UdDwEB/wQEAwIBBjAS
|
||||
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjAxBgNVHSAEKjAoMAgGBmeBDAECAjANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO
|
||||
IQECbjBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B
|
||||
QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB
|
||||
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC
|
||||
AQEAHlbhZpjhkPEAQVhPHRtBDaHqiisVD20pQ1TUeSx3l62Qe7NQ0b7vWQy7DN0i
|
||||
KOu7BGpP5EnIjvXbz9FhpfPJ4FhKEt3tQ51rqXz2oD8vRaryvExWPobDUz3j7HN6
|
||||
OzBsVMIe3PD7AAkpky0x8zMSi/ep5R9OMzybmNptbtppyuB5ytgq+h7lQdRIP+CQ
|
||||
TaJX3qTgfMqzntYre5GljmEB5hU2/rSn8zoSU2APkJidlZFpghtgbD4I2VRHZvCw
|
||||
FFr/SrFM9pwrgbJNZ1MtyTSVgI5NlNb0aMfLR+/r6jJt3ROeY2yh2QQ2dx+Yqgdb
|
||||
uL5ChFWOQU+ErSDnUq7yr+pjBQ==
|
||||
-----END CERTIFICATE-----";
|
||||
|
||||
/// 证书里要覆盖的域名。抓包目标 host 必须在这里面,否则 App 会报证书不受信任。
|
||||
const SAN: &[&str] = &[
|
||||
// 国服
|
||||
"*.dailygn.com",
|
||||
// 日服
|
||||
"production-game-api.sekai.colorfulpalette.org",
|
||||
"*.sekai.colorfulpalette.org",
|
||||
// 国际服
|
||||
"n-production-game-api.sekai-en.com",
|
||||
"*.sekai-en.com",
|
||||
// 台服 / 韩服(cdn 和 https 两个域都会出现)
|
||||
"*.bytedgame.com",
|
||||
"mk-zian-obt-cdn.bytedgame.com",
|
||||
"mk-zian-obt-https.bytedgame.com",
|
||||
"mkkorea-obt-prod01-cdn.bytedgame.com",
|
||||
"mkkorea-obt-prod01-https.bytedgame.com",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
|
||||
let ca_key = KeyPair::from_pem(CA_KEY).expect("parse CA key failed");
|
||||
let first_ca_pem = first_pem_certificate(CA_CHAIN);
|
||||
let issuer = rcgen::Issuer::from_ca_cert_pem(&first_ca_pem, ca_key)
|
||||
.expect("build Issuer from CA cert + key failed");
|
||||
|
||||
let mut leaf_params = CertificateParams::default();
|
||||
let mut dn = DistinguishedName::new();
|
||||
dn.push(DnType::CommonName, SAN[0]);
|
||||
leaf_params.distinguished_name = dn;
|
||||
|
||||
leaf_params.subject_alt_names = SAN
|
||||
.iter()
|
||||
.map(|d| SanType::DnsName((*d).try_into().unwrap()))
|
||||
.collect();
|
||||
|
||||
leaf_params.key_usages = vec![
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::KeyEncipherment,
|
||||
];
|
||||
leaf_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
|
||||
leaf_params.is_ca = IsCa::NoCa;
|
||||
// AKI 必须和签发者证书里的 SKI 一致,rcgen 的 Issuer 会自动从 CA 证书里取
|
||||
leaf_params.use_authority_key_identifier_extension = true;
|
||||
// 有效期落在 CA 的有效期内(2025-04-30 ~ 2040-04-28)
|
||||
leaf_params.not_before = rcgen::date_time_ymd(2025, 5, 1);
|
||||
leaf_params.not_after = rcgen::date_time_ymd(2038, 1, 1);
|
||||
|
||||
let leaf_key = KeyPair::generate().expect("generate leaf key failed");
|
||||
let leaf_cert = leaf_params
|
||||
.signed_by(&leaf_key, &issuer)
|
||||
.expect("sign leaf cert failed");
|
||||
|
||||
// leaf + 完整 CA 链(两个 G3C 交叉签名都带上,让验证器自己选路径)
|
||||
let fullchain_pem = format!("{}{}", leaf_cert.pem(), CA_CHAIN);
|
||||
fs::write(out_dir.join("fullchain.pem"), fullchain_pem).unwrap();
|
||||
|
||||
let leaf_key_pem = leaf_key.serialize_pem();
|
||||
fs::write(out_dir.join("leaf.key"), leaf_key_pem).unwrap();
|
||||
}
|
||||
|
||||
fn first_pem_certificate(pem: &str) -> String {
|
||||
let begin = "-----BEGIN CERTIFICATE-----";
|
||||
let end = "-----END CERTIFICATE-----";
|
||||
if let (Some(s), Some(e)) = (pem.find(begin), pem.find(end)) {
|
||||
pem[s..e + end.len()].to_string()
|
||||
} else {
|
||||
panic!("no certificate found in CA chain PEM");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use rustls_pemfile::{certs, private_key};
|
||||
use std::io::BufReader;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn build_tls_acceptor() -> Arc<rustls::ServerConfig> {
|
||||
let fullchain = include_bytes!(concat!(env!("OUT_DIR"), "/fullchain.pem"));
|
||||
let leaf_key = include_bytes!(concat!(env!("OUT_DIR"), "/leaf.key"));
|
||||
|
||||
let cert_chain = certs(&mut BufReader::new(&fullchain[..]))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
let key = private_key(&mut BufReader::new(&leaf_key[..]))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let mut config = rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(cert_chain, key)
|
||||
.unwrap();
|
||||
|
||||
// 真实服务器(dailygn / 拼锁 API)只支持 HTTP/1.1(ALPN 实测不会选 h2),
|
||||
// 所以这里也只声明 http/1.1,让 App 用 HTTP/1.1 跟我们握手,避免跨协议转换。
|
||||
config.alpn_protocols = vec![b"http/1.1".to_vec()];
|
||||
|
||||
Arc::new(config)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
mod cert;
|
||||
mod mitm;
|
||||
mod tun;
|
||||
|
||||
use crate::mitm::run_capture_async;
|
||||
use jni::objects::{Global, JClass, JObject, JString};
|
||||
use jni::sys::{jint, jstring};
|
||||
use jni::errors::ThrowRuntimeExAndDefault;
|
||||
use jni::{Env, EnvUnowned, JValue, JavaVM, jni_sig, jni_str};
|
||||
use regex::Regex;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
static CANCLE_TOKEN: RwLock<Option<CancellationToken>> = RwLock::new(None);
|
||||
static JVM: OnceLock<JavaVM> = OnceLock::new();
|
||||
static VPN_BRIDGE_CLASS: OnceLock<Global<JClass>> = OnceLock::new();
|
||||
|
||||
fn init_jvm(env: &mut Env) {
|
||||
if JVM.get().is_some() {
|
||||
return;
|
||||
}
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
// 关键:在调用线程(Java 线程)上 find_class,此时 classloader 正确
|
||||
if let Ok(class) = env.find_class(jni_str!("net/bluemangoo/indpjsk/vpn/VpnBridge")) {
|
||||
match env.new_global_ref(class) {
|
||||
Ok(g) => {
|
||||
let _ = VPN_BRIDGE_CLASS.set(g);
|
||||
}
|
||||
Err(e) => eprintln!("new_global_ref failed: {e:?}"),
|
||||
}
|
||||
} else {
|
||||
eprintln!("find_class failed on init thread");
|
||||
}
|
||||
let _ = JVM.set(vm);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_with_env(env: &mut Env, label: &str, msg: &str) {
|
||||
let Some(class_ref) = VPN_BRIDGE_CLASS.get() else {
|
||||
return;
|
||||
};
|
||||
let Ok(jlabel) = env.new_string(label) else {
|
||||
return;
|
||||
};
|
||||
let Ok(jmsg) = env.new_string(msg) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// jni 0.21 中,GlobalRef 可能需要转成 JClass:
|
||||
let jclass: &JClass = unsafe { &*(class_ref.as_obj() as *const JObject as *const JClass) };
|
||||
// 或者更安全:let jclass = unsafe { JClass::from_raw(class_ref.as_obj().as_raw()) };
|
||||
|
||||
let _ = env.call_static_method(
|
||||
jclass,
|
||||
jni_str!("nativeLog"),
|
||||
jni_sig!("(Ljava/lang/String;Ljava/lang/String;)V"),
|
||||
&[JValue::Object(&jlabel), JValue::Object(&jmsg)],
|
||||
);
|
||||
}
|
||||
pub fn log_to_kotlin(label: &str, msg: &str) {
|
||||
let Some(vm) = JVM.get() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = vm.attach_current_thread(|env: &mut Env| -> Result<(), jni::errors::Error> {
|
||||
log_with_env(env, label, msg);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($($arg:tt)*) => {$crate::log_to_kotlin("info", &format!($($arg)*))};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($($arg:tt)*) => {$crate::log_to_kotlin("warn", &format!($($arg)*))};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => {$crate::log_to_kotlin("error", &format!($($arg)*))};
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_net_bluemangoo_indpjsk_vpn_VpnBridge_runCapture(
|
||||
mut env: EnvUnowned,
|
||||
_class: JClass,
|
||||
tun_fd: jint,
|
||||
target_host: JString,
|
||||
target_path: JString,
|
||||
save_path: JString,
|
||||
) -> jstring {
|
||||
let _ = env.with_env(|env: &mut Env| -> Result<(), jni::errors::Error> {
|
||||
init_jvm(env);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
|
||||
let host_arg: String = target_host.to_string();
|
||||
let path: String = target_path.to_string();
|
||||
let out: String = save_path.to_string();
|
||||
|
||||
let hosts: Vec<String> = host_arg
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if hosts.is_empty() {
|
||||
error!("target hosts 为空");
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
let re = match Regex::new(&path) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
error!("Invalid regex pattern: {path} ({e})");
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let cancel_token = CancellationToken::new();
|
||||
*CANCLE_TOKEN.write().unwrap() = Some(cancel_token.clone());
|
||||
|
||||
let cancel_token_clone = cancel_token.clone();
|
||||
let result =
|
||||
rt.block_on(async move { run_capture_async(tun_fd, hosts, re, out, cancel_token_clone).await });
|
||||
cancel_token.cancel();
|
||||
|
||||
// 不要直接 drop runtime:万一有阻塞任务没退干净,drop 会无限等下去,
|
||||
// JNI 调用就永远不返回(表现为按停止没反应)。设上限,超时就放弃。
|
||||
rt.shutdown_timeout(std::time::Duration::from_secs(2));
|
||||
|
||||
// 抓到目标请求 -> 返回原始完整 URL(https://...?query);否则 null
|
||||
let url = result.unwrap_or_else(|e| {
|
||||
error!("capture failed: {e:?}");
|
||||
None
|
||||
});
|
||||
|
||||
let obj: JObject = env
|
||||
.with_env(|env: &mut Env| -> Result<JObject, jni::errors::Error> {
|
||||
match &url {
|
||||
Some(url) => {
|
||||
let s = env.new_string(url)?;
|
||||
Ok(unsafe { JObject::from_raw(env, s.as_raw()) })
|
||||
}
|
||||
None => Ok(JObject::null()),
|
||||
}
|
||||
})
|
||||
.resolve::<ThrowRuntimeExAndDefault>();
|
||||
|
||||
obj.into_raw()
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_net_bluemangoo_indpjsk_vpn_VpnBridge_requestStop(
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
) {
|
||||
info!("Stopping capture...");
|
||||
if let Some(token) = CANCLE_TOKEN.write().unwrap().take() {
|
||||
token.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
use crate::cert::build_tls_acceptor;
|
||||
use crate::tun::{BorrowedTunIo, RewindStream};
|
||||
use crate::{error, info, warn};
|
||||
use bytes::Bytes;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Incoming;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use ipstack::IpStackStream;
|
||||
use regex::Regex;
|
||||
use rustls::RootCertStore;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio_rustls::StartHandshake;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tun_rs::BorrowedAsyncDevice;
|
||||
|
||||
static ROOT_STORE: OnceLock<Arc<RootCertStore>> = OnceLock::new();
|
||||
|
||||
/// 嗅探 ClientHello 的最长等待时间。超时后按「非目标」原样透传,
|
||||
/// 这样「服务器先说话」的协议(客户端连上后不发数据)也不会被卡住。
|
||||
const SNIFF_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
const DOH_BLACKLIST: &[&str] = &[
|
||||
"dns.google",
|
||||
"dns.google.com",
|
||||
"cloudflare-dns.com",
|
||||
"mozilla.cloudflare-dns.com",
|
||||
"dns.quad9.net",
|
||||
"dns.adguard.com",
|
||||
"doh.opendns.com",
|
||||
"dns.alidns.com", // 阿里 DoH
|
||||
"doh.pub", // 腾讯 DoH
|
||||
"dns.alidns.com",
|
||||
];
|
||||
|
||||
fn root_store() -> Arc<RootCertStore> {
|
||||
ROOT_STORE
|
||||
.get_or_init(|| {
|
||||
let mut root_store = RootCertStore::empty();
|
||||
let native_certs = rustls_native_certs::load_native_certs();
|
||||
for err in native_certs.errors {
|
||||
warn!("加载证书警告: {err}");
|
||||
}
|
||||
for cert in native_certs.certs {
|
||||
let _ = root_store.add(cert);
|
||||
}
|
||||
|
||||
load_system_ca_dirs(&mut root_store);
|
||||
|
||||
// 兜底:一个根证书都没有时用内置 Mozilla 列表,否则上游必然握手失败
|
||||
if root_store.is_empty() {
|
||||
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
}
|
||||
Arc::new(root_store)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// 读取系统 CA 目录(Android 的路径,其他平台不存在就走空)
|
||||
fn load_system_ca_dirs(store: &mut RootCertStore) {
|
||||
use std::io::BufReader;
|
||||
|
||||
const DIRS: &[&str] = &[
|
||||
"/apex/com.android.conscrypt/cacerts", // Android 14+
|
||||
"/system/etc/security/cacerts", // 旧版 Android
|
||||
"/data/misc/user/0/cacerts-added", // 用户自己装的 CA
|
||||
];
|
||||
|
||||
for dir in DIRS {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
continue;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let Ok(file) = std::fs::File::open(entry.path()) else {
|
||||
continue;
|
||||
};
|
||||
for cert in rustls_pemfile::certs(&mut BufReader::new(file)).flatten() {
|
||||
let _ = store.add(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 嗅探 TLS ClientHello(不破坏字节流)。
|
||||
///
|
||||
/// 返回 `None` 表示对端不是 TLS(明文 HTTP、游戏自定义协议、超时……),
|
||||
/// 调用方应当调用 `io.rewind()` 之后原样透传,而**不是**断开连接。
|
||||
async fn sniff_client_hello<S>(
|
||||
io: &mut RewindStream<S>,
|
||||
) -> io::Result<Option<(Option<String>, rustls::server::Accepted)>>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let mut acceptor = rustls::server::Acceptor::default();
|
||||
let mut buf = vec![0u8; 16 * 1024];
|
||||
|
||||
loop {
|
||||
let n = match tokio::time::timeout(SNIFF_TIMEOUT, io.read(&mut buf)).await {
|
||||
Ok(Ok(n)) => n,
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(_) => return Ok(None), // 对端迟迟不发数据,当作非 TLS
|
||||
};
|
||||
if n == 0 {
|
||||
return Ok(None); // 还没收到 ClientHello 就 EOF
|
||||
}
|
||||
|
||||
// 只喂给 acceptor 一份拷贝,原始字节仍留在 RewindStream 的 history 里
|
||||
if acceptor.read_tls(&mut &buf[..n]).is_err() {
|
||||
return Ok(None);
|
||||
}
|
||||
match acceptor.accept() {
|
||||
Ok(Some(accepted)) => {
|
||||
let sni = accepted.client_hello().server_name().map(|s| s.to_string());
|
||||
return Ok(Some((sni, accepted)));
|
||||
}
|
||||
Ok(None) => continue, // 还没凑齐
|
||||
Err(_) => return Ok(None), // 不是合法 TLS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_tls_connection<S>(
|
||||
tcp: S,
|
||||
dst: SocketAddr,
|
||||
target_hosts: Vec<String>,
|
||||
target_path: Regex,
|
||||
save_path: String,
|
||||
server_config: Arc<rustls::ServerConfig>,
|
||||
cancel: CancellationToken,
|
||||
result_tx: Arc<Mutex<Option<tokio::sync::oneshot::Sender<String>>>>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let cancel_listen = cancel.clone();
|
||||
|
||||
tokio::select! {
|
||||
_ = cancel_listen.cancelled() => Ok(()),
|
||||
res = async {
|
||||
// 1. 先嗅探 ClientHello 的 SNI(可重放,不丢字节)
|
||||
let mut io = RewindStream::new(tcp);
|
||||
let sniffed = match sniff_client_hello(&mut io).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
warn!("client hello sniff error on {dst:?}: {e:?}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// 不是 TLS:明文 HTTP / 自定义协议 / 服务器先说话,一律原样透传
|
||||
let Some((sni, accepted)) = sniffed else {
|
||||
io.rewind();
|
||||
io.stop_recording();
|
||||
return tcp_passthrough(io, dst).await.map(|_| ());
|
||||
};
|
||||
|
||||
if let Some(ref sni_str) = sni
|
||||
&& DOH_BLACKLIST.iter().any(|d| sni_str.eq_ignore_ascii_case(d)) {
|
||||
return Ok(());
|
||||
}
|
||||
let host_matched = sni
|
||||
.as_deref()
|
||||
.map(|s| target_hosts.iter().any(|h| s.eq_ignore_ascii_case(h)))
|
||||
.unwrap_or(false);
|
||||
|
||||
// 不是目标 host,原样透传(必须把已嗅探的 ClientHello 重放给上游)
|
||||
if !host_matched {
|
||||
io.rewind();
|
||||
io.stop_recording();
|
||||
let r=tcp_passthrough(io, dst).await.map(|_| ());
|
||||
if r.is_err() {
|
||||
warn!("tcp_passthrough error on {sni:?} {dst:?}");
|
||||
}
|
||||
return r;
|
||||
}
|
||||
info!(
|
||||
"Encountered target host."
|
||||
);
|
||||
|
||||
// 2. 与客户端完成 TLS(accepted 里已经缓存了 ClientHello,不能再重放)
|
||||
io.stop_recording();
|
||||
let client_tls = StartHandshake::from_parts(accepted, io)
|
||||
.into_stream(server_config)
|
||||
.await?;
|
||||
|
||||
// 3. 用 hyper 的 HTTP/1.1 服务端服务这条连接,
|
||||
// 每个请求单独起一条上游 TLS 连接转发(HKDF 每请求)
|
||||
let svc_host = sni.clone().unwrap_or_default();
|
||||
let svc = hyper::service::service_fn(move |req: hyper::Request<Incoming>| {
|
||||
let host = svc_host.clone();
|
||||
let path = target_path.clone();
|
||||
let save = save_path.clone();
|
||||
let result_tx = result_tx.clone();
|
||||
let cancel = cancel.clone();
|
||||
async move {
|
||||
Ok::<_, std::convert::Infallible>(
|
||||
forward_request(req, dst, host, path, save, result_tx, cancel).await,
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
hyper::server::conn::http1::Builder::new()
|
||||
.serve_connection(TokioIo::new(client_tls), svc)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
} => res
|
||||
}
|
||||
}
|
||||
|
||||
/// 转发单个请求到真实服务器,命中 target_path 时把响应体落盘。
|
||||
///
|
||||
/// 这里用「一个请求一条上游连接」的方式:目标是抓一个 API 响应,
|
||||
/// 简单、不会踩 keep-alive / 多路复用状态机的坑。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn forward_request(
|
||||
req: hyper::Request<Incoming>,
|
||||
dst: SocketAddr,
|
||||
url_host: String,
|
||||
target_path: Regex,
|
||||
save_path: String,
|
||||
result_tx: Arc<Mutex<Option<tokio::sync::oneshot::Sender<String>>>>,
|
||||
cancel: CancellationToken,
|
||||
) -> hyper::Response<Full<Bytes>> {
|
||||
match forward_request_inner(req, dst, &url_host, &target_path).await {
|
||||
Ok((response, matched_body)) => {
|
||||
if let Some((url, body)) = matched_body {
|
||||
info!(
|
||||
"Target matched! Downloading {} bytes",
|
||||
body.len()
|
||||
);
|
||||
let path = std::path::Path::new(&save_path);
|
||||
if !path.is_absolute() {
|
||||
error!(
|
||||
"save_path 不是绝对路径({save_path})"
|
||||
);
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = tokio::fs::create_dir_all(parent).await;
|
||||
}
|
||||
match tokio::fs::write(path, &body).await {
|
||||
Ok(()) => {
|
||||
if let Some(tx) = result_tx.lock().await.take() {
|
||||
let _ = tx.send(url);
|
||||
}
|
||||
}
|
||||
Err(e) => error!("write {save_path} failed: {e:?}"),
|
||||
}
|
||||
cancel.cancel();
|
||||
}
|
||||
response
|
||||
}
|
||||
Err(e) => {
|
||||
error!("forward to {dst} failed: {e:?}");
|
||||
hyper::Response::builder()
|
||||
.status(hyper::StatusCode::BAD_GATEWAY)
|
||||
.header("content-type", "text/plain; charset=utf-8")
|
||||
.body(Full::new(Bytes::from(format!("mitm forward error: {e}"))))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn forward_request_inner(
|
||||
req: hyper::Request<Incoming>,
|
||||
dst: SocketAddr,
|
||||
url_host: &str,
|
||||
target_path: &Regex,
|
||||
) -> anyhow::Result<(hyper::Response<Full<Bytes>>, Option<(String, Bytes)>)> {
|
||||
let (mut parts, body) = req.into_parts();
|
||||
|
||||
// 和「模块」的 URL Rewrite 行为一致:把 MySekai 的增量刷新标志强制改成 True,
|
||||
// 让服务端下发全量数据(游戏自己有时会发 False)。改完再匹配路径 / 转发 / 记 URL。
|
||||
if let Some(rewritten) = force_full_mysekai(&parts.uri) {
|
||||
warn!("rewrite mysekai flag: {} -> {}", parts.uri, rewritten);
|
||||
parts.uri = rewritten;
|
||||
}
|
||||
|
||||
let parts_uri = parts.uri.clone();
|
||||
// 目标正则可能带 query(例如 (?\?isLogin=true)?$),所以要拿 path + query 去匹配
|
||||
let path_and_query = parts_uri
|
||||
.path_and_query()
|
||||
.map(|p| p.as_str().to_string())
|
||||
.unwrap_or_else(|| "/".to_string());
|
||||
let body_bytes = body.collect().await?.to_bytes();
|
||||
|
||||
// 去掉逐跳头,避免把下游的 chunked/keep-alive 语义原样塞给上游
|
||||
for h in [
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-connection",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
"te",
|
||||
"trailer",
|
||||
"content-length",
|
||||
] {
|
||||
parts.headers.remove(h);
|
||||
}
|
||||
|
||||
let mut client_config = rustls::ClientConfig::builder()
|
||||
.with_root_certificates(root_store())
|
||||
.with_no_client_auth();
|
||||
client_config.alpn_protocols = vec![b"http/1.1".to_vec()];
|
||||
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config));
|
||||
let server_name = rustls::pki_types::ServerName::try_from(url_host.to_string())?;
|
||||
let tcp_to_server = tokio::net::TcpStream::connect(dst).await?;
|
||||
let server_tls = connector.connect(server_name, tcp_to_server).await?;
|
||||
|
||||
let (mut sender, conn) =
|
||||
hyper::client::conn::http1::handshake::<_, Full<Bytes>>(TokioIo::new(server_tls)).await?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = conn.await {
|
||||
warn!("upstream http1 connection error: {e:?}");
|
||||
}
|
||||
});
|
||||
|
||||
let upstream_req = hyper::Request::from_parts(parts, Full::new(body_bytes));
|
||||
let resp = sender.send_request(upstream_req).await?;
|
||||
|
||||
let (mut res_parts, res_body) = resp.into_parts();
|
||||
let res_bytes = res_body.collect().await?.to_bytes();
|
||||
for h in [
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-connection",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
"te",
|
||||
"trailer",
|
||||
] {
|
||||
res_parts.headers.remove(h);
|
||||
}
|
||||
|
||||
let is_target = target_path.is_match(&path_and_query);
|
||||
|
||||
// 原始完整 URL(https://host + path + ?query)
|
||||
let url = build_url(url_host, &parts_uri);
|
||||
|
||||
let save = if is_target {
|
||||
Some((url, res_bytes.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((
|
||||
hyper::Response::from_parts(res_parts, Full::new(res_bytes)),
|
||||
save,
|
||||
))
|
||||
}
|
||||
|
||||
/// 拼出 "https://host/path?query" 形式的完整 URL
|
||||
fn build_url(host: &str, uri: &http::Uri) -> String {
|
||||
if uri.scheme().is_some() && uri.authority().is_some() {
|
||||
return uri.to_string();
|
||||
}
|
||||
let pq = uri.path_and_query().map(|p| p.as_str()).unwrap_or("/");
|
||||
format!("https://{host}{pq}")
|
||||
}
|
||||
|
||||
/// `?isForceAllReloadOnlyMysekai=False` -> `=True`
|
||||
///
|
||||
/// 游戏在对 MySekai 做增量刷新时会带 False;模块里把它改写成 True 来触发全量下发。
|
||||
/// 值已经是 True(或没这个参数)时返回 None,不做改动。
|
||||
fn force_full_mysekai(uri: &http::Uri) -> Option<http::Uri> {
|
||||
const KEY: &str = "isForceAllReloadOnlyMysekai=";
|
||||
let pq = uri.path_and_query()?.as_str();
|
||||
let start = pq.find(KEY)? + KEY.len();
|
||||
let (head, rest) = pq.split_at(start);
|
||||
let end = rest.find('&').unwrap_or(rest.len());
|
||||
let (value, tail) = rest.split_at(end);
|
||||
if !value.eq_ignore_ascii_case("false") {
|
||||
return None;
|
||||
}
|
||||
http::Uri::try_from(format!("{head}True{tail}").as_str()).ok()
|
||||
}
|
||||
|
||||
pub async fn run_capture_async(
|
||||
tun_fd: i32,
|
||||
target_hosts: Vec<String>,
|
||||
target_path: Regex,
|
||||
save_path: String,
|
||||
cancel_token: CancellationToken,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
let device = unsafe { BorrowedAsyncDevice::borrow_raw(tun_fd as RawFd)? };
|
||||
|
||||
let mut ipstack_config = ipstack::IpStackConfig::default();
|
||||
ipstack_config.mtu(1500)?;
|
||||
let device_ref: &BorrowedAsyncDevice<'static> = unsafe { std::mem::transmute(&device) };
|
||||
let tun_io = BorrowedTunIo::new(device_ref);
|
||||
let mut ip_stack = ipstack::IpStack::new(ipstack_config, tun_io);
|
||||
let mut bridge_tasks = JoinSet::new();
|
||||
|
||||
let tls_acceptor = build_tls_acceptor();
|
||||
|
||||
let (result_tx, mut result_rx) = tokio::sync::oneshot::channel::<String>();
|
||||
let result_tx = Arc::new(Mutex::new(Some(result_tx)));
|
||||
|
||||
info!("VPN capture started.");
|
||||
|
||||
let result: anyhow::Result<Option<String>> = loop {
|
||||
let token = cancel_token.clone();
|
||||
tokio::select! {
|
||||
_ = cancel_token.cancelled() => {
|
||||
// 取消时如果刚好已经抓到了 URL,也带回去
|
||||
let final_result = result_rx.try_recv().ok();
|
||||
break Ok(final_result);
|
||||
}
|
||||
Ok(url) = &mut result_rx => {
|
||||
cancel_token.cancel(); // 确保所有任务退出
|
||||
break Ok(Some(url));
|
||||
}
|
||||
// 定期清理已经结束的搬运任务,防止内存泄漏
|
||||
_ = bridge_tasks.join_next(), if !bridge_tasks.is_empty() => {}
|
||||
|
||||
res = ip_stack.accept() => {
|
||||
let stream = match res {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("ip_stack accept error (device might be dropped): {e:?}");
|
||||
break Err(anyhow::Error::from(e));
|
||||
}
|
||||
};
|
||||
|
||||
match stream {
|
||||
IpStackStream::Udp(udp) => {
|
||||
let dst = udp.peer_addr();
|
||||
if dst.port() == 443 {
|
||||
// 丢弃 UDP 443,使客户端的 QUIC 快速回退到 TCP
|
||||
drop(udp);
|
||||
} else{
|
||||
bridge_tasks.spawn(async move {
|
||||
// info!("UDP passthrough: {:?}", dst);
|
||||
if let Err(e) = udp_passthrough(udp, dst, token).await {
|
||||
error!("udp passthrough error: {e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
IpStackStream::Tcp(mut tcp) => {
|
||||
let dst = tcp.peer_addr();
|
||||
if dst.port() == 443 {
|
||||
// info!("Intercepting TCP 443: {:?}", dst);
|
||||
//
|
||||
// 用 duplex 替代手写的 mpsc 通道:duplex 自带正确的
|
||||
// 反压与 waker 语义(旧实现 try_send 满了直接返回 Pending
|
||||
// 却不注册 waker,连接会永久卡死,最后被协议栈 RST)。
|
||||
let (app_io, mut tun_io) = tokio::io::duplex(256 * 1024);
|
||||
|
||||
// 在受借用保护的 join_set 启动读写泵
|
||||
bridge_tasks.spawn(async move {
|
||||
if let Err(e) =
|
||||
tokio::io::copy_bidirectional(&mut tun_io, &mut tcp).await
|
||||
{
|
||||
// 连接被任意一端关闭时属正常现象
|
||||
let _ = e;
|
||||
}
|
||||
});
|
||||
|
||||
// 独立 SPAWN 运行 TLS 业务逻辑(无借用约束)
|
||||
let hosts = target_hosts.clone();
|
||||
let path = target_path.clone();
|
||||
let save = save_path.clone();
|
||||
let server_config = tls_acceptor.clone();
|
||||
let token = cancel_token.clone();
|
||||
let result_tx = result_tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match handle_tls_connection(
|
||||
app_io,
|
||||
dst,
|
||||
hosts,
|
||||
path,
|
||||
save,
|
||||
server_config,
|
||||
token,
|
||||
result_tx
|
||||
).await {
|
||||
Ok(()) => { /* 命中时已通过 result_tx 把 URL 送回主循环 */ }
|
||||
Err(e) => error!("handle tls conn error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}else{
|
||||
bridge_tasks.spawn(async move {
|
||||
let _ = tcp_passthrough(tcp, dst).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 关键:必须在 runtime 还活着的时候,把所有 bridge task(它们持有 ipstack 的
|
||||
// IpStackTcpStream)彻底 drop 掉。
|
||||
// 原因:ipstack 的 Drop 实现会 `block_in_place(...block_on(task_handle))` 等它自己的
|
||||
// TCP 任务退出。如果拖到 runtime 析构阶段才 drop,worker 已经不跑了,就会永远等下去,
|
||||
// 表现出来就是「按了停止没反应」。这里主动 abort + join,让 drop 发生在正常调度中。
|
||||
bridge_tasks.abort_all();
|
||||
while bridge_tasks.join_next().await.is_some() {}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn tcp_passthrough<IO>(mut client: IO, dst: SocketAddr) -> anyhow::Result<()>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
// info!("TCP passthrough to {dst}");
|
||||
let mut server = tokio::net::TcpStream::connect(dst).await?;
|
||||
tokio::io::copy_bidirectional(&mut client, &mut server).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn udp_passthrough<IO>(
|
||||
client: IO,
|
||||
dst: SocketAddr,
|
||||
cancel: CancellationToken, // 把 cancel 传进来
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
// 1. 绑定本地 socket
|
||||
let bind_addr: SocketAddr = if dst.is_ipv4() {
|
||||
"0.0.0.0:0".parse()?
|
||||
} else {
|
||||
"[::]:0".parse()?
|
||||
};
|
||||
|
||||
let socket = UdpSocket::bind(bind_addr).await?;
|
||||
|
||||
// 2. Android 关键:一定要 protect,否则流量会重新进 TUN
|
||||
// 如果你有 VpnService 的 protect 方法,在这里调用
|
||||
// protect(socket.as_raw_fd());
|
||||
|
||||
socket.connect(dst).await?;
|
||||
let socket = Arc::new(socket);
|
||||
|
||||
let (mut cr, mut cw) = tokio::io::split(client);
|
||||
|
||||
let mut client_buf = vec![0u8; 65535];
|
||||
let mut server_buf = vec![0u8; 65535];
|
||||
|
||||
// 空闲超时(DNS 建议 8~15 秒,其他 UDP 可以更短)
|
||||
let idle_timeout = Duration::from_secs(12);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 外部取消
|
||||
_ = cancel.cancelled() => break,
|
||||
|
||||
// client → server
|
||||
res = tokio::time::timeout(idle_timeout, cr.read(&mut client_buf)) => {
|
||||
match res {
|
||||
Ok(Ok(0)) | Err(_) => break, // EOF 或超时
|
||||
Ok(Ok(n)) => {
|
||||
if socket.send(&client_buf[..n]).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Err(_)) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// server → client
|
||||
res = tokio::time::timeout(idle_timeout, socket.recv(&mut server_buf)) => {
|
||||
match res {
|
||||
Ok(Ok(n)) if n > 0 => {
|
||||
if cw.write_all(&server_buf[..n]).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => break, // 超时 / 错误 / 空包
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
use std::io;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
/// 包装你的 Borrowed 设备(无论类型是 BorrowedAsyncDevice 还是对其借用)
|
||||
pub struct BorrowedTunIo<'a, D> {
|
||||
device: &'a D,
|
||||
packet_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a, D> BorrowedTunIo<'a, D> {
|
||||
pub fn new(device: &'a D) -> Self {
|
||||
Self {
|
||||
device,
|
||||
packet_buf: vec![0u8; 65536],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, D> AsyncRead for BorrowedTunIo<'a, D>
|
||||
where
|
||||
D: DevicePoller,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let this = &mut *self;
|
||||
if buf.remaining() == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// 调用公开方法 poll_recv
|
||||
match this.device.poll_recv(cx, &mut this.packet_buf) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
let to_copy = std::cmp::min(buf.remaining(), n);
|
||||
buf.put_slice(&this.packet_buf[..to_copy]);
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, D> AsyncWrite for BorrowedTunIo<'a, D>
|
||||
where
|
||||
D: DevicePoller,
|
||||
{
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
// 调用公开方法 poll_send
|
||||
self.device.poll_send(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// 抽象 trait,用于对接你的设备对象
|
||||
pub trait DevicePoller {
|
||||
fn poll_recv(&self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<io::Result<usize>>;
|
||||
fn poll_send(&self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>>;
|
||||
}
|
||||
|
||||
// 为 tun_rs 提供的结构实现(源码里的 AsyncDevice 实现了对应方法)
|
||||
impl DevicePoller for tun_rs::AsyncDevice {
|
||||
fn poll_recv(&self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<io::Result<usize>> {
|
||||
self.poll_recv(cx, buf)
|
||||
}
|
||||
fn poll_send(&self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
|
||||
self.poll_send(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> DevicePoller for tun_rs::BorrowedAsyncDevice<'b> {
|
||||
fn poll_recv(&self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<io::Result<usize>> {
|
||||
self.deref().poll_recv(cx, buf)
|
||||
}
|
||||
fn poll_send(&self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
|
||||
self.deref().poll_send(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// 可回退(可重放)的流包装器。
|
||||
///
|
||||
/// 用于在不破坏字节流的前提下嗅探 TLS ClientHello:
|
||||
/// * 读过的字节会被记录在 `history` 里;
|
||||
/// * `rewind()` 之后,下次读取会从 `history` 开头重新吐出来;
|
||||
/// * `history` 读完之后继续从底层流读取并追加。
|
||||
///
|
||||
/// 这样即使最终决定「不是目标,原样透传」,上游依然能收到完整的 ClientHello。
|
||||
pub struct RewindStream<S> {
|
||||
inner: S,
|
||||
history: Vec<u8>,
|
||||
/// 已经交付给调用方的 `history` 字节数
|
||||
pos: usize,
|
||||
/// history 最多记录多少字节
|
||||
max_record: usize,
|
||||
recording: bool,
|
||||
}
|
||||
|
||||
impl<S> RewindStream<S> {
|
||||
pub fn new(inner: S) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
history: Vec::new(),
|
||||
pos: 0,
|
||||
max_record: 64 * 1024,
|
||||
recording: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 回到流的起点,让后续读取重放已经嗅探过的字节
|
||||
pub fn rewind(&mut self) {
|
||||
self.pos = 0;
|
||||
}
|
||||
|
||||
/// 决策完成后调用:不再缓存新字节(老字节仍会正常重放)。
|
||||
/// 必须在确定「不会再 rewind」之后调用,否则会丢数据。
|
||||
pub fn stop_recording(&mut self) {
|
||||
self.recording = false;
|
||||
self.history.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsyncRead for RewindStream<S>
|
||||
where
|
||||
S: AsyncRead + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let this = &mut *self;
|
||||
|
||||
if this.pos < this.history.len() {
|
||||
let to_read = std::cmp::min(buf.remaining(), this.history.len() - this.pos);
|
||||
buf.put_slice(&this.history[this.pos..this.pos + to_read]);
|
||||
this.pos += to_read;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if !this.recording {
|
||||
return Pin::new(&mut this.inner).poll_read(cx, buf);
|
||||
}
|
||||
|
||||
// 记录容量用尽:绝不能再从底层读,否则 rewind 会丢字节。
|
||||
// 直接报错让上层放弃解析(此时的流量本来就不像 TLS)。
|
||||
let room = this.max_record - this.history.len();
|
||||
if buf.remaining() > room {
|
||||
return Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"sniff buffer overflow",
|
||||
)));
|
||||
}
|
||||
|
||||
let start = buf.filled().len();
|
||||
match Pin::new(&mut this.inner).poll_read(cx, buf) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
let n = buf.filled().len() - start;
|
||||
if n > 0 {
|
||||
this.history
|
||||
.extend_from_slice(&buf.filled()[start..start + n]);
|
||||
this.pos = this.history.len();
|
||||
}
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsyncWrite for RewindStream<S>
|
||||
where
|
||||
S: AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.inner).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user