diff --git a/Cargo.lock b/Cargo.lock index a8e90e0..8cb7508 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 403a09d..30bc1ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/querying/Cargo.toml b/querying/Cargo.toml index e787d03..485c348 100644 --- a/querying/Cargo.toml +++ b/querying/Cargo.toml @@ -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" diff --git a/querying/src/asn.rs b/querying/src/asn.rs new file mode 100644 index 0000000..c9bcf17 --- /dev/null +++ b/querying/src/asn.rs @@ -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, +} + +#[derive(Deserialize)] +pub struct Prefix { + pub prefix: String, +} + +#[derive(Debug)] +pub enum AsnError { + NotFound, + NetworkError(String), + ParseError(String), +} + +impl From for AsnError { + fn from(err: reqwest::Error) -> Self { + AsnError::NetworkError(err.to_string()) + } +} + +pub struct CachedAsnData { + pub prefixes: Vec, + pub cached_at: SystemTime, +} + +impl CachedAsnData { + pub fn new(prefixes: Vec) -> 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, 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>, + cache_result: impl FnOnce(u32, Vec), +) -> Result, 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, + pub blocked_prefixes: Vec, +} + +impl AsnInfo { + pub fn new(asn: u32, prefixes: Vec, blocked_prefixes: Vec) -> 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>>, +} + +impl AsnCache { + pub fn new() -> Self { + Self { + cache: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub fn get_cached_asn(&self, asn: u32) -> Option> { + 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) { + if let Ok(mut cache) = self.cache.write() { + cache.insert(asn, CachedAsnData::new(prefixes)); + } + } +} diff --git a/querying/src/geoip.rs b/querying/src/geoip.rs index 4b4c397..e29567c 100644 --- a/querying/src/geoip.rs +++ b/querying/src/geoip.rs @@ -13,7 +13,7 @@ pub struct GeoIp { country: Option>>, } -#[derive(Serialize, Debug)] +#[derive(Serialize, Debug, Clone)] pub struct IpInfo { pub asn: Option, pub country_code: Option, diff --git a/querying/src/lib.rs b/querying/src/lib.rs index 74ecba1..d9f705b 100644 --- a/querying/src/lib.rs +++ b/querying/src/lib.rs @@ -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>>, @@ -29,13 +33,16 @@ pub struct Checker { resolver: Resolver, } +#[derive(Clone)] pub struct Check { pub verdict: CheckVerdict, pub geo: IpInfo, pub ips: Vec, pub rkn_subnets: HashSet, + pub asn_info: Option, } +#[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 = prefixes + .iter() + .filter(|prefix| { + if let Ok(ipnet) = prefix.parse::() { + ru_blacklist.contains_ip(&ipnet.network()).is_some() + } else { + false + } + }) + .cloned() + .collect(); + + for prefix in &prefixes { + if let Ok(ipnet) = prefix.parse::() { + 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, }) } diff --git a/querying/src/resolver.rs b/querying/src/resolver.rs index fe3adcc..7d00542 100644 --- a/querying/src/resolver.rs +++ b/querying/src/resolver.rs @@ -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, + pub asn_cache: Arc, } #[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 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, ResolveError> { diff --git a/querying/src/subnet_sampler.rs b/querying/src/subnet_sampler.rs new file mode 100644 index 0000000..b2112cf --- /dev/null +++ b/querying/src/subnet_sampler.rs @@ -0,0 +1,47 @@ +use ipnet::{Ipv4Net, Ipv6Net}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +pub fn sample_ipv4_subnet(net: Ipv4Net) -> Vec { + 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 { + 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 + } +} diff --git a/querying/src/target.rs b/querying/src/target.rs index 030e697..0592bee 100644 --- a/querying/src/target.rs +++ b/querying/src/target.rs @@ -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::() { + if asn >= 1 { + return Target::Asn(asn); + } + } + } + if input.contains('/') { if let Ok(ipv4_net) = input.parse::() { 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::() { + all_ips.extend(sample_ipv4_subnet(ipv4_net)); + } else if let Ok(ipv6_net) = prefix.parse::() { + 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), } } diff --git a/website/Cargo.toml b/website/Cargo.toml index 7ccc308..dbcc481 100644 --- a/website/Cargo.toml +++ b/website/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "website" -version = "0.3.0" +version = "0.4.0" edition = "2024" [dependencies] diff --git a/website/src/db.rs b/website/src/db.rs index fa5699f..6a140f5 100644 --- a/website/src/db.rs +++ b/website/src/db.rs @@ -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::>(), + 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::>(), - ) + .bind(resolved_ips) .bind(cdn_networks) .bind(cdn_providers) .bind(rkn_domain) diff --git a/website/src/main.rs b/website/src/main.rs index cc582fc..20a4513 100644 --- a/website/src/main.rs +++ b/website/src/main.rs @@ -101,7 +101,7 @@ async fn check( addr: &ClientRealAddr, mut db: Connection, ) -> Result { - 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) -> 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( diff --git a/website/static/main.css b/website/static/main.css index 59e8f93..d95087b 100644 --- a/website/static/main.css +++ b/website/static/main.css @@ -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; + } +} diff --git a/website/templates/error.html.tera b/website/templates/error.html.tera index 7946984..cde3917 100644 --- a/website/templates/error.html.tera +++ b/website/templates/error.html.tera @@ -3,5 +3,25 @@ {% block content %}

{{ status }} {{ reason }}

-

Что-то пошло не так. Возможно в запросе есть ошибка?

+

+ {% 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 %} +

{% endblock content %} diff --git a/website/templates/result.html.tera b/website/templates/result.html.tera index f32d80d..0d4bd07 100644 --- a/website/templates/result.html.tera +++ b/website/templates/result.html.tera @@ -42,9 +42,25 @@
{{ target }}
+ {% if asn_info %} +
+
+ Всего подсетей: + {{ asn_info.prefixes | length }} +
+ {% if asn_info.blocked_prefixes | length > 0 %} +
+ Заблокировано: + {{ asn_info.blocked_prefixes | length }} +
+ {% endif %} +
+ {% endif %} +

Сетевые данные

+ {% if not asn_info %}
IP-адреса
@@ -64,7 +80,7 @@
{{ ips | first }} — {{ ips | last }} {% if subnet_size %} - ({{ subnet_size }} всего, показано {{ ips | length }}) + ({{ subnet_size }} всего, проверено {{ ips | length }}) {% else %} ({{ ips | length }} адресов) {% endif %} @@ -82,6 +98,7 @@ {% endif %}
+ {% endif %}
Хостинг / ISP {% if geo.organisation %}{{ geo.organisation }}{% else %}-{% endif %} @@ -92,8 +109,56 @@
ASN - {% if geo.asn %}{{ geo.asn }}{% else %}-{% endif %} + + {% if geo.asn %} + {{ geo.asn }} + {% else %} + - + {% endif %} +
+ + {% if asn_info %} +
+ Подсети ASN +
+ {% 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 %} +
+ {% for prefix in all_prefixes %} +

{{ prefix }}

+ {% endfor %} +
+ {% else %} +
+ {% for prefix in all_prefixes | slice(end=10) %} +

{{ prefix }}

+ {% endfor %} +
+
+ + Показать все ({{ all_prefixes | length }}) + +
+ {% for prefix in all_prefixes %} +

{{ prefix }}

+ {% endfor %} +
+
+ {% endif %} +
+
+ {% endif %}
@@ -110,10 +175,28 @@ {% for provider, networks in providers %}
{{ provider }} -
- {% for network in networks %} -

{{ network.cidr }}

- {% endfor %} +
+ {% if networks | length <= 5 %} + {% for network in networks %} +

{{ network.cidr }}

+ {% endfor %} + {% else %} +
+ {% for network in networks | slice(end=5) %} +

{{ network.cidr }}

+ {% endfor %} +
+
+ + Показать все ({{ networks | length }}) + +
+ {% for network in networks %} +

{{ network.cidr }}

+ {% endfor %} +
+
+ {% endif %}
{% endfor %} @@ -154,7 +237,7 @@ {{ domain }}
{% endif %} - {% if blocked_subnets %} + {% if blocked_subnets and not asn_info %}
Заблокированные подсети
@@ -164,6 +247,37 @@
{% endif %} + + {% if asn_info and asn_info.blocked_prefixes | length > 0 %} +
+ Заблокированные подсети ASN +
+ {% if asn_info.blocked_prefixes | length <= 10 %} +
+ {% for prefix in asn_info.blocked_prefixes %} +

{{ prefix }}

+ {% endfor %} +
+ {% else %} +
+ {% for prefix in asn_info.blocked_prefixes | slice(end=10) %} +

{{ prefix }}

+ {% endfor %} +
+
+ + Показать все ({{ asn_info.blocked_prefixes | length }}) + +
+ {% for prefix in asn_info.blocked_prefixes %} +

{{ prefix }}

+ {% endfor %} +
+
+ {% endif %} +
+
+ {% endif %}
diff --git a/website/templates/search-form.html.tera b/website/templates/search-form.html.tera index 2f6134a..7fd7240 100644 --- a/website/templates/search-form.html.tera +++ b/website/templates/search-form.html.tera @@ -2,7 +2,7 @@