mirror of
https://github.com/LowderPlay/cheburcheck.git
synced 2026-09-28 17:58:03 +03:00
feat: subnets support
* 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 <me@lowderplay.dev>
This commit is contained in:
co-authored by
Lowder
parent
020ed7be9a
commit
fb3febd54e
Generated
+1
-1
@@ -4225,7 +4225,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "website"
|
||||
version = "0.2.4"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"env_logger",
|
||||
|
||||
+106
-3
@@ -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::<Ipv4Net>() {
|
||||
if ipv4_net.prefix_len() >= 8 {
|
||||
return Target::Ipv4Subnet(ipv4_net);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(ipv6_net) = input.parse::<Ipv6Net>() {
|
||||
if ipv6_net.prefix_len() >= 32 {
|
||||
return Target::Ipv6Subnet(ipv6_net);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if let Ok(ipv4) = input.parse::<Ipv4Addr>() {
|
||||
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<Vec<IpAddr>, 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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "website"
|
||||
version = "0.2.4"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,39 @@
|
||||
<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 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>
|
||||
<div class="detail-row">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="target"
|
||||
placeholder="example.com или 1.1.1.1 или 2606:4700:4700::1001"
|
||||
placeholder="example.com, 1.1.1.1, 1.0.0.0/24, 2606:4700::/32"
|
||||
class="search-input"
|
||||
minlength="3"
|
||||
required
|
||||
|
||||
Reference in New Issue
Block a user