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 = {
- {#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-адреса без домена этот этап пропускается.
-
- -
- Сканер подключается к настроенным тестовым хостам, но передаёт проверяемый
- домен в TLS ClientHello и HTTP Host. Затем он оценивает, удалось ли
- завершить TLS-обмен и получить минимальный объём данных. Для проверки
- IP-адреса без домена этот этап пропускается.
-
- -
+
+
Что показывает результат
@@ -116,19 +106,19 @@ import KbNote from "$lib/components/kb/KbNote.svelte";
практике это похоже на ситуацию, когда оборудование оператора (ТСПУ) видит
имя домена и разрывает соединение до передачи полезных данных.
- -
+
-
Белый список – данные передаются в полном объеме при подстановке
- домена в SNI при подключении к заблокированному серверу.
+ >Исключение для CDN – данные передаются в полном объеме при
+ подстановке домена в SNI при подключении к заблокированному серверу.
-
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,
}
|