feat!: svelte rewrite (#68)

* feat!: svelte rewrite

* feat: remove mock

* fix: formatting and optimisation

* feat: db optimizations

* feat: dockerfile and version

* ci: change dockerfile syntax

* fix: remove domain trie
This commit is contained in:
LowderPlay
2026-05-28 22:34:50 +05:00
committed by GitHub
parent 741ae887f5
commit bac3ab1507
94 changed files with 4254 additions and 1578 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "website"
version = "0.4.3"
version = "1.0.0"
edition = "2024"
[dependencies]
@@ -11,6 +11,7 @@ rocket-client-addr = "0.5.4"
serde = { workspace = true }
reports = { path = "../reports" }
querying = { path = "../querying" }
frontend = { path = "../frontend" }
env_logger = "0.11.10"
rocket-cache-response = "0.6.4"
log = { workspace = true }
-73
View File
@@ -1,79 +1,6 @@
use std::{
env,
fs::File,
io::{self, Cursor},
path::Path,
};
use std::collections::HashMap;
use std::path::PathBuf;
use flate2::read::GzDecoder;
use reqwest::blocking::Client;
use serde::Deserialize;
use tar::Archive;
#[derive(Deserialize)]
struct NpmMetadata {
dist: Dist,
}
#[derive(Deserialize)]
struct Dist {
tarball: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=migrations");
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
println!("cargo:rerun-if-env-changed=LUCIDE_VERSION");
install_npm("lucide", option_env!("LUCIDE_VERSION"), HashMap::from([
(PathBuf::from("package/dist/umd/lucide.min.js"), out_dir.join("lucide.js")),
]))?;
println!("cargo:rerun-if-env-changed=CHARTJS_VERSION");
install_npm("chart.js", option_env!("CHARTJS_VERSION"), HashMap::from([
(PathBuf::from("package/dist/chart.umd.min.js"), out_dir.join("chart.js")),
]))?;
println!("cargo:rerun-if-env-changed=CHARTDATALABELS_VERSION");
install_npm("chartjs-plugin-datalabels", option_env!("CHARTDATALABELS_VERSION"), HashMap::from([
(PathBuf::from("package/dist/chartjs-plugin-datalabels.min.js"), out_dir.join("chartjs-plugin-datalabels.js")),
]))?;
println!("cargo:rerun-if-changed=build.rs");
Ok(())
}
fn install_npm(package: &str, version: Option<&str>, mut entries: HashMap<PathBuf, PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let metadata_url = format!(
"https://registry.npmjs.org/{package}/{}",
version.unwrap_or("latest")
);
println!("Fetching npm metadata from {}", metadata_url);
let metadata: NpmMetadata = client.get(metadata_url).send()?.json()?;
println!("Downloading tarball from {}", metadata.dist.tarball);
let tarball_bytes = client.get(&metadata.dist.tarball).send()?.bytes()?;
let tar = GzDecoder::new(Cursor::new(tarball_bytes));
let mut archive = Archive::new(tar);
for entry in archive.entries()? {
let mut entry = entry?;
let path = PathBuf::from(entry.path()?);
if let Some(dest) = entries.remove(&path) {
println!("Found {:?}, extracting to {:?}", path.file_name(), dest);
let mut out_file = File::create(&dest)?;
io::copy(&mut entry, &mut out_file)?;
}
}
if !entries.is_empty() {
panic!("Not all entries found! {:?}", entries);
}
Ok(())
}
@@ -0,0 +1,61 @@
ALTER TABLE
queries
ALTER COLUMN
id
SET
DEFAULT uuidv7();
CREATE INDEX IF NOT EXISTS reports_reporter_date_id_desc_idx ON reports (reporter, date DESC, id DESC);
CREATE INDEX IF NOT EXISTS report_row_domain_idx ON report_row (domain);
DROP MATERIALIZED VIEW IF EXISTS whitelist;
CREATE MATERIALIZED VIEW whitelist AS WITH report_domains AS (
SELECT
DISTINCT domain
FROM
report_row
)
SELECT
latest.domain,
d.rank,
MAX(
CASE
WHEN latest.evidence = 'ok' THEN latest.date
END
) AS last_ok
FROM
report_domains rd
CROSS JOIN LATERAL (
SELECT
rd.domain,
rr.evidence,
r.date
FROM
reports r
JOIN report_row rr ON rr.report_id = r.id
AND rr.domain = rd.domain
WHERE
r.reporter = 1
ORDER BY
r.date DESC,
r.id DESC
LIMIT
5
) latest
LEFT JOIN domains d ON d.domain = latest.domain
GROUP BY
latest.domain,
d.rank
HAVING
COUNT(*) FILTER (
WHERE
latest.evidence = 'ok'
) >= COUNT(*) / 2.0
ORDER BY
d.rank;
CREATE UNIQUE INDEX IF NOT EXISTS whitelist_domain_idx ON whitelist (domain);
CREATE INDEX IF NOT EXISTS whitelist_rank_idx ON whitelist (rank);
+5 -2
View File
@@ -1,10 +1,10 @@
use reports::AgencyReport;
use rocket::State;
use rocket::http::Status;
use rocket::serde::json::serde_json::json;
use rocket::serde::json::{Json, Value};
use rocket::serde::msgpack::MsgPack;
use rocket_client_addr::ClientRealAddr;
use rocket::State;
use sqlx::Acquire;
use sqlx::postgres::PgPool;
@@ -20,7 +20,10 @@ pub async fn upload_report(
agency: Agency,
pool: &State<PgPool>,
) -> Result<Json<Value>, (Status, String)> {
let mut db = pool.acquire().await.map_err(|e| (Status::InternalServerError, e.to_string()))?;
let mut db = pool
.acquire()
.await
.map_err(|e| (Status::InternalServerError, e.to_string()))?;
let mut tx = db
.begin()
.await
+56 -4
View File
@@ -1,4 +1,4 @@
use crate::db::{check_whitelist, save_query, WhitelistedEntry};
use crate::db::{WhitelistedEntry, check_whitelist, save_query};
use governor::clock::DefaultClock;
use governor::state::keyed::DefaultKeyedStateStore;
use governor::{Quota, RateLimiter};
@@ -8,20 +8,21 @@ use querying::geoip::IpInfo;
use querying::lists::NetworkRecord;
use querying::target::Target;
use querying::{Check, CheckError, CheckVerdict, Checker};
use rocket::State;
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::tokio::sync::RwLock;
use rocket::State;
use rocket_client_addr::ClientRealAddr;
use serde::Serialize;
use sqlx::postgres::PgPool;
use sqlx::types::Uuid;
use sqlx::types::chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::net::IpAddr;
use std::num::NonZeroU32;
use std::sync::Arc;
pub type ApiRateLimiter =
RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
pub type ApiRateLimiter = RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
pub fn build_rate_limiter(per_minute: u32) -> ApiRateLimiter {
RateLimiter::keyed(Quota::per_minute(
@@ -45,6 +46,8 @@ pub struct ApiCheckResponse {
pub asn_info: Option<AsnInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub whitelist: Option<WhitelistedEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subnet_size: Option<String>,
}
fn build_response(
@@ -79,6 +82,7 @@ fn build_response(
geo: check.geo,
asn_info: check.asn_info,
whitelist,
subnet_size: target.subnet_size(),
}
}
@@ -131,3 +135,51 @@ pub async fn check(
}
}
}
#[get("/healthcheck")]
pub async fn healthcheck(checker: &State<Arc<RwLock<Checker>>>) -> (Status, String) {
if checker.read().await.last_update().is_some() {
(Status::Ok, "OK".to_string())
} else {
(Status::InternalServerError, "LOADING DATABASES".to_string())
}
}
#[post("/feedback/<uuid>/<works>")]
pub async fn feedback(
uuid: &str,
works: bool,
pool: &State<PgPool>,
addr: &ClientRealAddr,
) -> Result<(), Status> {
sqlx::query!(
"INSERT INTO human_reports (id, source_ip, works) VALUES ($1, $2, $3)",
Uuid::try_parse(uuid).map_err(|_| Status::BadRequest)?,
addr.ip.to_string(),
works
)
.execute(&**pool)
.await
.map_err(|_| Status::InternalServerError)?;
Ok(())
}
#[derive(Serialize)]
pub struct ApiStatusResponse {
domain_count: usize,
v4_count: usize,
last_update: Option<DateTime<Utc>>,
version: &'static str,
}
#[get("/status")]
pub async fn get_system_status(checker: &State<Arc<RwLock<Checker>>>) -> Json<ApiStatusResponse> {
let checker_ref = checker.read().await;
Json(ApiStatusResponse {
domain_count: checker_ref.total_domains().await,
v4_count: checker_ref.total_v4s().await,
last_update: checker_ref.last_update(),
version: env!("CARGO_PKG_VERSION"),
})
}
+20 -15
View File
@@ -2,14 +2,14 @@ use crate::agency::Agency;
use querying::target::Target;
use querying::{Check, CheckVerdict, Checker};
use rocket::http::Status;
use rocket::outcome::{try_outcome, IntoOutcome};
use rocket::outcome::{IntoOutcome, try_outcome};
use rocket::request::{FromRequest, Outcome};
use rocket::tokio::sync::RwLockReadGuard;
use rocket::{Request, State};
use rocket_client_addr::ClientRealAddr;
use serde::Serialize;
use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::Uuid;
use sqlx::types::chrono::NaiveDateTime;
pub async fn save_query(
db: &mut sqlx::PgConnection,
@@ -40,12 +40,14 @@ pub async fn save_query(
let (resolved_ips, cdn_networks) = match target {
Target::Asn(_) => (vec![], vec![]),
_ => (check
.ips
.iter()
.map(|i| i.to_string())
.collect::<Vec<String>>(),
cdn_networks)
_ => (
check
.ips
.iter()
.map(|i| i.to_string())
.collect::<Vec<String>>(),
cdn_networks,
),
};
let id = sqlx::query_scalar(
@@ -93,11 +95,12 @@ impl<'r> FromRequest<'r> for Agency {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let pool = request.guard::<&State<sqlx::PgPool>>().await.unwrap();
let mut db = try_outcome!(pool
.acquire()
.await
.map_err(|e| Some(e))
.or_forward(Status::InternalServerError));
let mut db = try_outcome!(
pool.acquire()
.await
.map_err(|e| Some(e))
.or_forward(Status::InternalServerError)
);
let token = request.headers().get_one("Authorization");
let token = try_outcome!(
@@ -166,7 +169,6 @@ pub async fn collect_histogram(
limit: i32,
filter: bool,
) -> Result<Vec<WhitelistHistogramBin>, sqlx::Error> {
sqlx::query_as!(
WhitelistHistogramBin,
"WITH bins AS (
@@ -181,7 +183,10 @@ FROM bins b
LEFT JOIN whitelist w
ON FLOOR(w.rank / $2) = b.bin
GROUP BY b.bin
ORDER BY b.bin;", bins, limit / bins, filter
ORDER BY b.bin;",
bins,
limit / bins,
filter
)
.fetch_all(db)
.await
+83 -245
View File
@@ -5,205 +5,21 @@ mod api;
mod db;
mod whitelist;
use crate::db::{check_whitelist, save_query, WhitelistedEntry};
use log::{error, info, warn, LevelFilter};
use querying::target::Target;
use querying::{Check, CheckError, CheckVerdict, Checker};
use env_logger::Env;
use log::{LevelFilter, error, info};
use querying::Checker;
use rocket::fairing::AdHoc;
use rocket::fs::FileServer;
use rocket::http::Status;
use rocket::response::content::RawJavaScript;
use rocket::http::{ContentType, Status};
use rocket::response::content::RawHtml;
use rocket::serde::json::Json;
use rocket::tokio::sync::RwLock;
use rocket::tokio::time;
use rocket::{fairing, tokio, Build, Request, Rocket, State};
use rocket_cache_response::CacheResponse;
use rocket_client_addr::ClientRealAddr;
use rocket_dyn_templates::{context, Metadata, Template};
use rocket::{Build, Request, Rocket, fairing, tokio};
use serde::Serialize;
use sqlx::postgres::PgPool;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use env_logger::Env;
use rocket::serde::json::Json;
use sqlx::types::Uuid;
use sqlx::postgres::PgPool;
#[derive(Serialize)]
struct GlobalContext {
version: &'static str,
}
impl GlobalContext {
fn new() -> Self {
GlobalContext {
version: env!("CARGO_PKG_VERSION"),
}
}
}
#[get("/")]
async fn index(checker: &State<Arc<RwLock<Checker>>>) -> Template {
let checker_ref = checker.read().await;
Template::render(
"index",
context! {
global: GlobalContext::new(),
domain_count: format_number(checker_ref.total_domains().await),
v4_count: format_number(checker_ref.total_v4s().await),
last_update: checker_ref.last_update(),
},
)
}
#[get("/kb/<page>")]
fn page(metadata: Metadata, page: &str) -> Option<Template> {
let page = format!("pages/{}", page);
if !metadata.contains_template(&page) {
return None;
}
Some(Template::render(
page,
context! {
global: GlobalContext::new(),
},
))
}
#[get("/healthcheck")]
async fn healthcheck(checker: &State<Arc<RwLock<Checker>>>) -> (Status, String) {
if checker.read().await.last_update().is_some() {
(Status::Ok, "OK".to_string())
} else {
(Status::InternalServerError, "LOADING DATABASES".to_string())
}
}
#[post("/feedback/<uuid>/<works>")]
async fn feedback(uuid: &str, works: bool, pool: &State<PgPool>, addr: &ClientRealAddr) -> Result<(), Status> {
sqlx::query!(
"INSERT INTO human_reports (id, source_ip, works) VALUES ($1, $2, $3)",
Uuid::try_parse(uuid).map_err(|_| Status::BadRequest)?,
addr.ip.to_string(),
works
).execute(&**pool).await.map_err(|_| Status::InternalServerError)?;
Ok(())
}
#[get("/check?<target>")]
async fn check(
target: &str,
checker: &State<Arc<RwLock<Checker>>>,
addr: &ClientRealAddr,
pool: &State<PgPool>,
) -> Result<Template, Status> {
let target = Target::from(target.trim());
let check = checker.read().await.check(target.clone()).await;
let mut db = pool.acquire().await.map_err(|_| Status::InternalServerError)?;
let id: Option<String> = if let Ok(check) = &check {
match save_query(&mut *db, &target, check, addr, checker.read().await).await {
Ok(id) => Some(id.to_string()),
Err(e) => {
warn!("Failed to save check: {:?}", e);
None
}
}
} else {
None
};
let whitelist: Option<WhitelistedEntry> = if let Target::Domain(domain) = &target {
check_whitelist(domain, &mut *db)
.await
.map_err(|_| Status::InternalServerError)?
} else {
None
};
match check {
Err(CheckError::NotFound) => Ok(Template::render(
"empty",
context! {
global: GlobalContext::new(),
target: target.to_query(),
target_type: target.readable_type(),
},
)),
Ok(Check {
verdict: CheckVerdict::Clear,
geo,
ips,
rkn_subnets,
asn_info,
}) => Ok(Template::render(
"result",
context! {
id,
global: GlobalContext::new(),
found: false,
target: target.to_query(),
target_type: target.readable_type(),
blocked_subnets: rkn_subnets.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>(),
whitelist,
ips,
geo,
subnet_size: target.subnet_size(),
asn_info,
},
)),
Ok(Check {
verdict:
CheckVerdict::Blocked {
rkn_domain,
cdn_provider_subnets,
},
geo,
rkn_subnets,
ips,
asn_info,
}) => Ok(Template::render(
"result",
context! {
id,
global: GlobalContext::new(),
found: true,
domain: rkn_domain,
providers: cdn_provider_subnets,
blocked_subnets: rkn_subnets.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>(),
target: target.to_query(),
target_type: target.readable_type(),
whitelist,
ips,
geo,
subnet_size: target.subnet_size(),
asn_info,
},
)),
Err(e) => {
error!("check failed {:?}", e);
Err(Status::InternalServerError)
}
}
}
#[catch(default)]
fn default(status: Status, _req: &Request) -> Template {
Template::render(
"error",
context! {
global: GlobalContext::new(),
status: status.code,
reason: status.reason_lossy(),
},
)
}
#[derive(Debug, Serialize)]
struct JsonError {
@@ -213,44 +29,57 @@ struct JsonError {
#[catch(default)]
fn api_error(status: Status, _: &Request) -> Json<JsonError> {
Json(JsonError { code: status.code, info: status.reason_lossy().to_string() })
Json(JsonError {
code: status.code,
info: status.reason_lossy().to_string(),
})
}
#[rocket::get("/lucide.js")]
fn lucide() -> CacheResponse<RawJavaScript<&'static [u8]>> {
CacheResponse::Public {
responder: RawJavaScript(include_bytes!(concat!(env!("OUT_DIR"), "/lucide.js"))),
max_age: 604800,
must_revalidate: false,
}
}
#[rocket::get("/chart.js")]
fn chartjs() -> CacheResponse<RawJavaScript<&'static [u8]>> {
CacheResponse::Public {
responder: RawJavaScript(include_bytes!(concat!(env!("OUT_DIR"), "/chart.js"))),
max_age: 604800,
must_revalidate: false,
}
}
#[rocket::get("/chartjs-plugin-datalabels.js")]
fn chartjs_datalabels() -> CacheResponse<RawJavaScript<&'static [u8]>> {
CacheResponse::Public {
responder: RawJavaScript(include_bytes!(concat!(env!("OUT_DIR"), "/chartjs-plugin-datalabels.js"))),
max_age: 604800,
must_revalidate: false,
}
#[get("/")]
fn frontend_index() -> Option<RawHtml<&'static str>> {
frontend::DIST
.get_file("index.html")
.and_then(frontend::File::contents_utf8)
.map(RawHtml)
}
fn format_number(number: usize) -> String {
number
.to_string()
.as_bytes()
.rchunks(3)
.rev()
.map(std::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.join(" ")
#[get("/<path..>", rank = 20)]
fn frontend_asset(path: PathBuf) -> Option<(ContentType, &'static [u8])> {
if is_backend_path(&path) {
return None;
}
if let Some(file) = embedded_file(&path) {
return Some((content_type(&path), file.contents()));
}
if path.extension().is_none() {
let index = frontend::DIST.get_file("index.html")?;
return Some((ContentType::HTML, index.contents()));
}
None
}
fn embedded_file(path: &PathBuf) -> Option<&'static frontend::File<'static>> {
let path = path.to_string_lossy().replace('\\', "/");
frontend::DIST.get_file(path)
}
fn is_backend_path(path: &PathBuf) -> bool {
matches!(
path.components()
.next()
.and_then(|component| component.as_os_str().to_str()),
Some("api" | "agency" | "whitelist")
)
}
fn content_type(path: &PathBuf) -> ContentType {
path.extension()
.and_then(|extension| extension.to_str())
.and_then(ContentType::from_extension)
.unwrap_or(ContentType::Binary)
}
async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
@@ -293,8 +122,8 @@ async fn rocket() -> _ {
info!("Downloaded, updating...");
checker_clone.read().await.update_all(bases).await;
info!("Updated databases");
},
Err(e) => log::error!("Failed to download all DBs"),
}
Err(_) => log::error!("Failed to download all DBs"),
}
}
});
@@ -306,14 +135,18 @@ async fn rocket() -> _ {
let api_limiter = std::sync::Arc::new(api::build_rate_limiter(rate_limit_rpm));
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(std::env::var("DATABASE_MAX_CONNECTIONS")
.unwrap_or("100".to_string())
.parse()
.unwrap())
.min_connections(std::env::var("DATABASE_MIN_CONNECTIONS")
.unwrap_or("10".to_string())
.parse()
.unwrap())
.max_connections(
std::env::var("DATABASE_MAX_CONNECTIONS")
.unwrap_or("100".to_string())
.parse()
.unwrap(),
)
.min_connections(
std::env::var("DATABASE_MIN_CONNECTIONS")
.unwrap_or("10".to_string())
.parse()
.unwrap(),
)
.acquire_timeout(Duration::from_secs(5))
.idle_timeout(Duration::from_secs(60))
.connect(&dotenvy::var("DATABASE_URL").expect("DATABASE_URL must be set"))
@@ -325,14 +158,19 @@ async fn rocket() -> _ {
.manage(pool)
.manage(api_limiter)
.attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations))
.mount("/", routes![index, check, healthcheck, page, feedback])
.mount("/vendor", routes![lucide, chartjs, chartjs_datalabels])
.mount("/api/v1", routes![api::check])
.register("/api", catchers![api_error])
.mount("/", routes![api::feedback]) // DEPRECATED: backwards compatibility
.mount(
"/api/v1",
routes![
api::check,
api::healthcheck,
api::feedback,
api::get_system_status,
whitelist::histogram
],
)
.mount("/agency", routes![agency::upload_report])
.mount("/whitelist", routes![whitelist::histogram, whitelist::export_csv])
.register("/agency", catchers![api_error])
.register("/", catchers![default])
.mount("/", FileServer::from(PathBuf::from("static")))
.attach(Template::fairing())
.mount("/whitelist", routes![whitelist::export_csv])
.mount("/", routes![frontend_index, frontend_asset])
.register("/", catchers![api_error])
}
+22 -9
View File
@@ -1,12 +1,12 @@
use crate::db::{WhitelistHistogramBin, collect_histogram};
use rocket::State;
use rocket::futures::StreamExt;
use rocket::http::{ContentType, Status};
use rocket::request::FromParam;
use rocket_cache_response::CacheResponse;
use rocket::State;
use std::io;
use rocket::serde::json::Json;
use crate::db::{collect_histogram, WhitelistHistogramBin};
use rocket_cache_response::CacheResponse;
use sqlx::postgres::PgPool;
use std::io;
enum ExportType {
Full,
@@ -39,7 +39,10 @@ pub async fn export_csv(
}
};
let mut db = pool.acquire().await.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let mut db = pool
.acquire()
.await
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let mut stream = db
.copy_out_raw(query)
@@ -61,9 +64,19 @@ pub async fn export_csv(
}
#[get("/histogram?<filter>&<limit>")]
pub async fn histogram(pool: &State<PgPool>, filter: Option<bool>, limit: Option<i32>) -> Result<Json<Vec<WhitelistHistogramBin>>, Status> {
pub async fn histogram(
pool: &State<PgPool>,
filter: Option<bool>,
limit: Option<i32>,
) -> Result<Json<Vec<WhitelistHistogramBin>>, Status> {
let limit = limit.unwrap_or(100_000).clamp(0, 1_000_000);
let mut db = pool.acquire().await.map_err(|_| Status::InternalServerError)?;
Ok(Json(collect_histogram(&mut *db, 50, limit, filter.is_some()).await
.map_err(|_| Status::InternalServerError)?))
let mut db = pool
.acquire()
.await
.map_err(|_| Status::InternalServerError)?;
Ok(Json(
collect_histogram(&mut *db, 50, limit, filter.is_some())
.await
.map_err(|_| Status::InternalServerError)?,
))
}
-68
View File
@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
{% block head %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/main.css"/>
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
{% block metadata %}
<title>Cheburcheck</title>
<meta name="description" content="Проверка доменов и IP-адресов на наличие в реестре блокировок Роскомнадзора и заблокированных CDN.">
<meta name="keywords" content="проверка домена, заблокирован ли сайт, Роскомнадзор, чебурнет, cdn, блокировки">
<meta property="og:title" content="Cheburcheck">
<meta property="og:description" content="Проверка доменов и IP-адресов на наличие в реестре блокировок Роскомнадзора и заблокированных CDN.">
<meta property="og:url" content="https://cheburcheck.ru">
<meta property="og:image" content="https://cheburcheck.ru/og-cover.png">
{% endblock metadata %}
<meta property="og:site_name" content="Cheburcheck">
<meta property="og:type" content="website">
<meta name="author" content="Cheburcheck">
{% endblock head %}
</head>
<body>
<header class="header">
{% block header %}
<div class="flex justify-between items-center">
<a class="logo" href="/">
<i data-lucide="cloud-alert" width="32" height="32"></i>
<span class="text-lg font-bold uppercase">Cheburcheck</span>
</a>
<div class="flex gap-4 text-xs text-muted">
<a href="/kb/faq" class="text-muted">FAQ</a>
<a class="version-link text-muted" href="https://github.com/LowderPlay/cheburcheck">
<span>v{{ global.version }}</span>
<svg width="20" height="20" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
</a>
</div>
</div>
{% endblock header %}
</header>
<main class="container">
{% block content %}{% endblock content %}
</main>
<footer class="footer">
{% block footer %}
<p class="text-xs uppercase">
<a href="mailto:support@cheburcheck.ru">support@cheburcheck.ru</a>
-
Сделано в России
<i data-lucide="copyright" width="10" height="10"></i> {{ now() | date(format="%Y") }}
</p>
{% endblock footer %}
</footer>
<script src="/vendor/lucide.js"></script>
<script>
lucide.createIcons();
</script>
</body>
</html>
-82
View File
@@ -1,82 +0,0 @@
{% extends 'base' %}
{% block content %}
{% include 'search-form' %}
<div class="result-panel">
<div class="result-header">
<div class="icon-box status-icon">
<i data-lucide="shield-question-mark" width="32" height="32"></i>
</div>
<div>
<h2>Не найдено</h2>
<p class="subheading text-sm">Возможно вы неправильно ввели запрос?</p>
</div>
</div>
<div class="target-info">
<div class="target-info-label">{{ target_type }}:</div>
<div class="target-value target-display">{{ target }}</div>
</div>
{# <div class="details-grid">#}
{# <div class="detail-section">#}
{# <h3 class="section-title">Сетевые данные</h3>#}
{# <div class="detail-row">#}
{# <span class="row-label">IP-адреса</span>#}
{# <div>#}
{# {% for ip in ips %}#}
{# <p class="row-value">{{ ip }}</p>#}
{# {% endfor %}#}
{# </div>#}
{# </div>#}
{# <div class="detail-row">#}
{# <span class="row-label">Хостинг / ISP</span>#}
{# <span class="row-value">{{ geo.organisation }}</span>#}
{# </div>#}
{# <div class="detail-row">#}
{# <span class="row-label">Location</span>#}
{# <span class="row-value">{{ geo.location }}</span>#}
{# </div>#}
{# <div class="detail-row">#}
{# <span class="row-label">ASN</span>#}
{# <span class="row-value">{{ geo.asn }}</span>#}
{# </div>#}
{# </div>#}
{# <div class="detail-section">#}
{# <h3 class="reason-header">Нахождение в списках</h3>#}
{# <div class="detail-row">#}
{# <span class="row-label">CDN</span>#}
{# {% if found %}#}
{# <div>#}
{# {% for net in subnets %}#}
{# <p class="row-value">{{ net }}</p>#}
{# {% endfor %}#}
{# </div>#}
{# {% else %}#}
{# <span class="row-value">Не найден</span>#}
{# {% endif %}#}
{# </div>#}
{# <div class="detail-row">#}
{# <span class="row-label">Статус РКН</span>#}
{# <span class="row-value alert">ОГРАНИЧЕН</span>#}
{# </div>#}
{# <div class="detail-row">#}
{# <span class="row-label">Орган</span>#}
{# <span class="row-value">Генпрокуратура</span>#}
{# </div>#}
{# <div class="detail-row">#}
{# <span class="row-label">Номер решения</span>#}
{# <span class="row-value">27-31-2024/Ид4124</span>#}
{# </div>#}
{# <div class="detail-row">#}
{# <span class="row-label">Дата внесения</span>#}
{# <span class="row-value">12.11.2024</span>#}
{# </div>#}
{# </div>#}
{# </div>#}
</div>
{% endblock content %}
-27
View File
@@ -1,27 +0,0 @@
{% extends 'base' %}
{% block content %}
<i class="error-code" data-lucide="triangle-alert" width="196" height="196"></i>
<h1 class="error-code">{{ status }} {{ reason }}</h1>
<p class="text-muted text-lg">
{% if status == 500 %}
Внутренняя ошибка сервера. Пожалуйста, попробуйте позже.
{% elif status == 502 %}
Сервер временно недоступен. Попробуйте обновить страницу.
{% elif status == 503 %}
Сервис временно недоступен. Ведутся технические работы.
{% elif status == 504 %}
Превышено время ожидания ответа от сервера. Попробуйте позже.
{% elif status == 400 %}
Неверный запрос. Проверьте правильность введенных данных.
{% elif status == 404 %}
Страница не найдена. Проверьте правильность адреса.
{% elif status == 422 %}
Невозможно обработать запрос. Проверьте формат данных.
{% elif status == 429 %}
Слишком много запросов. Пожалуйста, подождите немного.
{% else %}
Что-то пошло не так. Возможно в запросе есть ошибка?
{% endif %}
</p>
{% endblock content %}
-137
View File
@@ -1,137 +0,0 @@
{% macro head() %}
<script src="/vendor/chart.js"></script>
<script src="/vendor/chartjs-plugin-datalabels.js"></script>
{% endmacro %}
{% macro histogram(id="histogram",
endpoint="/whitelist/histogram",
data_label="Количество ресурсов",
x_label="Ранг домена",
y_label="Количество ресурсов") %}
<canvas id="{{ id }}"></canvas>
<style>
#{{ id }} {
max-height: 480px;
}
</style>
<script>
(async function () {
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
const TEXT_LIGHT = '#d4d4d4';
const RED_ACCENT = '#ef4444';
const AXIS_COLOR = '#404040';
const GRID_COLOR = 'rgba(64, 64, 64, 0.2)';
const ctx = document.getElementById('{{ id }}').getContext('2d');
const response = await fetch("{{ endpoint | safe }}");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const labels = data.map(item =>
`${formatNumber(Math.round(item.bin_min_rank))}-${formatNumber(Math.round(item.bin_max_rank))}`
);
const counts = data.map(item => item.count);
Chart.register(ChartDataLabels);
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '{{ data_label }}',
data: counts,
backgroundColor: RED_ACCENT,
borderColor: RED_ACCENT,
borderWidth: 1,
borderRadius: 6,
hoverBackgroundColor: `${RED_ACCENT}cc`,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
datalabels: {
anchor: 'end',
align: 'top',
offset: 8,
color: TEXT_LIGHT,
font: {
weight: 'bold',
size: 10
},
formatter: (value) => value > 0 ? formatNumber(value) : '',
},
legend: {
display: false,
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += formatNumber(context.parsed.y);
}
return label;
}
},
backgroundColor: 'rgba(30, 30, 30, 0.9)',
titleColor: TEXT_LIGHT,
bodyColor: TEXT_LIGHT,
}
},
scales: {
x: {
title: {
display: true,
text: '{{ x_label }}',
color: TEXT_LIGHT
},
ticks: {
color: TEXT_LIGHT,
maxRotation: 45,
minRotation: 45
},
grid: {
color: GRID_COLOR,
drawBorder: false
},
border: {
color: AXIS_COLOR
}
},
y: {
type: 'linear',
title: {
display: true,
text: '{{ y_label }}',
color: TEXT_LIGHT
},
ticks: {
color: TEXT_LIGHT,
callback: () => ''
},
grid: {
color: GRID_COLOR,
drawBorder: false
},
border: {
color: AXIS_COLOR
}
}
}
}
});
})();
</script>
{% endmacro metadata %}
-34
View File
@@ -1,34 +0,0 @@
{% extends 'base' %}
{% block content %}
<div class="search-container">
<h1>Статус Ресурса</h1>
<p class="text-muted">Введите домен или IP-адрес для поиска по спискам заблокированных адресов и хостинг-провайдеров.</p>
</div>
{% include 'search-form' %}
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card-header">
<i data-lucide="globe" width="16" height="16"></i>
<span>Количество доменов</span>
</div>
<span>{{ domain_count }}</span>
</div>
<div class="stat-card">
<div class="stat-card-header">
<i data-lucide="server" width="16" height="16"></i>
<span>Количество IPv4-адресов</span>
</div>
<span>{{ v4_count }}</span>
</div>
<div class="stat-card">
<div class="stat-card-header">
<i data-lucide="activity" width="16" height="16"></i>
<span>Последнее обновление</span>
</div>
<span><script>document.write(new Date("{{ last_update }}").toLocaleString())</script></span>
</div>
</div>
{% endblock content %}
-18
View File
@@ -1,18 +0,0 @@
{% extends 'base' %}
{% import 'typography' as typography %}
{% macro metadata(title, description, image="https://cheburcheck.ru/og-cover.png", keywords, url="/") %}
<title>{{ title }} - Cheburcheck</title>
<meta name="description" content="{{ description }}">
<meta name="keywords" content="проверка домена, заблокирован ли сайт, Роскомнадзор, чебурнет, cdn, блокировки, {{ keywords }}">
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ description }}">
<meta property="og:url" content="https://cheburcheck.ru{{ url }}">
<meta property="og:image" content="{{ image }}">
{% endmacro metadata %}
{% block content %}
<div class="page-container">
{% block page_text %}{% endblock %}
</div>
{% endblock content %}
-92
View File
@@ -1,92 +0,0 @@
{% extends 'page' %}
{% import 'page' as page %}
{% block metadata %}
{{ page::metadata(title="FAQ", description="Ответы на частые вопросы", keywords="faq", url="/kb/faq") }}
{% endblock metadata %}
{% block page_text %}
<h1>Ответы на частые вопросы</h1>
<p>
На этой странице собраны ответы на часто задаваемые вопросы.
Если вас интересует что-то ещё &mdash; пишите нам на почту <a href="mailto:support@cheburcheck.ru">support@cheburcheck.ru</a>.
</p>
{{ typography::heading(title="Что это за сайт?") }}
<p>
Этот сайт &mdash; инструмент, который позволяет проверить, находится ли сайт, домен,
IP-адрес в <a href="#{{ "Откуда вы берете эти списки?" | slugify }}">списках</a>, блокируемых Роскомнадзором на территории России.
</p>
{{ typography::heading(title="Откуда вы берете эти списки?") }}
<p>
Для поиска сайтов мы используем списки IP-подсетей популярных CDN-провайдеров,
которые могут блокироваться Роскомнадзором, а также домены из самого реестра Роскомнадзора.
</p>
<p>
Подсети, которые мы используем, публикуют сами провайдеры, либо собираются из принадлежащих им автономных сетей.
Мы используем <a href="https://github.com/123jjck/cdn-ip-ranges">этот репозиторий</a>, в котором автоматически
агрегируются эти подсети.
</p>
<p>
Данные из Реестра Роскомнадзора подгружаются из сервисов <a href="https://antifilter.download/">antifilter.download</a>
и <a href="https://antifilter.network/">antifilter.network</a>.
</p>
{{ typography::heading(title="Что такое «белый список»?") }}
<p>
РКН формирует свой белый список доменов, которые доступны,
даже если их адреса находятся в списке заблокированных подсетей.
</p>
<a href="/kb/whitelist">Подробнее...</a>
{{ typography::heading(title='Мне написало, что сайт «Заблокирован», но у меня все работает!') }}
<p>
Мы не можем с точностью утверждать, заблокирован сайт или нет.
Блокировки Роскомнадзора, зачастую, непредсказуемые, непубличные и незаконные.
Например, блокировка CDN-сервисов, таких, как
<a href="https://blog.cloudflare.com/russian-internet-users-are-unable-to-access-the-open-internet/">Cloudflare</a>,
никак не комментировалась официально и не публиковались в Реестре. Аналогичная ситуация с
<a href="https://ria.ru/20241219/putin-1990168675.html">YouTube</a>, информация о
блокировке которого, не добавлена в Реестр &mdash; для таких случаев мы создали
<a href="https://github.com/LowderPlay/cheburcheck/blob/master/querying/dist-domains.txt">свой список</a> подобных сайтов.
Однако, список не исчерпывающий, поэтому
<a href="https://github.com/LowderPlay/cheburcheck/pulls">принимаются правки</a>.
</p>
<p>
Сайты помечаются как "заблокированные", в случае, если они были найдены хотя бы в одном списке.
</p>
{{ typography::heading(title='Как часто обновляются списки?') }}
<p>
Мы обновляем списки из источников раз в 6 часов.
</p>
<p>
Сами источники обновляют их с разной периодичностью:
</p>
<ul>
<li><a href="https://github.com/123jjck/cdn-ip-ranges">CDN</a> &mdash; раз в 12 часов.</li>
<li><a href="/kb/whitelist">Белые списки</a> &mdash; топ-100,000 раз в 6 часов; топ-1,000,000 раз в сутки.</li>
<li><a href="https://antifilter.download/">Домены из Реестра РКН</a> &mdash; раз в полчаса.</li>
<li><a href="https://antifilter.network/">Подсети из Реестра РКН</a> &mdash; раз в час.</li>
</ul>
{{ typography::heading(title='Как я могу помочь в развитии сайта?') }}
<p>
Самый простой способ помочь &mdash; рассказать о сайте знакомым.
</p>
<p>
Если у вас есть навыки разработки, то мы будем рады принять ваши pull-requests
на <a href="https://github.com/LowderPlay/cheburcheck">GitHub</a>.
</p>
<p>
Если вы хотите помочь финансово:
</p>
<ul>
<li>TON: <code>UQAACsiwpGryjP-kqp4TJPAWpXytuB6M_puuO0Cg5zNvaSJW</code></li>
</ul>
<p>
Большое спасибо! ❤️
</p>
{% endblock page_text %}
-111
View File
@@ -1,111 +0,0 @@
{% extends 'page' %}
{% import 'page' as page %}
{% block metadata %}
{{ page::metadata(title="Test", description="Проверка", keywords="test", url="/kb/test") }}
{% endblock metadata %}
{% block page_text %}
<img class="fullwidth" src="https://placehold.co/600x400">
<h1>Hello, world!</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
Curabitur sed dui sit amet felis consequat cursus quis at magna.
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
Quisque egestas diam non sem pellentesque bibendum.
Fusce posuere pretium nisl sit amet pharetra.
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
nec imperdiet odio ullamcorper sed.
</p>
{{ typography::heading(title="Test1") }}
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
Curabitur sed dui sit amet felis consequat cursus quis at magna.
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
Quisque egestas diam non sem pellentesque bibendum.
Fusce posuere pretium nisl sit amet pharetra.
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
nec imperdiet odio ullamcorper sed.
</p>
<img src="https://placehold.co/600x400">
<i class="caption">Centered image</i>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
Curabitur sed dui sit amet felis consequat cursus quis at magna.
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
Quisque egestas diam non sem pellentesque bibendum.
Fusce posuere pretium nisl sit amet pharetra.
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
nec imperdiet odio ullamcorper sed.
</p>
{{ typography::heading(title="Test2") }}
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
Curabitur sed dui sit amet felis consequat cursus quis at magna.
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
Quisque egestas diam non sem pellentesque bibendum.
Fusce posuere pretium nisl sit amet pharetra.
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
nec imperdiet odio ullamcorper sed.
</p>
{{ typography::heading(title="Test3") }}
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
Curabitur sed dui sit amet felis consequat cursus quis at magna.
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
Quisque egestas diam non sem pellentesque bibendum.
Fusce posuere pretium nisl sit amet pharetra.
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
nec imperdiet odio ullamcorper sed.
</p>
{{ typography::heading(title="Test4") }}
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
Curabitur sed dui sit amet felis consequat cursus quis at magna.
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
Quisque egestas diam non sem pellentesque bibendum.
Fusce posuere pretium nisl sit amet pharetra.
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
nec imperdiet odio ullamcorper sed.
</p>
{% endblock page_text %}
@@ -1,99 +0,0 @@
{% extends 'page' %}
{% import 'page' as page %}
{% import 'histogram' as histogram %}
{% block head %}
{{ super() }}
{{ histogram::head() }}
{% endblock head %}
{% block metadata %}
{{ page::metadata(title="Белые списки", description="Анализ данных из белых списков блокировок", keywords="белые списки, whitelist", url="/kb/whitelist") }}
{% endblock metadata %}
{% block page_text %}
<h1>Белые списки доменов</h1>
<p class="warning-note">
<i data-lucide="triangle-alert" width="32" height="32"></i>
<span>
Не путать с <a href="https://habr.com/ru/news/1000784/">белыми списками <i>мобильного</i> интернета</a>!
</span>
</p>
<p>
Российские операторы связи начали применять новый тип блокировок CDN,
при котором загрузка контента обрывается после передачи примерно 16–20 килобайт данных,
из‑за чего большинство сайтов на Cloudflare и других зарубежных платформах становятся
практически неработоспособными для пользователей в России.
Одновременно сохраняются и развиваются белые списки с популярными и «социально значимыми» доменами,
которые старательно выводятся из‑под подобных ограничений.
</p>
<p>
Суть схемы в том, что соединение технически устанавливается, первые килобайты HTML,
CSS или скриптов передаются, но затем трафик режется или соединение принудительно сбрасывается на уровне
российских операторов, обычно после примерно 16 килобайт (10–14 пакетов в зависимости от протокола).
Этого объёма достаточно, чтобы страница начала загружаться и создавалась иллюзия доступности,
но ключевые части сайта (основной HTML, JS‑бандлы, стили, API‑запросы) не проходят, и сервис фактически
перестаёт работать.
</p>
<pre class="code">$ curl -k https://cheburcheck.ru/100MB.bin -o/dev/null -r 0-65536 --resolve cheburcheck.ru:443:5.78.7.195 --max-time 5
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
24 65537 24 16101 0 0 3220 0 0:00:20 0:00:04 0:00:16 0
curl: (28) Operation timed out after 5000 milliseconds with 16101 out of 65537 bytes received</pre>
<p>
Определение домена и выбор, «разрешить или задушить» запрос, в текущей схеме делается на связке DPI‑фильтров.
По умолчанию, все HTTP(s) запросы на адреса из подсетей хостеров блокируются. Однако, запросы
к некоторым доменам из тех же подсетей проходят нормально &mdash; они обнаруживаются по открытому расширению
TLS SNI, либо по заголовку Host (в случае с plain http).
</p>
<p>
Проверить нахождение домена в белом списке можно с помощью простого HTTPS-запроса с подменой SNI на адрес из
заблокированных диапазонов:
</p>
<pre class="code">$ curl -k https://ok.ru/100MB.bin -o/dev/null -r 0-65536 --resolve ok.ru:443:5.78.7.195
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 65537 100 65537 0 0 55226 0 0:00:01 0:00:01 --:--:-- 55258</pre>
<p>
Также, стоит отметить, что домены белого списка добавляются с маской по поддоменам:
</p>
<pre class="code">$ curl -k https://cheburcheck.ok.ru/100MB.bin -o/dev/null -r 0-65536 --resolve cheburcheck.ok.ru:443:
5.78.7.195
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 65537 100 65537 0 0 53192 0 0:00:01 0:00:01 --:--:-- 53195</pre>
{{ typography::heading(title="Автоматическое сканирование") }}
<p>
Используя методы, указанные выше, мы разработали инструмент для автоматического сканирования и анализа
блокировок
<b><a href="https://github.com/LowderPlay/cheburcheck/tree/master/reporter">Cheburcheck Reporter</a></b>.
Он позволяет сканировать большое количество доменов
за короткий срок за счет параллелизации запросов.
</p>
<p>
Исходя из анализа 1,000,000 доменов из рейтинга
<a href="https://tranco-list.eu/list/2NPQ9">Tranco list от 26 ноября 2025</a>,
в белом списке содержится около 1000 доменов.
</p>
<i>
* &mdash; Мы не включаем в это число домены из зоны .co.uk, так как по какой-то причине, они все находятся в белом списке.
</i>
<i class="caption">Гистограмма количества доменов относительно их положения в рейтинге (топ-30k, без .co.uk)</i>
{{ histogram::histogram(endpoint="/whitelist/histogram?filter&limit=30000") }}
<br>
<i class="caption">Гистограмма количества доменов относительно их положения в рейтинге (топ-1kk, включая .co.uk)</i>
{{ histogram::histogram(id="filtered", endpoint="/whitelist/histogram?limit=1000000") }}
{{ typography::heading(title="Скачать списки") }}
<p>
Мы публикуем результаты наших сканирований в виде CSV-файлов:
</p>
<div>
<i data-lucide="file-symlink" width="16" height="16"></i> <a href="/whitelist/full.csv">Полный список (CSV)</a>
</div>
<div>
<i data-lucide="file-symlink" width="16" height="16"></i> <a href="/whitelist/domains.csv">Только домены (CSV)</a>
</div>
{% endblock page_text %}
-315
View File
@@ -1,315 +0,0 @@
{% extends 'base' %}
{% block content %}
{% include 'search-form' %}
{% set warning = whitelist and not domain %}
<div class="result-panel {% if warning %}whitelist-theme{% elif found %}blocked-theme{% else %}clean-theme{% endif %}">
<div class="result-header">
<div class="icon-box status-icon">
{% if whitelist %}
<i data-lucide="shield-alert" width="32" height="32"></i>
{% elif blocked %}
<i data-lucide="shield-x" width="32" height="32"></i>
{% else %}
<i data-lucide="shield-check" width="32" height="32"></i>
{% endif %}
</div>
{% if warning %}
<div>
<h2>Белый список</h2>
<p class="subheading text-sm">
Ресурс находится в белом списке
</p>
</div>
{% elif found %}
<div>
<h2>Заблокирован</h2>
<p class="subheading text-sm">Ресурс был найден в списках блокировок</p>
</div>
{% else %}
<div>
<h2>Доступен</h2>
<p class="subheading text-sm">Ограничений не обнаружено</p>
</div>
{% endif %}
</div>
<div class="target-info">
<div class="target-info-label">{{ target_type }}:</div>
<div class="target-value target-display">{{ target }}</div>
</div>
{% if asn_info %}
<div class="asn-summary">
<div class="asn-stat">
<span class="asn-stat-label">Всего подсетей:</span>
<span class="asn-stat-value">{{ asn_info.prefixes | length }}</span>
</div>
{% if asn_info.blocked_prefixes | length > 0 %}
<div class="asn-stat">
<span class="asn-stat-label">Заблокировано:</span>
<span class="asn-stat-value alert">{{ asn_info.blocked_prefixes | length }}</span>
</div>
{% endif %}
</div>
{% endif %}
<div class="details-grid">
<div class="detail-section">
<h3 class="section-title">Сетевые данные</h3>
{% if not asn_info %}
<div class="detail-row">
<span class="row-label">IP-адреса</span>
<div class="ip-list-container">
{% if ips | length == 1 %}
<span class="ip-item">{{ ips | first }}</span>
{% if subnet_size %}
<span class="ip-count">({{ subnet_size }} всего)</span>
{% endif %}
{% elif ips | length <= 5 %}
{% for ip in ips %}
<span class="ip-item">{{ ip }}</span>
{% endfor %}
{% if subnet_size %}
<span class="ip-count">({{ subnet_size }} всего)</span>
{% endif %}
{% else %}
<div class="ip-summary">
<span class="ip-range">{{ ips | first }} — {{ ips | last }}</span>
{% if subnet_size %}
<span class="ip-count">({{ subnet_size }} всего, проверено {{ ips | length }})</span>
{% else %}
<span class="ip-count">({{ ips | length }} адресов)</span>
{% endif %}
</div>
<details class="ip-list-details">
<summary class="ip-list-toggle">
Показать все
</summary>
<div class="ip-list-expanded">
{% for ip in ips %}
<span class="ip-item">{{ ip }}</span>
{% endfor %}
</div>
</details>
{% endif %}
</div>
</div>
{% endif %}
<div class="detail-row">
<span class="row-label">Хостинг / ISP</span>
<span class="row-value">{% if geo.organisation %}{{ geo.organisation }}{% else %}-{% endif %}</span>
</div>
<div class="detail-row">
<span class="row-label">Локация</span>
<span class="row-value">{{ geo.location }}</span>
</div>
<div class="detail-row">
<span class="row-label">ASN</span>
<span class="row-value">
{% if geo.asn %}
<a href="/check?target={{ geo.asn }}" class="asn-link">{{ geo.asn }}</a>
{% else %}
-
{% endif %}
</span>
</div>
{% if asn_info %}
<div class="detail-row">
<span class="row-label">Подсети ASN</span>
<div class="prefix-list-container">
{% set ipv4_list = [] %}
{% set ipv6_list = [] %}
{% for prefix in asn_info.prefixes %}
{% if ":" in prefix %}
{% set_global ipv6_list = ipv6_list | concat(with=prefix) %}
{% else %}
{% set_global ipv4_list = ipv4_list | concat(with=prefix) %}
{% endif %}
{% endfor %}
{% set all_prefixes = ipv4_list | concat(with=ipv6_list) %}
{% if all_prefixes | length <= 10 %}
<div>
{% for prefix in all_prefixes %}
<p class="row-value">{{ prefix }}</p>
{% endfor %}
</div>
{% else %}
<div>
{% for prefix in all_prefixes | slice(end=10) %}
<p class="row-value">{{ prefix }}</p>
{% endfor %}
</div>
<details class="prefix-list-details">
<summary class="prefix-list-toggle">
Показать все ({{ all_prefixes | length }})
</summary>
<div class="prefix-list-expanded">
{% for prefix in all_prefixes %}
<p class="row-value">{{ prefix }}</p>
{% endfor %}
</div>
</details>
{% endif %}
</div>
</div>
{% endif %}
</div>
<div class="detail-section">
<h3 class="reason-header">Нахождение в списках</h3>
<div class="detail-row">
<span class="row-label">CDN</span>
{% if providers %}
<p class="row-value alert">НАЙДЕН</p>
{% else %}
<span class="row-value">Не найден</span>
{% endif %}
</div>
{% if providers %}
{% for provider, networks in providers %}
<div class="detail-row">
<span class="row-label">{{ provider }}</span>
<div class="cdn-networks-container">
{% if networks | length <= 5 %}
{% for network in networks %}
<p class="row-value">{{ network.cidr }}</p>
{% endfor %}
{% else %}
<div>
{% for network in networks | slice(end=5) %}
<p class="row-value">{{ network.cidr }}</p>
{% endfor %}
</div>
<details class="cdn-networks-details">
<summary class="cdn-networks-toggle">
Показать все ({{ networks | length }})
</summary>
<div class="cdn-networks-expanded">
{% for network in networks %}
<p class="row-value">{{ network.cidr }}</p>
{% endfor %}
</div>
</details>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% if whitelist %}
<div class="detail-row">
<a href="/kb/whitelist" class="row-label">Белый список (?)</a>
<span class="row-value success">
НАЙДЕН - <span class="hint"
title="Дата последнего сканирования, когда данный домен был найден в белом списке">
<script>
document.write(new Date("{{ whitelist.last_ok }}").toLocaleDateString());
</script>
</span>
</span>
</div>
{% endif %}
<div class="detail-row">
<span class="row-label">Реестр РКН</span>
{% if domain %}
<span class="row-value alert">ОГРАНИЧЕН</span>
{% elif blocked_subnets %}
<span class="row-value alert hint"
title="Адреса пересекаются с подсетями заблокированных доменов (не гарантирует блокировку)">
IP-АДРЕСА
</span>
{% else %}
<span class="row-value">Не найден</span>
{% endif %}
</div>
{% if domain %}
<div class="detail-row">
<span class="row-label">Заблокированный домен</span>
<span class="row-value">{{ domain }}</span>
</div>
{% endif %}
{% if blocked_subnets and not asn_info %}
<div class="detail-row">
<span class="row-label">Заблокированные подсети</span>
<div>
{% for network in blocked_subnets %}
<p class="row-value">{{ network }}</p>
{% endfor %}
</div>
</div>
{% endif %}
{% if asn_info and asn_info.blocked_prefixes | length > 0 %}
<div class="detail-row">
<span class="row-label">Заблокированные подсети ASN</span>
<div class="blocked-prefix-list-container">
{% if asn_info.blocked_prefixes | length <= 10 %}
<div>
{% for prefix in asn_info.blocked_prefixes %}
<p class="row-value alert">{{ prefix }}</p>
{% endfor %}
</div>
{% else %}
<div>
{% for prefix in asn_info.blocked_prefixes | slice(end=10) %}
<p class="row-value alert">{{ prefix }}</p>
{% endfor %}
</div>
<details class="blocked-prefix-list-details">
<summary class="blocked-prefix-list-toggle">
Показать все ({{ asn_info.blocked_prefixes | length }})
</summary>
<div class="blocked-prefix-list-expanded">
{% for prefix in asn_info.blocked_prefixes %}
<p class="row-value alert">{{ prefix }}</p>
{% endfor %}
</div>
</details>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<div class="user-feedback-section">
<p class="feedback-prompt">У вас работает этот ресурс?</p>
<div class="feedback-buttons">
<button class="feedback-btn feedback-works" onclick="sendFeedback(true)">
<i data-lucide="thumbs-up" width="16" height="16"></i>
Работает
</button>
<button class="feedback-btn feedback-not-works" onclick="sendFeedback(false)">
<i data-lucide="thumbs-down" width="16" height="16"></i>
Не работает
</button>
</div>
<div class="feedback-status hidden">
<i data-lucide="thumbs-up" width="16" height="16"></i>
<span>Спасибо за ваш отзыв!</span>
</div>
</div>
</div>
<script>
function sendFeedback(works) {
document.querySelector('.feedback-buttons').classList.add('hidden');
document.querySelector('.feedback-prompt').classList.add('hidden');
document.querySelector('.feedback-status').classList.remove('hidden');
fetch(`/feedback/{{ id }}/${works}`, {
method: 'POST',
});
}
</script>
{% endblock content %}
-18
View File
@@ -1,18 +0,0 @@
<form class="search-form" action="/check">
<input
type="text"
name="target"
placeholder="example.com, 1.1.1.1, 1.0.0.0/24, AS13335"
class="search-input"
minlength="3"
required
autofocus
>
<div class="search-icon-wrapper">
<i data-lucide="search" width="20" height="20"></i>
</div>
<button type="submit" class="search-btn">
<span>Проверить</span>
<i data-lucide="chevron-right" width="16" height="16"></i>
</button>
</form>
-5
View File
@@ -1,5 +0,0 @@
{% macro heading(title) %}
<h2 id="{{ title | slugify }}">
<a href="#{{ title | slugify }}">{{ title }}</a>
</h2>
{% endmacro title %}