diff --git a/biome.json b/biome.json index f85db2b..eba3124 100644 --- a/biome.json +++ b/biome.json @@ -11,7 +11,7 @@ "enabled": true }, "formatter": { - "whitespaceSensitivity": "ignore", + "whitespaceSensitivity": "css", "enabled": true } }, diff --git a/docker-compose.yaml b/docker-compose.yaml index cbc8d56..82075bb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -34,6 +34,7 @@ services: MQTT_ADMIN_TOKEN: "${MQTT_ADMIN_TOKEN}" MQTT_HOST: rmqtt MQTT_PORT: 11883 + PROBE_RATE_LIMIT_RPM: "${PROBE_RATE_LIMIT_RPM:-30}" volumes: - database-cache:/var/cache/cheburcheck/databases expose: diff --git a/frontend/src/lib/components/result/ProbeTable.svelte b/frontend/src/lib/components/result/ProbeTable.svelte index 92ec4bf..d15851c 100644 --- a/frontend/src/lib/components/result/ProbeTable.svelte +++ b/frontend/src/lib/components/result/ProbeTable.svelte @@ -91,10 +91,10 @@ const verdictStyles = { 0 ? 'bg-green-500 animate-pulse' : 'bg-neutral-600'}`} > - Сканеров онлайн:{status.online_probes} + Сканеров онлайн: {status.online_probes}
- Получено ответов:{probes.length} /{status.online_probes} + Получено ответов: {probes.length} / {status.online_probes}
@@ -178,8 +178,9 @@ const verdictStyles = { >
- Сервер{host.host_id} - ({host.host === "Blacklist" ? "в заблокированных" : "в доступных"} диапазонах) + Сервер {host.host_id} + ({host.host === "Blacklist" ? "в заблокированных" : "в доступных"} + диапазонах) {#if host.probe_evidence.type === 'Good'} @@ -187,7 +188,8 @@ const verdictStyles = { {:else if host.probe_evidence.type === 'ClientHello'} Блокировка после ClientHello {:else if host.probe_evidence.type === 'DataTimeout'} - Таймаут получения данных, получено{host.probe_evidence.bytes} байт + Таймаут получения данных, получено{host.probe_evidence.bytes} + байт {:else if host.probe_evidence.type === 'ConnectionError'} Ошибка подключения {/if} diff --git a/querying/src/target.rs b/querying/src/target.rs index 933d5e8..a869d71 100644 --- a/querying/src/target.rs +++ b/querying/src/target.rs @@ -57,6 +57,36 @@ impl From<&str> for Target { } impl Target { + pub fn is_bogon(ip: IpAddr) -> bool { + match ip { + IpAddr::V4(ip) => { + let [a, b, c, d] = ip.octets(); + a == 0 + || a == 10 + || a == 127 + || (a == 100 && (64..=127).contains(&b)) + || (a == 169 && b == 254) + || (a == 172 && (16..=31).contains(&b)) + || (a == 192 && b == 0 && c == 0 && d != 9 && d != 10) + || (a == 192 && b == 0 && c == 2) + || (a == 192 && b == 88 && c == 99) + || (a == 192 && b == 168) + || (a == 198 && (b == 18 || b == 19)) + || (a == 198 && b == 51 && c == 100) + || (a == 203 && b == 0 && c == 113) + || a >= 224 + } + IpAddr::V6(ip) => { + let segments = ip.segments(); + let first = segments[0]; + first & 0xe000 != 0x2000 + || (first == 0x2001 && segments[1] < 0x0200) + || (first == 0x2001 && segments[1] == 0x0db8) + || (first == 0x3fff && segments[1] < 0x1000) + } + } + } + pub fn readable_type(&self) -> &'static str { match self { Target::Domain(_) => "Домен", @@ -155,3 +185,34 @@ fn format_large_number(n: u128) -> String { format!("{:.1}Z", n as f64 / 1_000_000_000_000_000_000_000.0) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identifies_bogon_addresses() { + for address in [ + "10.0.0.1", + "100.64.0.1", + "127.0.0.1", + "169.254.1.1", + "192.0.2.1", + "198.18.0.1", + "224.0.0.1", + "::1", + "2001:db8::1", + "fc00::1", + "fe80::1", + ] { + assert!(Target::is_bogon(address.parse().unwrap()), "{address}"); + } + } + + #[test] + fn accepts_public_addresses() { + for address in ["1.1.1.1", "8.8.8.8", "2001:4860:4860::8888"] { + assert!(!Target::is_bogon(address.parse().unwrap()), "{address}"); + } + } +} diff --git a/website/src/api.rs b/website/src/api.rs index 2a8126f..4ae66ef 100644 --- a/website/src/api.rs +++ b/website/src/api.rs @@ -9,5 +9,5 @@ mod status; pub use check_endpoint::check; pub use feedback_endpoint::feedback; pub use probe::probe_query; -pub use rate_limit::build_rate_limiter; +pub use rate_limit::{build_probe_rate_limiter, build_rate_limiter}; pub use status::{get_system_status, healthcheck}; diff --git a/website/src/api/check.rs b/website/src/api/check.rs index 105ac3d..505cafe 100644 --- a/website/src/api/check.rs +++ b/website/src/api/check.rs @@ -45,7 +45,7 @@ pub async fn check( pool: &State, limiter: &State>, ) -> Result, Status> { - if limiter.check_key(&addr.ip).is_err() { + if !limiter.check(&addr.ip) { return Err(Status::TooManyRequests); } diff --git a/website/src/api/probe.rs b/website/src/api/probe.rs index 4013548..c02ae74 100644 --- a/website/src/api/probe.rs +++ b/website/src/api/probe.rs @@ -1,4 +1,4 @@ -use super::rate_limit::ApiRateLimiter; +use super::rate_limit::ProbeRateLimiter; use crate::mqtt::{MqttPublisher, PublishError}; use log::warn; use querying::target::Target; @@ -32,9 +32,9 @@ pub async fn probe_query( addr: &ClientRealAddr, pool: &State, mqtt: &State, - limiter: &State>, + limiter: &State>, ) -> Result { - if limiter.check_key(&addr.ip).is_err() { + if !limiter.check(&addr.ip) { return Err(Status::TooManyRequests); } @@ -59,6 +59,9 @@ pub async fn probe_query( .first() .and_then(|ip| ip.parse::().ok()) .ok_or(Status::BadRequest)?; + if Target::is_bogon(ip) { + return Err(Status::Forbidden); + } let mut results = mqtt.subscribe_probe_results(id).await.map_err(|error| { warn!("api: failed to subscribe to probe results for {id}: {error}"); diff --git a/website/src/api/rate_limit.rs b/website/src/api/rate_limit.rs index 42a9be3..322a0d6 100644 --- a/website/src/api/rate_limit.rs +++ b/website/src/api/rate_limit.rs @@ -4,9 +4,33 @@ use governor::{Quota, RateLimiter}; use std::net::IpAddr; use std::num::NonZeroU32; -pub type ApiRateLimiter = RateLimiter, DefaultClock>; +type KeyedRateLimiter = RateLimiter, DefaultClock>; + +pub struct ApiRateLimiter(KeyedRateLimiter); + +pub struct ProbeRateLimiter(KeyedRateLimiter); + +impl ApiRateLimiter { + pub fn check(&self, ip: &IpAddr) -> bool { + self.0.check_key(ip).is_ok() + } +} + +impl ProbeRateLimiter { + pub fn check(&self, ip: &IpAddr) -> bool { + self.0.check_key(ip).is_ok() + } +} pub fn build_rate_limiter(per_minute: u32) -> ApiRateLimiter { + ApiRateLimiter(build_limiter(per_minute)) +} + +pub fn build_probe_rate_limiter(per_minute: u32) -> ProbeRateLimiter { + ProbeRateLimiter(build_limiter(per_minute)) +} + +fn build_limiter(per_minute: u32) -> KeyedRateLimiter { RateLimiter::keyed(Quota::per_minute( NonZeroU32::new(per_minute).expect("rate limit must be > 0"), )) diff --git a/website/src/main.rs b/website/src/main.rs index d3b6175..796c6e8 100644 --- a/website/src/main.rs +++ b/website/src/main.rs @@ -58,7 +58,12 @@ async fn rocket() -> _ { .unwrap_or("30".to_string()) .parse() .unwrap_or(30); + let probe_rate_limit_rpm: u32 = std::env::var("PROBE_RATE_LIMIT_RPM") + .unwrap_or("5".to_string()) + .parse() + .unwrap_or(5); let api_limiter = std::sync::Arc::new(api::build_rate_limiter(rate_limit_rpm)); + let probe_limiter = std::sync::Arc::new(api::build_probe_rate_limiter(probe_rate_limit_rpm)); let mqtt_publisher = mqtt::MqttPublisher::start_from_env(); let pool = sqlx::postgres::PgPoolOptions::new() @@ -84,6 +89,7 @@ async fn rocket() -> _ { .manage(checker) .manage(pool) .manage(api_limiter) + .manage(probe_limiter) .manage(mqtt_publisher) .attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations)) .mount(