From fb3febd54e0e2d767423a4ea914e333200f3abdb Mon Sep 17 00:00:00 2001 From: "senya xdearboy :)" <105206420+xdearboy@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:54:50 +0300 Subject: [PATCH] feat: subnets support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add cidr subnet support allows checking entire subnets using cidr notation (e.g. 1.1.1.0/24, 2001:db8::/32) - parse ipv4/ipv6 subnets from input with / delimiter - sample 256 addresses from large subnets for checking - show actual subnet size in ui (e.g. "8.2K всего, показано 256") - update search placeholder with cidr examples - limit min prefix to /8 for ipv4 and /32 for ipv6 to prevent abuse * chore: bump version --------- Co-authored-by: Lowder --- Cargo.lock | 2 +- querying/src/target.rs | 109 +++++++++++++++++++++++- website/Cargo.toml | 2 +- website/src/main.rs | 2 + website/static/main.css | 86 +++++++++++++++++++ website/templates/result.html.tera | 37 +++++++- website/templates/search-form.html.tera | 2 +- 7 files changed, 230 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3e84ea..a8e90e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4225,7 +4225,7 @@ dependencies = [ [[package]] name = "website" -version = "0.2.4" +version = "0.3.0" dependencies = [ "dotenvy", "env_logger", diff --git a/querying/src/target.rs b/querying/src/target.rs index ee77f28..030e697 100644 --- a/querying/src/target.rs +++ b/querying/src/target.rs @@ -1,4 +1,5 @@ use crate::resolver::{ResolveError, Resolver}; +use ipnet::{Ipv4Net, Ipv6Net}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use url::Url; @@ -7,10 +8,27 @@ pub enum Target { Domain(String), Ipv4(Ipv4Addr), Ipv6(Ipv6Addr), + Ipv4Subnet(Ipv4Net), + Ipv6Subnet(Ipv6Net), } impl From<&str> for Target { fn from(input: &str) -> Self { + if input.contains('/') { + if let Ok(ipv4_net) = input.parse::() { + if ipv4_net.prefix_len() >= 8 { + return Target::Ipv4Subnet(ipv4_net); + } + } + + if let Ok(ipv6_net) = input.parse::() { + if ipv6_net.prefix_len() >= 32 { + return Target::Ipv6Subnet(ipv6_net); + } + } + + } + if let Ok(ipv4) = input.parse::() { return Target::Ipv4(ipv4); } @@ -33,15 +51,60 @@ impl Target { match self { Target::Domain(_) => "Домен", Target::Ipv4(_) => "IPv4-адрес", - Target::Ipv6(_) => "IPv6-адрес" + Target::Ipv6(_) => "IPv6-адрес", + Target::Ipv4Subnet(_) => "IPv4-подсеть", + Target::Ipv6Subnet(_) => "IPv6-подсеть", } } pub async fn resolve(&self, resolver: &Resolver) -> Result, ResolveError> { Ok(match self { Target::Domain(domain) => resolver.lookup_ips(domain).await?, - Target::Ipv4(ipv4) => vec![IpAddr::V4(ipv4.clone())], - Target::Ipv6(ipv6) => vec![IpAddr::V6(ipv6.clone())], + 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::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 + } + }, }) } @@ -50,6 +113,46 @@ impl Target { Target::Domain(domain) => domain.clone(), Target::Ipv4(v4) => v4.to_string(), Target::Ipv6(v6) => v6.to_string(), + Target::Ipv4Subnet(net) => net.to_string(), + Target::Ipv6Subnet(net) => net.to_string(), + } + } + + pub fn subnet_size(&self) -> Option { + match self { + Target::Ipv4Subnet(net) => { + let size = 2u64.pow(32 - net.prefix_len() as u32); + Some(format_large_number(size as u128)) + } + Target::Ipv6Subnet(net) => { + if net.prefix_len() == 128 { + Some("1".to_string()) + } else { + let size = 2u128.pow(128 - net.prefix_len() as u32); + Some(format_large_number(size)) + } + } + _ => None, } } } + +fn format_large_number(n: u128) -> String { + if n < 1_000 { + n.to_string() + } else if n < 1_000_000 { + format!("{:.1}K", n as f64 / 1_000.0) + } else if n < 1_000_000_000 { + format!("{:.1}M", n as f64 / 1_000_000.0) + } else if n < 1_000_000_000_000 { + format!("{:.1}B", n as f64 / 1_000_000_000.0) + } else if n < 1_000_000_000_000_000 { + format!("{:.1}T", n as f64 / 1_000_000_000_000.0) + } else if n < 1_000_000_000_000_000_000 { + format!("{:.1}P", n as f64 / 1_000_000_000_000_000.0) + } else if n < 1_000_000_000_000_000_000_000 { + format!("{:.1}E", n as f64 / 1_000_000_000_000_000_000.0) + } else { + format!("{:.1}Z", n as f64 / 1_000_000_000_000_000_000_000.0) + } +} diff --git a/website/Cargo.toml b/website/Cargo.toml index 74b3c15..7ccc308 100644 --- a/website/Cargo.toml +++ b/website/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "website" -version = "0.2.4" +version = "0.3.0" edition = "2024" [dependencies] diff --git a/website/src/main.rs b/website/src/main.rs index c6ddb53..cc582fc 100644 --- a/website/src/main.rs +++ b/website/src/main.rs @@ -151,6 +151,7 @@ async fn check( whitelist, ips, geo, + subnet_size: target.subnet_size(), }, )), Ok(Check { @@ -178,6 +179,7 @@ async fn check( whitelist, ips, geo, + subnet_size: target.subnet_size(), }, )), Err(e) => { diff --git a/website/static/main.css b/website/static/main.css index 6cd982c..59e8f93 100644 --- a/website/static/main.css +++ b/website/static/main.css @@ -554,3 +554,89 @@ h1.error-code { } .reset-btn { width: auto; } } + +.ip-list-container { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; +} + +.ip-summary { + display: flex; + flex-direction: column; + gap: 0.25rem; + text-align: right; +} + +.ip-range { + font-size: 0.875rem; + font-weight: 500; + color: #e5e5e5; + font-family: var(--font-mono); +} + +.ip-count { + font-size: 0.75rem; + color: var(--text-muted); +} + +.ip-item { + font-size: 0.875rem; + font-weight: 500; + color: #e5e5e5; + font-family: var(--font-mono); + text-decoration: none; + word-break: break-all; + overflow-wrap: break-word; +} + +.ip-list-details { + margin: 0; +} + +.ip-list-toggle { + cursor: pointer; + color: var(--text-muted); + font-size: 0.75rem; + padding: 0.5rem 0.75rem; + 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); +} + +.ip-list-toggle:hover { + color: var(--text-bright); + border-color: var(--input-border); + background-color: rgba(38, 38, 38, 0.5); +} + +.ip-list-toggle::-webkit-details-marker { + display: none; +} + +.ip-list-expanded { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 0.5rem; + 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) { + .ip-list-container { + align-items: flex-end; + } + + .ip-summary { + align-items: flex-end; + } +} + diff --git a/website/templates/result.html.tera b/website/templates/result.html.tera index 5cc3672..f32d80d 100644 --- a/website/templates/result.html.tera +++ b/website/templates/result.html.tera @@ -47,10 +47,39 @@

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

IP-адреса -
- {% for ip in ips %} -

{{ ip }}

- {% endfor %} +
+ {% if ips | length == 1 %} + {{ ips | first }} + {% if subnet_size %} + ({{ subnet_size }} всего) + {% endif %} + {% elif ips | length <= 5 %} + {% for ip in ips %} + {{ ip }} + {% endfor %} + {% if subnet_size %} + ({{ subnet_size }} всего) + {% endif %} + {% else %} +
+ {{ ips | first }} — {{ ips | last }} + {% if subnet_size %} + ({{ subnet_size }} всего, показано {{ ips | length }}) + {% else %} + ({{ ips | length }} адресов) + {% endif %} +
+
+ + Показать все + +
+ {% for ip in ips %} + {{ ip }} + {% endfor %} +
+
+ {% endif %}
diff --git a/website/templates/search-form.html.tera b/website/templates/search-form.html.tera index b3ac70d..2f6134a 100644 --- a/website/templates/search-form.html.tera +++ b/website/templates/search-form.html.tera @@ -2,7 +2,7 @@