mirror of
https://github.com/Runnin4ik/dpi-detector.git
synced 2026-10-19 09:23:17 +00:00
refactor: drop the dead TLS probe, turn Nagle off on every dial
`probe::tls` (196 lines) had no caller — test 2 grew its own L7 path — and the blockpage signatures it checked are already kept per region in `RegionProfile::blockpage_signatures`, so the module only duplicated knowledge and kept nine names alive in the public surface. Deleted with its two re-exports. The remaining raw dials (DoH, DoT, SOCKS5, the netinfo GET and the three Telegram paths) now go through one `net::tcp::set_no_delay`, so the rule that `net/tcp.rs` states — Nagle off is a requirement of the exchange, not a tuning knob — holds for every dial instead of four of ten.
This commit is contained in:
parent
21bb1e80ed
commit
1aebf22ed2
9 changed files with 49 additions and 214 deletions
|
|
@ -16,6 +16,7 @@ use super::resolve::resolve_host;
|
|||
use super::types::{DnsError, DnsRecord};
|
||||
use super::wire::{build_dns_query, parse_dns_response, QTYPE_A};
|
||||
use crate::config::AppConfig;
|
||||
use crate::net::tcp::set_no_delay;
|
||||
use crate::net::tls::create_verifying_doh_tls_config;
|
||||
|
||||
pub enum DohSender {
|
||||
|
|
@ -77,7 +78,7 @@ pub async fn doh_connect(endpoint_url: &str, timeout_dur: Duration) -> Result<(D
|
|||
stage: "tcp_connect",
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
let _ = tcp.set_nodelay(true);
|
||||
set_no_delay(&tcp);
|
||||
|
||||
let tls_config = create_verifying_doh_tls_config();
|
||||
let connector = TlsConnector::from(tls_config);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use tokio_rustls::TlsConnector;
|
|||
use super::resolve::resolve_host;
|
||||
use super::types::DnsError;
|
||||
use super::wire::{build_dns_query, parse_dns_response, QTYPE_A};
|
||||
use crate::net::tcp::set_no_delay;
|
||||
use crate::net::tls::create_verifying_tls_config;
|
||||
|
||||
/// Splits a DoT endpoint `'host[:port]'` (default 853). Supports IPv6 literals.
|
||||
|
|
@ -98,7 +99,7 @@ impl DotSession {
|
|||
stage: "tcp_connect",
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
let _ = tcp.set_nodelay(true);
|
||||
set_no_delay(&tcp);
|
||||
let connector = TlsConnector::from(create_verifying_tls_config());
|
||||
let server_name = ServerName::try_from(host.to_string())
|
||||
.or_else(|_| {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use tokio::net::TcpStream;
|
|||
use url::Url;
|
||||
|
||||
use super::types::DnsError;
|
||||
use crate::net::tcp::set_no_delay;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SocksProxyConfig {
|
||||
|
|
@ -110,6 +111,7 @@ pub async fn associate_socks5_udp(
|
|||
let mut tcp = TcpStream::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| DnsError::Socks5(format!("TCP connect to {} failed: {}", addr, e)))?;
|
||||
set_no_delay(&tcp);
|
||||
|
||||
// 1. Handshake
|
||||
if proxy.username.is_some() && proxy.password.is_some() {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use tokio_rustls::TlsConnector;
|
|||
use url::Url;
|
||||
|
||||
use crate::dns::query_doh_txt;
|
||||
use crate::net::tcp::set_no_delay;
|
||||
use crate::net::tls::create_verifying_tls_config;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
@ -149,6 +150,7 @@ async fn http_get_once(url: &Url, timeout_dur: Duration) -> Result<(u16, Option<
|
|||
let tcp = TcpStream::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| format!("Connect to {} failed: {}", addr, e))?;
|
||||
set_no_delay(&tcp);
|
||||
|
||||
let req = Request::builder()
|
||||
.method(Method::GET)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ pub async fn dial_tcp(addr: &SocketAddr, timeout_dur: Duration) -> Result<TcpStr
|
|||
Ok(Err(e)) => return Err(DialError::Io(e)),
|
||||
Err(_) => return Err(DialError::Timeout),
|
||||
};
|
||||
let _ = stream.set_nodelay(true);
|
||||
set_no_delay(&stream);
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Turns Nagle off on a dialed stream. See [`dial_tcp`] for why every probe wants
|
||||
/// this — it is a requirement of the exchange, not a tuning knob.
|
||||
pub(crate) fn set_no_delay(stream: &TcpStream) {
|
||||
let _ = stream.set_nodelay(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
pub mod connector;
|
||||
pub mod tcp16;
|
||||
pub mod tls;
|
||||
pub mod domains;
|
||||
pub mod burst;
|
||||
pub mod telegram;
|
||||
|
|
@ -9,6 +8,5 @@ pub use connector::{DpiTlsConnector, RustlsConnector};
|
|||
pub mod http;
|
||||
pub use burst::{burst_targets, BurstAttempt, BurstProfileReport, BurstReport, BurstSettings, BurstTarget};
|
||||
pub use tcp16::{check_tcp_16_20, probe_tcp16};
|
||||
pub use tls::{is_suspicious_redirect, probe_tls_domain, probe_tls_domain_default};
|
||||
pub use telegram::{probe_telegram_all_dcs, probe_telegram_dc, run_download, run_telegram_full, run_telegram_test, run_upload, TelegramDcResult, TelegramFullReport, TelegramReport, TransferStats};
|
||||
pub use whitelist::{run_whitelist_sni, AsRow, AsVerdict, WhitelistReport};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use tokio_rustls::TlsConnector;
|
|||
use crate::classify::DET_SYN_TIMEOUT_SHORT;
|
||||
use crate::config::AppConfig;
|
||||
use crate::dns::resolve_host;
|
||||
use crate::net::tcp::set_no_delay;
|
||||
use crate::net::tls::create_insecure_dpi_tls_config;
|
||||
use crate::PhaseProgress;
|
||||
|
||||
|
|
@ -69,7 +70,8 @@ pub async fn probe_telegram_dc(dc: &TelegramDc, timeout_dur: Duration) -> Telegr
|
|||
match addr_str.parse::<SocketAddr>() {
|
||||
Ok(sock_addr) => {
|
||||
match timeout(timeout_dur, TcpStream::connect(sock_addr)).await {
|
||||
Ok(Ok(_stream)) => {
|
||||
Ok(Ok(stream)) => {
|
||||
set_no_delay(&stream);
|
||||
let latency = start.elapsed().as_millis() as u64;
|
||||
TelegramDcResult {
|
||||
name: dc.name.to_string(),
|
||||
|
|
@ -189,6 +191,7 @@ fn split_url(url: &str) -> Option<(String, String)> {
|
|||
async fn tls_get(host: &str, path: &str, user_agent: &str) -> Option<(impl Body<Data = Bytes, Error = hyper::Error> + Unpin, impl FnOnce() + Send)> {
|
||||
let addr = resolve_host(host, 443, Duration::from_secs(10)).await.ok()?.into_iter().next()?;
|
||||
let tcp = TcpStream::connect(addr).await.ok()?;
|
||||
set_no_delay(&tcp);
|
||||
let connector = TlsConnector::from(create_insecure_dpi_tls_config());
|
||||
let server_name = ServerName::try_from(host.to_string()).ok()?;
|
||||
let tls = connector.connect(server_name, tcp).await.ok()?;
|
||||
|
|
@ -371,7 +374,10 @@ pub async fn run_upload(cfg: &AppConfig) -> TransferStats {
|
|||
cfg.telegram_upload_port,
|
||||
);
|
||||
let tcp = match timeout(Duration::from_secs_f64(8.0), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Ok(s)) => {
|
||||
set_no_delay(&s);
|
||||
s
|
||||
}
|
||||
_ => {
|
||||
return TransferStats { status: "blocked".into(), duration: t0.elapsed().as_secs_f64(), ..Default::default() };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,196 +0,0 @@
|
|||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::time::{Duration, Instant};
|
||||
use http_body_util::BodyExt;
|
||||
use hyper::Method;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use rustls::pki_types::ServerName;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::connector::{DpiTlsConnector, RustlsConnector};
|
||||
use super::http::{negotiated_h2, HttpRequest, HttpSender};
|
||||
use crate::classify::{
|
||||
classify_connect_error, classify_tls_error, ConnectionStage, DpiProbeStream, DpiProbeTracker,
|
||||
DpiStatus, ProbeMetrics,
|
||||
};
|
||||
|
||||
const DEFAULT_USER_AGENT: &str =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36";
|
||||
|
||||
/// Checks whether an HTTP redirect location points to a known DPI/ISP blockpage.
|
||||
pub fn is_suspicious_redirect(location: &str) -> bool {
|
||||
let loc = location.to_ascii_lowercase();
|
||||
const BLOCKPAGE_SIGNATURES: &[&str] = &[
|
||||
"warning.rt.ru",
|
||||
"blocked.rt.ru",
|
||||
"block.mts.ru",
|
||||
"eais.rkn.gov.ru",
|
||||
"blackhole",
|
||||
"zapret",
|
||||
"blocked",
|
||||
"blockpage",
|
||||
"megafon.ru/blocked",
|
||||
"beeline.ru/blocked",
|
||||
"domru.ru/blocked",
|
||||
];
|
||||
|
||||
BLOCKPAGE_SIGNATURES.iter().any(|sig| loc.contains(sig))
|
||||
}
|
||||
|
||||
/// Probes a domain over TLS / HTTPS and classifies the DPI state.
|
||||
pub async fn probe_tls_domain<C: DpiTlsConnector>(
|
||||
domain: &str,
|
||||
target_ip: IpAddr,
|
||||
port: u16,
|
||||
timeout_dur: Duration,
|
||||
connector: &C,
|
||||
) -> ProbeMetrics {
|
||||
let start = Instant::now();
|
||||
let tracker = DpiProbeTracker::new();
|
||||
let addr = SocketAddr::new(target_ip, port);
|
||||
let conn = connector;
|
||||
|
||||
let probe_future = async {
|
||||
tracker.set_stage(ConnectionStage::TcpConnecting);
|
||||
let tcp = match TcpStream::connect(&addr).await {
|
||||
Ok(stream) => {
|
||||
tracker.set_stage(ConnectionStage::TcpConnected);
|
||||
stream
|
||||
}
|
||||
Err(err) => {
|
||||
let (status, detail) = classify_connect_error(Some(&err), false);
|
||||
tracker.record_error(status, detail);
|
||||
return Err(err.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let probe_stream = DpiProbeStream::new(tcp, tracker.clone());
|
||||
let server_name = match ServerName::try_from(domain.to_string()) {
|
||||
Ok(name) => name,
|
||||
Err(err) => {
|
||||
let msg = format!("invalid domain for TLS SNI: {}", err);
|
||||
tracker.record_error(DpiStatus::Unknown, &msg);
|
||||
return Err(msg);
|
||||
}
|
||||
};
|
||||
|
||||
let tls_stream = match conn.connect(server_name, probe_stream).await {
|
||||
Ok(stream) => {
|
||||
tracker.set_stage(ConnectionStage::TlsHandshakeDone);
|
||||
stream
|
||||
}
|
||||
Err(err) => {
|
||||
let err_str = err.to_string();
|
||||
let lock = tracker.state.lock();
|
||||
let (status, detail) = classify_tls_error(
|
||||
lock.stage,
|
||||
lock.bytes_sent,
|
||||
lock.bytes_recv,
|
||||
&err_str,
|
||||
false,
|
||||
);
|
||||
drop(lock);
|
||||
tracker.record_error(status, detail);
|
||||
return Err(err_str);
|
||||
}
|
||||
};
|
||||
|
||||
// Send minimal HTTP GET /
|
||||
tracker.set_stage(ConnectionStage::HttpPayload);
|
||||
let alpn_h2 = negotiated_h2(&tls_stream);
|
||||
let io = TokioIo::new(tls_stream);
|
||||
let mut sender = match HttpSender::handshake(io, alpn_h2).await {
|
||||
Ok(sender) => sender,
|
||||
Err(err) => {
|
||||
tracker.record_error(DpiStatus::Unknown, format!("HTTP handshake failed: {}", err));
|
||||
return Err(err.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let req = HttpRequest {
|
||||
method: Method::GET,
|
||||
host: domain,
|
||||
path: "/",
|
||||
user_agent: DEFAULT_USER_AGENT,
|
||||
headers: Vec::new(),
|
||||
};
|
||||
|
||||
let resp = match sender.send(req).await {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
tracker.record_error(DpiStatus::Unknown, format!("HTTP request failed: {}", err));
|
||||
return Err(err.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
// Check for suspicious redirect location headers
|
||||
if let Some(loc) = resp.headers().get("location") {
|
||||
if let Ok(loc_str) = loc.to_str() {
|
||||
if is_suspicious_redirect(loc_str) {
|
||||
tracker.record_error(
|
||||
DpiStatus::HttpBlocked,
|
||||
format!("ISP blockpage redirect detected: {}", loc_str),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = resp.into_body().collect().await;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match timeout(timeout_dur, probe_future).await {
|
||||
Ok(Ok(())) => {
|
||||
let elapsed = start.elapsed().as_millis() as u64;
|
||||
let mut metrics = tracker.get_metrics(elapsed);
|
||||
if metrics.status == DpiStatus::Unknown {
|
||||
metrics.status = DpiStatus::Ok;
|
||||
}
|
||||
metrics
|
||||
}
|
||||
Ok(Err(_)) => {
|
||||
let elapsed = start.elapsed().as_millis() as u64;
|
||||
tracker.get_metrics(elapsed)
|
||||
}
|
||||
Err(_) => {
|
||||
let elapsed = start.elapsed().as_millis() as u64;
|
||||
let lock = tracker.state.lock();
|
||||
let (status, detail) = classify_tls_error(
|
||||
lock.stage,
|
||||
lock.bytes_sent,
|
||||
lock.bytes_recv,
|
||||
"timeout",
|
||||
true,
|
||||
);
|
||||
drop(lock);
|
||||
tracker.record_error(status, detail);
|
||||
tracker.get_metrics(elapsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Probes a domain with the default insecure DPI TLS connector.
|
||||
pub async fn probe_tls_domain_default(
|
||||
domain: &str,
|
||||
target_ip: IpAddr,
|
||||
port: u16,
|
||||
timeout_dur: Duration,
|
||||
) -> ProbeMetrics {
|
||||
let connector = RustlsConnector::new_insecure();
|
||||
probe_tls_domain(domain, target_ip, port, timeout_dur, &connector).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_suspicious_redirect() {
|
||||
assert!(is_suspicious_redirect("http://warning.rt.ru/blocked"));
|
||||
assert!(is_suspicious_redirect("https://eais.rkn.gov.ru/"));
|
||||
assert!(is_suspicious_redirect("http://block.mts.ru/index.html"));
|
||||
assert!(!is_suspicious_redirect("https://example.com/login"));
|
||||
assert!(!is_suspicious_redirect("/relative/path"));
|
||||
}
|
||||
}
|
||||
|
|
@ -83,9 +83,12 @@ config, profile, i18n — сбоку; бинарник — только пред
|
|||
Аудит показал ~63 `pub`-элемента без единой ссылки вне своего файла. У библиотечного
|
||||
крейта такие вещи не ловятся компилятором, поэтому чистка + защита от повторения:
|
||||
|
||||
* Удалить (или достроить и позвать из теста 2) `probe/tls.rs` — 196 строк,
|
||||
`probe_tls_domain`, `probe_tls_domain_default`, `is_suspicious_redirect`;
|
||||
сейчас живут только в реэкспорте `probe/mod.rs:12`.
|
||||
* **Решено: удалить** `probe/tls.rs` — 196 строк, `probe_tls_domain`,
|
||||
`probe_tls_domain_default`, `is_suspicious_redirect`; сейчас живут только в
|
||||
реэкспорте `probe/mod.rs:12`. Знание не теряется: те же сигнатуры блок-страниц,
|
||||
но разложенные по регионам, уже есть в `RegionProfile::blockpage_signatures`
|
||||
(`profile/mod.rs:117-149`) — это и остаётся единственным домом для них, а плоский
|
||||
регионо-независимый список из `probe/tls.rs:23-33` только дублировал его.
|
||||
* Понизить до `pub(crate)` либо удалить: `net/version.rs:154` `version_badge`
|
||||
(зовётся только из своего теста), 27 неиспользуемых реэкспортов `dns/mod.rs:10-17`
|
||||
(живой через реэкспорт — один, `parse_socks_proxy`),
|
||||
|
|
@ -167,8 +170,9 @@ config, profile, i18n — сбоку; бинарник — только пред
|
|||
* Разрывает `net → dns` (`net/netinfo.rs:21`) и делает граф ацикличным.
|
||||
* Заодно `TCP_NODELAY`: он есть в `net/tcp.rs:32`, `dns/doh.rs:79`, `dns/dot.rs:102`
|
||||
и отсутствует в шести голых дозвонах — `netinfo.rs:149`, `telegram.rs:71,191,369`,
|
||||
`probe/tls.rs:55`, `dns/socks.rs:110`. Либо ставить везде, либо перестать называть
|
||||
его требованием в `net/tcp.rs:22`.
|
||||
`probe/tls.rs:55`, `dns/socks.rs:110`. **Решено: ставить везде** — одним хелпером
|
||||
(`net::tcp::set_no_delay`), чтобы правило из `net/tcp.rs:22` («это требование, а не
|
||||
оптимизация») было верным для всех проб, а не для четырёх из десяти.
|
||||
|
||||
### P4.5 — Свернуть TLS-фабрики
|
||||
|
||||
|
|
@ -179,12 +183,23 @@ config, profile, i18n — сбоку; бинарник — только пред
|
|||
`RustlsConnector::from(profile)`. Обязательные тесты: `grease_version_leads_supported_versions`,
|
||||
`bundle_versions_match_their_ja3`, `bundle_versions_match_their_ja4`.
|
||||
|
||||
### P5 — `i18n` из ядра в бинарник (ждёт решения)
|
||||
### P5 — `i18n` из ядра в бинарник (решено: выносим)
|
||||
|
||||
Плюс: ядро перестаёт возить тексты интерфейса, слоистость становится строгой.
|
||||
Минус: разрывается связка «константы `DET_*` в ядре ↔ их переводы» (`i18n/details.rs`
|
||||
импортирует `classify::*`), правятся правила проекта и README.
|
||||
**Рекомендация:** делать после P2.5 и только если появится второй потребитель ядра.
|
||||
Решение принято. Порядок обязателен: после **P1** (иначе ядру всё равно нужен `i18n`
|
||||
для `net/version.rs`) и после **P2.5/P2.6** (чтобы прозу деталей не пришлось переносить
|
||||
дважды): сначала `detail` становится типом с `code()`, потом тексты уезжают в
|
||||
`dpi-detector/src/i18n/`.
|
||||
|
||||
* Что переезжает: `Messages` (4 блока), `legend_sections*()`, `details.rs`,
|
||||
`fingerprint_label`, `fmt_speed`/`fmt_size`, `Messages::{config_warning, phase_text}`.
|
||||
* Что остаётся в ядре: `PhaseId`/`ProgressBlock` (языконезависимые идентификаторы) и
|
||||
константы деталей; `i18n` в бинарнике импортирует их — направление зависимости
|
||||
становится односторонним.
|
||||
* Вместе с переносом правятся правила проекта (§1 этого документа и README): точка
|
||||
входа для новой строки — крейт бинарника, а не ядро.
|
||||
* Проверка: тесты паритета четырёх блоков (`test_messages_coverage`,
|
||||
`test_finglish_is_latin_only`, `i18n::details::tests`) переезжают вместе с модулем;
|
||||
живой прогон меню и `--legend` на двух языках (`--lang en`, `--lang fa`).
|
||||
|
||||
### P6 — CI-гейт
|
||||
|
||||
|
|
@ -207,7 +222,7 @@ config, profile, i18n — сбоку; бинарник — только пред
|
|||
кортеж компилируется молча. Ввести `struct TestKind`/битовый набор, потребителей —
|
||||
`menu.rs`, `runner.rs`.
|
||||
|
||||
### P9 — Типизированный конфиг (опционально, продуктовое решение)
|
||||
### P9 — Типизированный конфиг (отложено до конца рефакторинга)
|
||||
|
||||
1092 строки `config.rs` поверх `serde_yaml`: `sanitize_mapping` (`:426+`), `yaml_str`,
|
||||
приведение типов, `ConfigWarning`. Слой существует, чтобы принимать «грязные»
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue