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(