feat: block bogon and rate limit

This commit is contained in:
Lowder
2026-08-17 01:14:07 +05:00
parent 1a06b8a0dc
commit 4bf19a3242
9 changed files with 109 additions and 12 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
"enabled": true
},
"formatter": {
"whitespaceSensitivity": "ignore",
"whitespaceSensitivity": "css",
"enabled": true
}
},
+1
View File
@@ -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:
@@ -91,10 +91,10 @@ const verdictStyles = {
<span
class={`w-2 h-2 rounded-full ${status.online_probes > 0 ? 'bg-green-500 animate-pulse' : 'bg-neutral-600'}`}
></span>
Сканеров онлайн:{status.online_probes}
Сканеров онлайн: {status.online_probes}
</div>
<div class="flex items-center gap-1">
Получено ответов:{probes.length} /{status.online_probes}
Получено ответов: {probes.length} / {status.online_probes}
</div>
</div>
</div>
@@ -178,8 +178,9 @@ const verdictStyles = {
>
<div class="flex flex-col">
<span class="text-xs font-bold text-neutral-400">
Сервер{host.host_id}
({host.host === "Blacklist" ? "в заблокированных" : "в доступных"} диапазонах)
Сервер {host.host_id}
({host.host === "Blacklist" ? "в заблокированных" : "в доступных"}
диапазонах)
</span>
<span class="text-xs text-neutral-200">
{#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}
+61
View File
@@ -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}");
}
}
}
+1 -1
View File
@@ -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};
+1 -1
View File
@@ -45,7 +45,7 @@ pub async fn check(
pool: &State<PgPool>,
limiter: &State<Arc<ApiRateLimiter>>,
) -> Result<Json<ApiCheckResponse>, Status> {
if limiter.check_key(&addr.ip).is_err() {
if !limiter.check(&addr.ip) {
return Err(Status::TooManyRequests);
}
+6 -3
View File
@@ -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<PgPool>,
mqtt: &State<MqttPublisher>,
limiter: &State<Arc<ApiRateLimiter>>,
limiter: &State<Arc<ProbeRateLimiter>>,
) -> Result<EventStream![Event], Status> {
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::<IpAddr>().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}");
+25 -1
View File
@@ -4,9 +4,33 @@ use governor::{Quota, RateLimiter};
use std::net::IpAddr;
use std::num::NonZeroU32;
pub type ApiRateLimiter = RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
type KeyedRateLimiter = RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, 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"),
))
+6
View File
@@ -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(