mirror of
https://github.com/LowderPlay/cheburcheck.git
synced 2026-10-11 08:38:13 +03:00
feat: asn support
* add asn and cidr subnet support - parse AS numbers (AS13335 format) - fetch prefixes from ripe stat api - cache asn data for 24h, check results for 1h - sample ips across subnets for checking - show asn stats and prefix lists in ui - link asn in results for quick lookup - collapsible lists for long prefix/network lists - trim whitespace from search input * refactor: improve code structure and add enhanced error messages * refactor: replace match with if let for single branch pattern * fix: don't store ASN in database * chore: bump version --------- Co-authored-by: Lowder <me@lowderplay.dev>
This commit is contained in:
co-authored by
Lowder
parent
fb3febd54e
commit
58d9e7b68e
Generated
+2
-1
@@ -2344,6 +2344,7 @@ dependencies = [
|
||||
"maxminddb",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"trie-rs",
|
||||
@@ -4225,7 +4226,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "website"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"env_logger",
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@ resolver = "3"
|
||||
|
||||
[workspace.dependencies]
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
reqwest = { version = "0.12.24", default-features = false, features = ["stream", "rustls-tls-webpki-roots"] }
|
||||
reqwest = { version = "0.12.24", default-features = false, features = ["stream", "rustls-tls-webpki-roots", "json"] }
|
||||
log = "0.4.28"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -20,3 +20,4 @@ url = "2.5.7"
|
||||
indicatif = "0.18.3"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
futures-util = "0.3.31"
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RipeStatResponse {
|
||||
pub data: RipeStatData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RipeStatData {
|
||||
pub prefixes: Vec<Prefix>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Prefix {
|
||||
pub prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AsnError {
|
||||
NotFound,
|
||||
NetworkError(String),
|
||||
ParseError(String),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for AsnError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
AsnError::NetworkError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CachedAsnData {
|
||||
pub prefixes: Vec<String>,
|
||||
pub cached_at: SystemTime,
|
||||
}
|
||||
|
||||
impl CachedAsnData {
|
||||
pub fn new(prefixes: Vec<String>) -> Self {
|
||||
Self {
|
||||
prefixes,
|
||||
cached_at: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
SystemTime::now()
|
||||
.duration_since(self.cached_at)
|
||||
.map(|d| d.as_secs() > 86400)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_asn_prefixes(asn: u32) -> Result<Vec<String>, AsnError> {
|
||||
let url = format!(
|
||||
"https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS{}",
|
||||
asn
|
||||
);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()?;
|
||||
|
||||
let response = client.get(&url).send().await?;
|
||||
|
||||
if response.status() == 404 {
|
||||
return Err(AsnError::NotFound);
|
||||
}
|
||||
|
||||
let data: RipeStatResponse = response.json().await
|
||||
.map_err(|e| AsnError::ParseError(e.to_string()))?;
|
||||
|
||||
Ok(data.data.prefixes.into_iter().map(|p| p.prefix).collect())
|
||||
}
|
||||
|
||||
pub async fn fetch_asn_prefixes_cached(
|
||||
asn: u32,
|
||||
get_cached: impl FnOnce(u32) -> Option<Vec<String>>,
|
||||
cache_result: impl FnOnce(u32, Vec<String>),
|
||||
) -> Result<Vec<String>, AsnError> {
|
||||
if let Some(cached_prefixes) = get_cached(asn) {
|
||||
return Ok(cached_prefixes);
|
||||
}
|
||||
|
||||
let prefixes = fetch_asn_prefixes(asn).await?;
|
||||
|
||||
cache_result(asn, prefixes.clone());
|
||||
|
||||
Ok(prefixes)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct AsnInfo {
|
||||
pub asn: u32,
|
||||
pub prefixes: Vec<String>,
|
||||
pub blocked_prefixes: Vec<String>,
|
||||
}
|
||||
|
||||
impl AsnInfo {
|
||||
pub fn new(asn: u32, prefixes: Vec<String>, blocked_prefixes: Vec<String>) -> Self {
|
||||
Self {
|
||||
asn,
|
||||
prefixes,
|
||||
blocked_prefixes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipv4_prefixes(&self) -> Vec<&str> {
|
||||
self.prefixes.iter()
|
||||
.filter(|p| !p.contains(':'))
|
||||
.map(|s| s.as_str())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn ipv6_prefixes(&self) -> Vec<&str> {
|
||||
self.prefixes.iter()
|
||||
.filter(|p| p.contains(':'))
|
||||
.map(|s| s.as_str())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct AsnCache {
|
||||
cache: Arc<RwLock<HashMap<u32, CachedAsnData>>>,
|
||||
}
|
||||
|
||||
impl AsnCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cache: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cached_asn(&self, asn: u32) -> Option<Vec<String>> {
|
||||
let cache = self.cache.read().ok()?;
|
||||
let cached_data = cache.get(&asn)?;
|
||||
|
||||
if cached_data.is_expired() {
|
||||
None
|
||||
} else {
|
||||
Some(cached_data.prefixes.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_asn(&self, asn: u32, prefixes: Vec<String>) {
|
||||
if let Ok(mut cache) = self.cache.write() {
|
||||
cache.insert(asn, CachedAsnData::new(prefixes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ pub struct GeoIp {
|
||||
country: Option<maxminddb::Reader<Vec<u8>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct IpInfo {
|
||||
pub asn: Option<String>,
|
||||
pub country_code: Option<String>,
|
||||
|
||||
+54
-4
@@ -14,11 +14,15 @@ use maxminddb::MaxMindDbError;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{watch, RwLock};
|
||||
|
||||
pub mod asn;
|
||||
pub mod geoip;
|
||||
pub mod lists;
|
||||
pub mod resolver;
|
||||
pub mod updater;
|
||||
pub mod target;
|
||||
pub mod subnet_sampler;
|
||||
|
||||
pub use subnet_sampler::{sample_ipv4_subnet, sample_ipv6_subnet};
|
||||
|
||||
pub struct Checker {
|
||||
rx: watch::Receiver<Option<DateTime<Utc>>>,
|
||||
@@ -29,13 +33,16 @@ pub struct Checker {
|
||||
resolver: Resolver,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Check {
|
||||
pub verdict: CheckVerdict,
|
||||
pub geo: IpInfo,
|
||||
pub ips: Vec<IpAddr>,
|
||||
pub rkn_subnets: HashSet<IpNet>,
|
||||
pub asn_info: Option<crate::asn::AsnInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CheckVerdict {
|
||||
Clear,
|
||||
Blocked {
|
||||
@@ -117,17 +124,60 @@ impl Checker {
|
||||
.filter_map(|ip| ru_blacklist.contains_ip(ip))
|
||||
.collect();
|
||||
|
||||
let asn_info = if let Target::Asn(asn) = &target {
|
||||
let prefixes = crate::asn::fetch_asn_prefixes_cached(
|
||||
*asn,
|
||||
|asn| self.resolver.asn_cache.get_cached_asn(asn),
|
||||
|asn, prefixes| self.resolver.asn_cache.cache_asn(asn, prefixes),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut blocked_prefixes: Vec<String> = prefixes
|
||||
.iter()
|
||||
.filter(|prefix| {
|
||||
if let Ok(ipnet) = prefix.parse::<IpNet>() {
|
||||
ru_blacklist.contains_ip(&ipnet.network()).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for prefix in &prefixes {
|
||||
if let Ok(ipnet) = prefix.parse::<IpNet>() {
|
||||
if cdn_list.contains(&ipnet.network()).is_some() {
|
||||
if !blocked_prefixes.contains(prefix) {
|
||||
blocked_prefixes.push(prefix.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(crate::asn::AsnInfo::new(*asn, prefixes, blocked_prefixes))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let asn_has_blocked = asn_info.as_ref()
|
||||
.map(|info| !info.blocked_prefixes.is_empty())
|
||||
.unwrap_or(false);
|
||||
|
||||
let has_blocked_subnets = !rkn_subnets.is_empty();
|
||||
|
||||
Ok(Check {
|
||||
verdict: match (domain, cdn_provider_subnets.is_empty()) {
|
||||
(None, true) => CheckVerdict::Clear,
|
||||
(domain, _) => CheckVerdict::Blocked {
|
||||
verdict: match (domain, cdn_provider_subnets.is_empty(), asn_has_blocked, has_blocked_subnets) {
|
||||
(None, true, false, false) => CheckVerdict::Clear,
|
||||
(domain, _, _, _) => CheckVerdict::Blocked {
|
||||
rkn_domain: domain,
|
||||
cdn_provider_subnets,
|
||||
},
|
||||
},
|
||||
rkn_subnets,
|
||||
geo,
|
||||
ips
|
||||
ips,
|
||||
asn_info,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,14 @@ use hickory_resolver::config::{LookupIpStrategy, ResolverConfig, ResolverOpts};
|
||||
use hickory_resolver::name_server::TokioConnectionProvider;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::asn::{AsnCache, AsnError};
|
||||
|
||||
pub struct Resolver {
|
||||
resolver: hickory_resolver::Resolver<TokioConnectionProvider>,
|
||||
pub asn_cache: Arc<AsnCache>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -14,6 +18,24 @@ pub enum ResolveError {
|
||||
NxDomain,
|
||||
#[error("resolver error")]
|
||||
Other(#[from] Error),
|
||||
#[error("not implemented")]
|
||||
NotImplemented,
|
||||
#[error("asn not found")]
|
||||
AsnNotFound,
|
||||
#[error("asn network error: {0}")]
|
||||
AsnNetworkError(String),
|
||||
#[error("asn parse error: {0}")]
|
||||
AsnParseError(String),
|
||||
}
|
||||
|
||||
impl From<AsnError> for ResolveError {
|
||||
fn from(err: AsnError) -> Self {
|
||||
match err {
|
||||
AsnError::NotFound => ResolveError::AsnNotFound,
|
||||
AsnError::NetworkError(msg) => ResolveError::AsnNetworkError(msg),
|
||||
AsnError::ParseError(msg) => ResolveError::AsnParseError(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
@@ -24,7 +46,10 @@ impl Resolver {
|
||||
let resolver = hickory_resolver::Resolver::builder_with_config(config, TokioConnectionProvider::default())
|
||||
.with_options(opts)
|
||||
.build();
|
||||
Resolver { resolver }
|
||||
Resolver {
|
||||
resolver,
|
||||
asn_cache: Arc::new(AsnCache::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lookup_ips(&self, domain: &str) -> Result<Vec<IpAddr>, ResolveError> {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use ipnet::{Ipv4Net, Ipv6Net};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub fn sample_ipv4_subnet(net: Ipv4Net) -> Vec<IpAddr> {
|
||||
if net.prefix_len() >= 24 {
|
||||
net.hosts().map(IpAddr::V4).collect()
|
||||
} else {
|
||||
let mut ips = Vec::with_capacity(12);
|
||||
ips.push(IpAddr::V4(net.network()));
|
||||
|
||||
let base = u32::from(net.network());
|
||||
let max = (1u64 << (32 - net.prefix_len())) - 1;
|
||||
|
||||
for i in 1..=10 {
|
||||
let offset = (max * i) / 10;
|
||||
ips.push(IpAddr::V4(Ipv4Addr::from(base + offset as u32)));
|
||||
}
|
||||
|
||||
ips.push(IpAddr::V4(net.broadcast()));
|
||||
ips
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_ipv6_subnet(net: Ipv6Net) -> Vec<IpAddr> {
|
||||
if net.prefix_len() >= 124 {
|
||||
net.hosts().map(IpAddr::V6).collect()
|
||||
} else {
|
||||
let mut ips = Vec::with_capacity(12);
|
||||
ips.push(IpAddr::V6(net.network()));
|
||||
|
||||
let base = u128::from(net.network());
|
||||
let bits = 128 - net.prefix_len();
|
||||
let max = if bits >= 64 {
|
||||
u64::MAX as u128
|
||||
} else {
|
||||
(1u128 << bits) - 1
|
||||
};
|
||||
|
||||
for i in 1..=10 {
|
||||
let offset = (max * i) / 10;
|
||||
ips.push(IpAddr::V6(Ipv6Addr::from(base + offset)));
|
||||
}
|
||||
|
||||
ips.push(IpAddr::V6(net.broadcast()));
|
||||
ips
|
||||
}
|
||||
}
|
||||
+41
-40
@@ -1,4 +1,6 @@
|
||||
use crate::asn::fetch_asn_prefixes_cached;
|
||||
use crate::resolver::{ResolveError, Resolver};
|
||||
use crate::{sample_ipv4_subnet, sample_ipv6_subnet};
|
||||
use ipnet::{Ipv4Net, Ipv6Net};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use url::Url;
|
||||
@@ -10,10 +12,20 @@ pub enum Target {
|
||||
Ipv6(Ipv6Addr),
|
||||
Ipv4Subnet(Ipv4Net),
|
||||
Ipv6Subnet(Ipv6Net),
|
||||
Asn(u32),
|
||||
}
|
||||
|
||||
|
||||
impl From<&str> for Target {
|
||||
fn from(input: &str) -> Self {
|
||||
if input.len() > 2 && input[..2].eq_ignore_ascii_case("as") {
|
||||
if let Ok(asn) = input[2..].parse::<u32>() {
|
||||
if asn >= 1 {
|
||||
return Target::Asn(asn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if input.contains('/') {
|
||||
if let Ok(ipv4_net) = input.parse::<Ipv4Net>() {
|
||||
if ipv4_net.prefix_len() >= 8 {
|
||||
@@ -54,6 +66,7 @@ impl Target {
|
||||
Target::Ipv6(_) => "IPv6-адрес",
|
||||
Target::Ipv4Subnet(_) => "IPv4-подсеть",
|
||||
Target::Ipv6Subnet(_) => "IPv6-подсеть",
|
||||
Target::Asn(_) => "Автономная система",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,48 +75,35 @@ impl Target {
|
||||
Target::Domain(domain) => resolver.lookup_ips(domain).await?,
|
||||
Target::Ipv4(ipv4) => vec![IpAddr::V4(*ipv4)],
|
||||
Target::Ipv6(ipv6) => vec![IpAddr::V6(*ipv6)],
|
||||
Target::Ipv4Subnet(net) => {
|
||||
if net.prefix_len() >= 27 {
|
||||
net.hosts().map(IpAddr::V4).collect()
|
||||
} else {
|
||||
let mut ips = Vec::with_capacity(256);
|
||||
ips.push(IpAddr::V4(net.network()));
|
||||
|
||||
let base = u32::from(net.network());
|
||||
let max = (1u64 << (32 - net.prefix_len())) - 1;
|
||||
|
||||
for i in 1..255 {
|
||||
let offset = ((max as f64 * i as f64) / 255.0) as u32;
|
||||
ips.push(IpAddr::V4(Ipv4Addr::from(base + offset)));
|
||||
}
|
||||
|
||||
ips.push(IpAddr::V4(net.broadcast()));
|
||||
ips
|
||||
Target::Ipv4Subnet(net) => sample_ipv4_subnet(*net),
|
||||
Target::Ipv6Subnet(net) => sample_ipv6_subnet(*net),
|
||||
Target::Asn(asn) => {
|
||||
let mut prefixes = fetch_asn_prefixes_cached(
|
||||
*asn,
|
||||
|asn| resolver.asn_cache.get_cached_asn(asn),
|
||||
|asn, prefixes| resolver.asn_cache.cache_asn(asn, prefixes),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if prefixes.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
},
|
||||
Target::Ipv6Subnet(net) => {
|
||||
if net.prefix_len() >= 124 {
|
||||
net.hosts().map(IpAddr::V6).collect()
|
||||
} else {
|
||||
let mut ips = Vec::with_capacity(256);
|
||||
ips.push(IpAddr::V6(net.network()));
|
||||
|
||||
let base = u128::from(net.network());
|
||||
let bits = 128 - net.prefix_len();
|
||||
let max = if bits >= 64 {
|
||||
u64::MAX as u128
|
||||
} else {
|
||||
(1u128 << bits) - 1
|
||||
};
|
||||
|
||||
for i in 1..255 {
|
||||
let offset = ((max as f64 * i as f64) / 255.0) as u128;
|
||||
ips.push(IpAddr::V6(Ipv6Addr::from(base + offset)));
|
||||
}
|
||||
|
||||
ips.push(IpAddr::V6(net.broadcast()));
|
||||
ips
|
||||
|
||||
if prefixes.len() > 100 {
|
||||
prefixes.truncate(100);
|
||||
}
|
||||
|
||||
let mut all_ips = Vec::new();
|
||||
|
||||
for prefix in &prefixes {
|
||||
if let Ok(ipv4_net) = prefix.parse::<Ipv4Net>() {
|
||||
all_ips.extend(sample_ipv4_subnet(ipv4_net));
|
||||
} else if let Ok(ipv6_net) = prefix.parse::<Ipv6Net>() {
|
||||
all_ips.extend(sample_ipv6_subnet(ipv6_net));
|
||||
}
|
||||
}
|
||||
|
||||
all_ips
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -115,6 +115,7 @@ impl Target {
|
||||
Target::Ipv6(v6) => v6.to_string(),
|
||||
Target::Ipv4Subnet(net) => net.to_string(),
|
||||
Target::Ipv6Subnet(net) => net.to_string(),
|
||||
Target::Asn(asn) => format!("AS{}", asn),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "website"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+11
-7
@@ -40,6 +40,16 @@ pub async fn save_query(
|
||||
(vec![], vec![], None)
|
||||
};
|
||||
|
||||
let (resolved_ips, cdn_networks) = match target {
|
||||
Target::Asn(_) => (vec![], vec![]),
|
||||
_ => (check
|
||||
.ips
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
cdn_networks)
|
||||
};
|
||||
|
||||
let id = sqlx::query_scalar(
|
||||
"INSERT INTO queries (
|
||||
query,
|
||||
@@ -69,13 +79,7 @@ pub async fn save_query(
|
||||
.bind(check.geo.country_code.clone())
|
||||
.bind(check.geo.asn.clone())
|
||||
.bind(check.geo.organisation.clone())
|
||||
.bind(
|
||||
check
|
||||
.ips
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.bind(resolved_ips)
|
||||
.bind(cdn_networks)
|
||||
.bind(cdn_providers)
|
||||
.bind(rkn_domain)
|
||||
|
||||
+8
-2
@@ -101,7 +101,7 @@ async fn check(
|
||||
addr: &ClientRealAddr,
|
||||
mut db: Connection<Db>,
|
||||
) -> Result<Template, Status> {
|
||||
let target = Target::from(target);
|
||||
let target = Target::from(target.trim());
|
||||
let check = checker.read().await.check(target.clone()).await;
|
||||
let id = if let Ok(check) = &check {
|
||||
match save_query(&mut db, &target, check, addr, checker.read().await).await {
|
||||
@@ -137,6 +137,7 @@ async fn check(
|
||||
geo,
|
||||
ips,
|
||||
rkn_subnets,
|
||||
asn_info,
|
||||
}) => Ok(Template::render(
|
||||
"result",
|
||||
context! {
|
||||
@@ -152,6 +153,7 @@ async fn check(
|
||||
ips,
|
||||
geo,
|
||||
subnet_size: target.subnet_size(),
|
||||
asn_info,
|
||||
},
|
||||
)),
|
||||
Ok(Check {
|
||||
@@ -163,6 +165,7 @@ async fn check(
|
||||
geo,
|
||||
rkn_subnets,
|
||||
ips,
|
||||
asn_info,
|
||||
}) => Ok(Template::render(
|
||||
"result",
|
||||
context! {
|
||||
@@ -180,6 +183,7 @@ async fn check(
|
||||
ips,
|
||||
geo,
|
||||
subnet_size: target.subnet_size(),
|
||||
asn_info,
|
||||
},
|
||||
)),
|
||||
Err(e) => {
|
||||
@@ -265,7 +269,9 @@ async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.filter_level(log::LevelFilter::Warn)
|
||||
.filter_module("website", log::LevelFilter::Info)
|
||||
.filter_module("querying", log::LevelFilter::Info)
|
||||
.init();
|
||||
|
||||
let mut interval = time::interval(Duration::from_secs(
|
||||
|
||||
+152
-2
@@ -349,6 +349,18 @@ h1.error-code {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.row-value a, .asn-link {
|
||||
color: var(--text-bright);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--text-muted);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.row-value a:hover, .asn-link:hover {
|
||||
color: white;
|
||||
text-decoration-color: var(--text-bright);
|
||||
}
|
||||
|
||||
.row-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
@@ -560,6 +572,7 @@ h1.error-code {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.ip-summary {
|
||||
@@ -593,19 +606,22 @@ h1.error-code {
|
||||
|
||||
.ip-list-details {
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.ip-list-toggle {
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
background-color: rgba(23, 23, 23, 0.5);
|
||||
width: fit-content;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.ip-list-toggle:hover {
|
||||
@@ -640,3 +656,137 @@ h1.error-code {
|
||||
}
|
||||
}
|
||||
|
||||
.asn-summary {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding: 1rem;
|
||||
background-color: rgba(23, 23, 23, 0.5);
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.asn-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.asn-stat-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.asn-stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
.asn-stat-value.alert {
|
||||
color: var(--red-color);
|
||||
}
|
||||
|
||||
.prefix-list-container,
|
||||
.blocked-prefix-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.prefix-list-details,
|
||||
.blocked-prefix-list-details {
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.prefix-list-toggle,
|
||||
.blocked-prefix-list-toggle {
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.7rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
background-color: rgba(23, 23, 23, 0.5);
|
||||
width: fit-content;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.prefix-list-toggle:hover,
|
||||
.blocked-prefix-list-toggle:hover {
|
||||
color: var(--text-bright);
|
||||
border-color: var(--input-border);
|
||||
background-color: rgba(38, 38, 38, 0.5);
|
||||
}
|
||||
|
||||
.prefix-list-toggle::-webkit-details-marker,
|
||||
.blocked-prefix-list-toggle::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cdn-networks-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.cdn-networks-details {
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.cdn-networks-toggle {
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.7rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
background-color: rgba(23, 23, 23, 0.5);
|
||||
width: fit-content;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.cdn-networks-toggle:hover {
|
||||
color: var(--text-bright);
|
||||
border-color: var(--input-border);
|
||||
background-color: rgba(38, 38, 38, 0.5);
|
||||
}
|
||||
|
||||
.cdn-networks-toggle::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.prefix-list-expanded,
|
||||
.blocked-prefix-list-expanded,
|
||||
.cdn-networks-expanded {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: rgba(23, 23, 23, 0.3);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prefix-list-container,
|
||||
.blocked-prefix-list-container {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,25 @@
|
||||
{% 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">Что-то пошло не так. Возможно в запросе есть ошибка?</p>
|
||||
<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 %}
|
||||
|
||||
@@ -42,9 +42,25 @@
|
||||
<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">
|
||||
@@ -64,7 +80,7 @@
|
||||
<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>
|
||||
<span class="ip-count">({{ subnet_size }} всего, проверено {{ ips | length }})</span>
|
||||
{% else %}
|
||||
<span class="ip-count">({{ ips | length }} адресов)</span>
|
||||
{% endif %}
|
||||
@@ -82,6 +98,7 @@
|
||||
{% 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>
|
||||
@@ -92,8 +109,56 @@
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="row-label">ASN</span>
|
||||
<span class="row-value">{% if geo.asn %}{{ geo.asn }}{% else %}-{% endif %}</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">
|
||||
@@ -110,10 +175,28 @@
|
||||
{% for provider, networks in providers %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">{{ provider }}</span>
|
||||
<div>
|
||||
{% for network in networks %}
|
||||
<p class="row-value">{{ network.cidr }}</p>
|
||||
{% endfor %}
|
||||
<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 %}
|
||||
@@ -154,7 +237,7 @@
|
||||
<span class="row-value">{{ domain }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if blocked_subnets %}
|
||||
{% if blocked_subnets and not asn_info %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Заблокированные подсети</span>
|
||||
<div>
|
||||
@@ -164,6 +247,37 @@
|
||||
</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">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="target"
|
||||
placeholder="example.com, 1.1.1.1, 1.0.0.0/24, 2606:4700::/32"
|
||||
placeholder="example.com, 1.1.1.1, 1.0.0.0/24, AS13335"
|
||||
class="search-input"
|
||||
minlength="3"
|
||||
required
|
||||
|
||||
Reference in New Issue
Block a user