mirror of
https://github.com/LowderPlay/cheburcheck.git
synced 2026-10-06 21:57:58 +03:00
feat: block bogon and rate limit
This commit is contained in:
+1
-1
@@ -11,7 +11,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"formatter": {
|
||||
"whitespaceSensitivity": "ignore",
|
||||
"whitespaceSensitivity": "css",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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"),
|
||||
))
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user