From baff76b3e5d38743fe7aaac041e9de5c327316a7 Mon Sep 17 00:00:00 2001 From: Lowder Date: Mon, 17 Aug 2026 19:18:46 +0500 Subject: [PATCH] feat: disable traceroute --- docker-compose.yaml | 1 + .../src/lib/components/ResultPanel.svelte | 22 +++++----- .../lib/components/result/ProbeTable.svelte | 30 +++++++++----- frontend/src/routes/kb/probing/+page.svelte | 36 ++++++----------- probe/src/main.rs | 40 +++++++++++++------ reports/src/probe.rs | 2 + website/src/api/probe.rs | 10 +++-- website/src/mqtt.rs | 4 ++ website/src/whitelist.rs | 2 +- 9 files changed, 87 insertions(+), 60 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 82075bb..e2e2d18 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -34,6 +34,7 @@ services: MQTT_ADMIN_TOKEN: "${MQTT_ADMIN_TOKEN}" MQTT_HOST: rmqtt MQTT_PORT: 11883 + PROBE_TRACEROUTE_ENABLED: "${PROBE_TRACEROUTE_ENABLED:-false}" PROBE_RATE_LIMIT_RPM: "${PROBE_RATE_LIMIT_RPM:-30}" volumes: - database-cache:/var/cache/cheburcheck/databases diff --git a/frontend/src/lib/components/ResultPanel.svelte b/frontend/src/lib/components/ResultPanel.svelte index 0002e50..9406865 100644 --- a/frontend/src/lib/components/ResultPanel.svelte +++ b/frontend/src/lib/components/ResultPanel.svelte @@ -98,16 +98,18 @@ const providerCidrs = (provider: Provider) => {#if result.reverseLookup.length > 0} - {#each result.reverseLookup as ptr} - - - {ptr} - - - {/each} +
+ {#each result.reverseLookup as ptr} + + + {ptr} + + + {/each} +
{/if} diff --git a/frontend/src/lib/components/result/ProbeTable.svelte b/frontend/src/lib/components/result/ProbeTable.svelte index 14eced3..204b0a0 100644 --- a/frontend/src/lib/components/result/ProbeTable.svelte +++ b/frontend/src/lib/components/result/ProbeTable.svelte @@ -8,7 +8,6 @@ import { CircleX, LoaderCircle, ShieldCheck, - TriangleAlert, } from "@lucide/svelte"; import type { ProbeResult, ProbeStatus } from "$lib/api/probe"; @@ -59,7 +58,7 @@ const verdictStyles = { }, whitelist: { icon: ShieldCheck, - text: "Белый список", + text: "Исключение для CDN", class: "text-amber-500", bg: "bg-amber-500/10", border: "border-amber-500/20", @@ -149,12 +148,23 @@ const verdictStyles = {
{probe.asn || ""}
-
- - {style.text} -
+ {#if probe.verdict === "whitelist"} + event.stopPropagation()} + class={`inline-flex items-center gap-1.5 px-2 py-1 rounded border ${style.bg} ${style.border} ${style.class} text-xs font-bold transition-colors hover:bg-amber-500/20 hover:border-amber-500/50 hover:text-amber-400`} + > + + {style.text} + + {:else} +
+ + {style.text} +
+ {/if} {#if isExpanded} @@ -167,7 +177,7 @@ const verdictStyles = { {#if isExpanded} - {#if probe.verdict === "tspu_block"} +
{#each probe.host_results as host}
Сканер не загружает целевой сайт целиком. Для домена он делает небольшие контрольные HTTPS-загрузки с тестовых хостов, подставляя проверяемое имя в - SNI, а до разрешённого IP-адреса выполняет короткую TCP-трассировку на порт - 443. Эти проверки позволяют независимо увидеть блокировку по имени домена и - аномально раннее завершение маршрута до адреса. + SNI.

Методика сканирования

- Каждая задача содержит IP-адрес цели и, если пользователь проверяет домен, - само доменное имя. SNI-проверка, трассировка цели и контрольные трассировки - запускаются одновременно, чтобы состояние сети во время измерений было как - можно более одинаковым. + Сканер подключается к настроенным тестовым хостам, но передаёт проверяемый + домен в TLS ClientHello и HTTP Host. Затем он оценивает, удалось ли + завершить TLS-обмен и получить минимальный объём данных. Для проверки + IP-адреса без домена этот этап пропускается.

-
    -
  1. - Сканер подключается к настроенным тестовым хостам, но передаёт проверяемый - домен в TLS ClientHello и HTTP Host. Затем он оценивает, удалось ли - завершить TLS-обмен и получить минимальный объём данных. Для проверки - IP-адреса без домена этот этап пропускается. -
  2. -
  3. + + Что показывает результат @@ -116,19 +106,19 @@ import KbNote from "$lib/components/kb/KbNote.svelte"; практике это похоже на ситуацию, когда оборудование оператора (ТСПУ) видит имя домена и разрывает соединение до передачи полезных данных.
  4. -
  5. +
  6. Белый список – данные передаются в полном объеме при подстановке - домена в SNI при подключении к заблокированному серверу. + >Исключение для CDN – данные передаются в полном объеме при + подстановке домена в SNI при подключении к заблокированному серверу.
  7. diff --git a/probe/src/main.rs b/probe/src/main.rs index 348104a..7b22588 100644 --- a/probe/src/main.rs +++ b/probe/src/main.rs @@ -186,21 +186,23 @@ async fn update_config( let value: ProbeConfig = serde_json::from_slice(payload).context("decode probe config")?; let mut control_hosts_v4 = HashSet::new(); let mut control_hosts_v6 = HashSet::new(); - for domain in &value.control_hosts { - match tokio::net::lookup_host((domain.as_str(), 443)).await { - Ok(addresses) => { - for address in addresses { - match address.ip() { - IpAddr::V4(address) => { - control_hosts_v4.insert(address); - } - IpAddr::V6(address) => { - control_hosts_v6.insert(address); + if value.traceroute_enabled { + for domain in &value.control_hosts { + match tokio::net::lookup_host((domain.as_str(), 443)).await { + Ok(addresses) => { + for address in addresses { + match address.ip() { + IpAddr::V4(address) => { + control_hosts_v4.insert(address); + } + IpAddr::V6(address) => { + control_hosts_v6.insert(address); + } } } } + Err(error) => warn!("failed to resolve control host {domain}: {error}"), } - Err(error) => warn!("failed to resolve control host {domain}: {error}"), } } *config.write().await = Some(LoadedProbeConfig { @@ -271,7 +273,13 @@ async fn handle_task( let result_topic = format!("probe/results/v1/{job_id}/{}", args.probe_id); let config = config.read().await.clone(); + let traceroute_enabled = config + .as_ref() + .is_some_and(|config| config.config.traceroute_enabled); let control_targets = config.as_ref().map_or_else(Vec::new, |config| { + if !traceroute_enabled { + return Vec::new(); + } let mut rng = rand::thread_rng(); match task.ip { IpAddr::V4(_) => config @@ -295,8 +303,14 @@ async fn handle_task( job_id, task.timeout_ms, ); - let target_traceroute = - traceroute::tcp_traceroute(task.ip, args.traceroute_max_hops, args.traceroute_retries); + let target_traceroute = async { + if traceroute_enabled { + traceroute::tcp_traceroute(task.ip, args.traceroute_max_hops, args.traceroute_retries) + .await + } else { + None + } + }; let control_traceroute = async { join_all(control_targets.into_iter().map(|target| { traceroute::tcp_traceroute(target, args.traceroute_max_hops, args.traceroute_retries) diff --git a/reports/src/probe.rs b/reports/src/probe.rs index b749eea..81af713 100644 --- a/reports/src/probe.rs +++ b/reports/src/probe.rs @@ -15,6 +15,8 @@ pub struct ProbeConfig { pub published_at: String, pub hosts: Vec, #[serde(default)] + pub traceroute_enabled: bool, + #[serde(default)] pub control_hosts: Vec, } diff --git a/website/src/api/probe.rs b/website/src/api/probe.rs index c02ae74..2f96160 100644 --- a/website/src/api/probe.rs +++ b/website/src/api/probe.rs @@ -55,6 +55,10 @@ pub async fn probe_query( return Err(Status::BadRequest); } }; + let probe_config = mqtt.probe_config(); + if domain.is_none() && !probe_config.traceroute_enabled { + return Err(Status::BadRequest); + } let ip = resolved_ips .first() .and_then(|ip| ip.parse::().ok()) @@ -77,7 +81,6 @@ pub async fn probe_query( let timeout = mqtt.task_timeout(); let online_probes = mqtt.online_probe_count().await; - let probe_config = mqtt.probe_config(); let pool = pool.inner().clone(); let query_id = id; let id = id.to_string(); @@ -428,7 +431,7 @@ mod tests { let target = icmp_trace(5); assert_eq!( build_probe_verdict(&[], &empty_config(), Some(&target), Some(&control)), - "uncertain" + "ok" ); } @@ -441,7 +444,7 @@ mod tests { let control = icmp_trace(5); assert_eq!( build_probe_verdict(&[], &empty_config(), Some(&target), Some(&control)), - "uncertain" + "ok" ); } @@ -451,6 +454,7 @@ mod tests { task_timeout_ms: 0, published_at: String::new(), hosts: vec![], + traceroute_enabled: true, control_hosts: vec![], } } diff --git a/website/src/mqtt.rs b/website/src/mqtt.rs index 948461c..4a3ebba 100644 --- a/website/src/mqtt.rs +++ b/website/src/mqtt.rs @@ -88,6 +88,7 @@ impl MqttPublisher { task_timeout_ms, published_at: Utc::now().to_rfc3339(), hosts: Vec::new(), + traceroute_enabled: false, control_hosts: Vec::new(), } })); @@ -267,6 +268,9 @@ fn load_probe_config(task_timeout_ms: u64) -> Result task_timeout_ms, published_at: Utc::now().to_rfc3339(), hosts: config.hosts, + traceroute_enabled: std::env::var("PROBE_TRACEROUTE_ENABLED") + .ok() + .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")), control_hosts: config.control_hosts, }) } diff --git a/website/src/whitelist.rs b/website/src/whitelist.rs index 8bb4ec5..1f9a211 100644 --- a/website/src/whitelist.rs +++ b/website/src/whitelist.rs @@ -8,7 +8,7 @@ use rocket_cache_response::CacheResponse; use sqlx::postgres::PgPool; use std::io; -enum ExportType { +pub(crate) enum ExportType { Full, Domains, }