feat: disable traceroute

This commit is contained in:
Lowder
2026-08-17 19:18:46 +05:00
parent f101951e9b
commit baff76b3e5
9 changed files with 87 additions and 60 deletions
+1
View File
@@ -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
+12 -10
View File
@@ -98,16 +98,18 @@ const providerCidrs = (provider: Provider) =>
{#if result.reverseLookup.length > 0}
<DetailRow label="Обратный DNS" icon={EthernetPort}>
{#each result.reverseLookup as ptr}
<span class={valueClass}>
<a
href={`/check?target=${ptr}`}
class="text-neutral-100 underline decoration-neutral-500 transition-all hover:text-white hover:decoration-neutral-100"
>
{ptr}
</a>
</span>
{/each}
<div class="flex w-full flex-col items-end gap-2">
{#each result.reverseLookup as ptr}
<span class={valueClass}>
<a
href={`/check?target=${ptr}`}
class="text-neutral-100 underline decoration-neutral-500 transition-all hover:text-white hover:decoration-neutral-100"
>
{ptr}
</a>
</span>
{/each}
</div>
</DetailRow>
{/if}
@@ -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 = {
<div class="text-xs text-neutral-500">{probe.asn || ""}</div>
</td>
<td class="p-3">
<div
class={`inline-flex items-center gap-1.5 px-2 py-1 rounded border ${style.bg} ${style.border} ${style.class} text-xs font-bold`}
>
<style.icon size={14} />
{style.text}
</div>
{#if probe.verdict === "whitelist"}
<a
href="/kb/whitelist"
onclick={(event) => 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.icon size={14} />
{style.text}
</a>
{:else}
<div
class={`inline-flex items-center gap-1.5 px-2 py-1 rounded border ${style.bg} ${style.border} ${style.class} text-xs font-bold`}
>
<style.icon size={14} />
{style.text}
</div>
{/if}
</td>
<td class="p-3 text-right">
{#if isExpanded}
@@ -167,7 +177,7 @@ const verdictStyles = {
{#if isExpanded}
<tr class="bg-neutral-900/30">
<td colspan="4" class="p-4 border-b border-neutral-800/50">
{#if probe.verdict === "tspu_block"}
<!-- {#if probe.verdict === "tspu_block"}
<div
class="mb-3 flex items-center gap-2 rounded-md border border-red-500/50 bg-red-500/15 px-3 py-2 text-red-200"
>
@@ -184,7 +194,7 @@ const verdictStyles = {
{probe.target_hop}
прыжков
</div>
{/if}
{/if} -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{#each probe.host_results as host}
<div
+13 -23
View File
@@ -49,28 +49,19 @@ import KbNote from "$lib/components/kb/KbNote.svelte";
<p>
Сканер не загружает целевой сайт целиком. Для домена он делает небольшие
контрольные HTTPS-загрузки с тестовых хостов, подставляя проверяемое имя в
SNI, а до разрешённого IP-адреса выполняет короткую TCP-трассировку на порт
443. Эти проверки позволяют независимо увидеть блокировку по имени домена и
аномально раннее завершение маршрута до адреса.
SNI.
</p>
<KbHeading id="методика-сканирования" title="Методика сканирования">
Методика сканирования
</KbHeading>
<p>
Каждая задача содержит IP-адрес цели и, если пользователь проверяет домен,
само доменное имя. SNI-проверка, трассировка цели и контрольные трассировки
запускаются одновременно, чтобы состояние сети во время измерений было как
можно более одинаковым.
Сканер подключается к настроенным тестовым хостам, но передаёт проверяемый
домен в TLS ClientHello и HTTP Host. Затем он оценивает, удалось ли
завершить TLS-обмен и получить минимальный объём данных. Для проверки
IP-адреса без домена этот этап пропускается.
</p>
<ol class="list-decimal">
<li>
Сканер подключается к настроенным тестовым хостам, но передаёт проверяемый
домен в TLS ClientHello и HTTP Host. Затем он оценивает, удалось ли
завершить TLS-обмен и получить минимальный объём данных. Для проверки
IP-адреса без домена этот этап пропускается.
</li>
<li>
<!-- <li>
Сканер открывает TCP-соединения к разрешённому IP-адресу на порту 443,
последовательно увеличивая TTL от 1 до 5. На каждом TTL по умолчанию
одновременно отправляются три попытки: это уменьшает влияние потерь
@@ -83,13 +74,12 @@ import KbNote from "$lib/components/kb/KbNote.svelte";
IP-адресов той же версии протокола, что и цель. Все контрольные
трассировки идут параллельно, а для сравнения используется наименьший
измеренный контрольный TTL.
</li>
</ol>
<p>
</li> -->
<!-- <p>
Если трассировки цели и контроля завершились ответом ICMP Time Exceeded, но
ответ для цели пришёл на меньшем TTL, сканер выставляет вердикт «ТСПУ Блок».
В результатах показывается номер последнего достигнутого перехода для цели.
</p>
</p> -->
<KbHeading id="что-показывает-результат" title="Что показывает результат">
Что показывает результат
@@ -116,19 +106,19 @@ import KbNote from "$lib/components/kb/KbNote.svelte";
практике это похоже на ситуацию, когда оборудование оператора (ТСПУ) видит
имя домена и разрывает соединение до передачи полезных данных.
</li>
<li>
<!-- <li>
<b>ТСПУ Блок</b>
– TCP-трассировка до цели остановилась раньше контрольной трассировки.
Обычно означает, что соединение разрывается на оборудовании оператора
(ТСПУ) до того как соединение дошло до цели.
</li>
</li> -->
<li>
<!-- biome-ignore format: link punctuation -->
<a
class="underline decoration-dotted underline-offset-2"
href="/kb/whitelist"
>Белый список</a> – данные передаются в полном объеме при подстановке
домена в SNI при подключении к заблокированному серверу.
>Исключение для CDN</a> – данные передаются в полном объеме при
подстановке домена в SNI при подключении к заблокированному серверу.
</li>
<li>
+27 -13
View File
@@ -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)
+2
View File
@@ -15,6 +15,8 @@ pub struct ProbeConfig {
pub published_at: String,
pub hosts: Vec<Host>,
#[serde(default)]
pub traceroute_enabled: bool,
#[serde(default)]
pub control_hosts: Vec<String>,
}
+7 -3
View File
@@ -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::<IpAddr>().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![],
}
}
+4
View File
@@ -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<ProbeConfig, PublishError>
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,
})
}
+1 -1
View File
@@ -8,7 +8,7 @@ use rocket_cache_response::CacheResponse;
use sqlx::postgres::PgPool;
use std::io;
enum ExportType {
pub(crate) enum ExportType {
Full,
Domains,
}