feat: filter probes with unblocked cdn

This commit is contained in:
Lowder
2026-09-23 14:48:52 +05:00
parent f9fc87c664
commit aef74d7772
6 changed files with 163 additions and 34 deletions
Generated
+1 -1
View File
@@ -4509,7 +4509,7 @@ dependencies = [
[[package]]
name = "website"
version = "1.3.7"
version = "1.3.8"
dependencies = [
"anyhow",
"dotenvy",
+5 -1
View File
@@ -44,6 +44,7 @@ export type ProbeResult = {
provider?: string | null;
asn?: string | null;
verdicts: ProbeVerdict[];
cdn_unblocked?: boolean;
host_results: ProbeHostResult[] | null;
target_hop: number | null;
dpi_hop: number | null;
@@ -61,7 +62,10 @@ export function displayProbeVerdicts(
isStaticCdn: boolean,
): DisplayProbeVerdict[] {
return probe.verdicts.map((verdict) =>
isStaticCdn && probe.host_results?.length !== 0 && verdict === "ok"
isStaticCdn &&
!probe.cdn_unblocked &&
probe.host_results?.length !== 0 &&
verdict === "ok"
? "cdn_block"
: verdict,
);
+3 -2
View File
@@ -126,8 +126,9 @@ import KbNote from "$lib/components/kb/KbNote.svelte";
<ul class="list-disc">
<li>
<b>Доступен</b>
– сайт не найден в списках CDN, не блокируется на контрольных хостах, но
может блокироваться на зарубежных CDN.
– для домена блокировка на контрольных хостах не выявлена, но возможна
блокировка на зарубежных CDN. Для IP-адреса TCP-трассировка получила ответ
после DPI-узла, поэтому блокировка на ТСПУ не выявлена.
</li>
<li>
<b>CDN Блок (16-20)</b>
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "website"
version = "1.3.7"
version = "1.3.8"
edition = "2024"
[dependencies]
@@ -0,0 +1,2 @@
ALTER TABLE reporters
ADD COLUMN IF NOT EXISTS cdn_unblocked BOOLEAN NOT NULL DEFAULT FALSE;
+151 -29
View File
@@ -25,6 +25,7 @@ pub struct ProbeReporterInfo {
pub provider: Option<String>,
pub asn: Option<String>,
pub disable_traceroutes: bool,
pub cdn_unblocked: bool,
}
#[get("/probe/<id>?<token>")]
@@ -226,6 +227,9 @@ pub fn build_probe_response(
raw.dpi_hop,
raw.dns.as_ref(),
is_ip_target,
reporter_info
.as_ref()
.is_some_and(|info| info.cdn_unblocked),
);
let target_hop =
raw.target_traceroute
@@ -261,6 +265,7 @@ pub fn build_probe_response(
"region": region,
"provider": provider,
"asn": asn,
"cdn_unblocked": reporter_info.as_ref().is_some_and(|info| info.cdn_unblocked),
"verdicts": verdicts,
"host_results": host_results,
"target_hop": target_hop,
@@ -348,7 +353,7 @@ async fn fetch_probe_reporter_info(
pool: &PgPool,
) -> Result<Option<ProbeReporterInfo>, sqlx::Error> {
sqlx::query_as::<_, ProbeReporterInfo>(
"SELECT region, provider, asn, disable_traceroutes FROM reporters WHERE id = $1 LIMIT 1",
"SELECT region, provider, asn, disable_traceroutes, cdn_unblocked FROM reporters WHERE id = $1 LIMIT 1",
)
.bind(probe_id.parse::<i32>().unwrap_or(-1))
.fetch_optional(pool)
@@ -362,6 +367,7 @@ fn build_probe_verdicts(
dpi_hop: Option<u8>,
dns: Option<&reports::probe::DnsProbeResult>,
is_ip_target: bool,
cdn_unblocked: bool,
) -> Vec<&'static str> {
if results.is_empty() && target_traceroute.is_none() && dns.is_none() {
return vec!["uncertain"];
@@ -371,17 +377,19 @@ fn build_probe_verdicts(
&& target_traceroute
.is_some_and(|traceroute| matches!(&traceroute.result, TcpTracerouteOutcome::Timeout));
// IP tasks do not run the SNI/CDN host checks. A reachable IP therefore does not prove that
// a statically listed CDN address is unblocked.
if is_ip_target {
return if tspu_block {
vec!["tspu_block"]
} else if target_traceroute
.is_some_and(|traceroute| !matches!(&traceroute.result, TcpTracerouteOutcome::Timeout))
{
vec!["ok"]
} else {
vec!["uncertain"]
};
}
let host_verdict = build_host_verdict(results, config);
let host_verdict = build_host_verdict(results, config, cdn_unblocked);
let dns_spoofing = dns.is_some_and(|result| result.spoofing_detected);
let mut verdicts = [
@@ -400,7 +408,11 @@ fn build_probe_verdicts(
verdicts
}
fn build_host_verdict(results: &[HostProbeResult], config: &ProbeConfig) -> &'static str {
fn build_host_verdict(
results: &[HostProbeResult],
config: &ProbeConfig,
cdn_unblocked: bool,
) -> &'static str {
let matched = results
.iter()
.filter_map(|result| {
@@ -430,19 +442,21 @@ fn build_host_verdict(results: &[HostProbeResult], config: &ProbeConfig) -> &'st
.count(),
) {
"sni_block"
} else if is_strict_majority(
matched
.iter()
.filter(|(h, _)| matches!(h.host_type, HostType::Blacklist))
.count(),
matched
.iter()
.filter(|(host, evidence)| {
matches!(host.host_type, HostType::Blacklist)
&& !matches!(evidence, ProbeEvidence::DataTimeout { .. })
})
.count(),
) {
} else if !cdn_unblocked
&& is_strict_majority(
matched
.iter()
.filter(|(h, _)| matches!(h.host_type, HostType::Blacklist))
.count(),
matched
.iter()
.filter(|(host, evidence)| {
matches!(host.host_type, HostType::Blacklist)
&& !matches!(evidence, ProbeEvidence::DataTimeout { .. })
})
.count(),
)
{
"whitelist"
} else {
let blacklist = matched
@@ -454,12 +468,18 @@ fn build_host_verdict(results: &[HostProbeResult], config: &ProbeConfig) -> &'st
.filter(|(host, _)| matches!(host.host_type, HostType::Whitelist))
.collect::<Vec<_>>();
let most_blacklist_timed_out = !blacklist.is_empty()
let most_blacklist_matches_baseline = !blacklist.is_empty()
&& is_strict_majority(
blacklist.len(),
blacklist
.iter()
.filter(|(_, evidence)| matches!(evidence, ProbeEvidence::DataTimeout { .. }))
.filter(|(_, evidence)| {
if cdn_unblocked {
matches!(evidence, ProbeEvidence::Good)
} else {
matches!(evidence, ProbeEvidence::DataTimeout { .. })
}
})
.count(),
);
let most_whitelist_good = !whitelist.is_empty()
@@ -471,7 +491,7 @@ fn build_host_verdict(results: &[HostProbeResult], config: &ProbeConfig) -> &'st
.count(),
);
if most_blacklist_timed_out && most_whitelist_good {
if most_blacklist_matches_baseline && most_whitelist_good {
"ok"
} else {
"uncertain"
@@ -562,7 +582,7 @@ mod tests {
#[test]
fn no_checks_returns_uncertain() {
assert_eq!(
build_probe_verdicts(&[], &empty_config(), None, None, None, false),
build_probe_verdicts(&[], &empty_config(), None, None, None, false, false),
vec!["uncertain"]
);
}
@@ -574,17 +594,33 @@ mod tests {
result: TcpTracerouteOutcome::Timeout,
};
assert_eq!(
build_probe_verdicts(&[], &empty_config(), Some(&timeout), Some(4), None, true),
build_probe_verdicts(
&[],
&empty_config(),
Some(&timeout),
Some(4),
None,
true,
false
),
vec!["tspu_block"]
);
assert_eq!(
build_probe_verdicts(&[], &empty_config(), Some(&timeout), None, None, true),
build_probe_verdicts(
&[],
&empty_config(),
Some(&timeout),
None,
None,
true,
false
),
vec!["uncertain"]
);
}
#[test]
fn reachable_ip_target_is_uncertain() {
fn reachable_ip_target_is_ok() {
for result in [
TcpTracerouteOutcome::IcmpTimeExceeded { hop: 5 },
TcpTracerouteOutcome::Connected { hop: 5 },
@@ -595,8 +631,16 @@ mod tests {
result,
};
assert_eq!(
build_probe_verdicts(&[], &empty_config(), Some(&target), Some(4), None, true),
vec!["uncertain"]
build_probe_verdicts(
&[],
&empty_config(),
Some(&target),
Some(4),
None,
true,
false
),
vec!["ok"]
);
}
}
@@ -625,7 +669,7 @@ mod tests {
};
assert_eq!(
build_probe_verdicts(&results, &config, None, None, Some(&dns), false),
build_probe_verdicts(&results, &config, None, None, Some(&dns), false, false),
vec!["sni_block", "dns_spoofing"]
);
}
@@ -647,11 +691,89 @@ mod tests {
}];
assert_eq!(
build_probe_verdicts(&results, &config, None, None, None, false),
build_probe_verdicts(&results, &config, None, None, None, false, false),
vec!["whitelist"]
);
}
#[test]
fn accessible_cdn_is_not_a_whitelist_exception_for_unblocked_reporter() {
let mut config = empty_config();
for (id, host_type) in [
("foreign-cdn", HostType::Blacklist),
("control", HostType::Whitelist),
] {
config.hosts.push(Host {
id: id.to_string(),
host: "192.0.2.1".to_string(),
host_type,
file_path: String::new(),
timeout_sec: 1,
min_data: 1,
});
}
let results = ["foreign-cdn", "control"]
.into_iter()
.map(|host_id| HostProbeResult {
host_id: host_id.to_string(),
probe_evidence: ProbeEvidence::Good,
})
.collect::<Vec<_>>();
assert_eq!(
build_probe_verdicts(&results, &config, None, None, None, false, false),
vec!["whitelist"]
);
assert_eq!(
build_probe_verdicts(&results, &config, None, None, None, false, true),
vec!["ok"]
);
let response = build_probe_response(
ProbeResultEvent {
job_id: "job".to_string(),
probe_id: "42".to_string(),
host_results: results,
target_traceroute: None,
dpi_hop: None,
dns: None,
},
&config,
Some(ProbeReporterInfo {
region: None,
provider: None,
asn: None,
disable_traceroutes: false,
cdn_unblocked: true,
}),
false,
);
assert_eq!(response["cdn_unblocked"], json!(true));
assert_eq!(response["verdicts"], json!(["ok"]));
}
#[test]
fn unblocked_reporter_still_detects_sni_block() {
let mut config = empty_config();
config.hosts.push(Host {
id: "control".to_string(),
host: "192.0.2.1".to_string(),
host_type: HostType::Whitelist,
file_path: String::new(),
timeout_sec: 1,
min_data: 1,
});
let results = vec![HostProbeResult {
host_id: "control".to_string(),
probe_evidence: ProbeEvidence::ClientHello,
}];
assert_eq!(
build_probe_verdicts(&results, &config, None, None, None, false, true),
vec!["sni_block"]
);
}
fn empty_config() -> ProbeConfig {
ProbeConfig {
version: String::new(),