add: ban ipv6 addresses by prefix

assumes all ipv6 addresses have 64-bit prefix (8 bytes at the start + 2 bytes for colons)
This commit is contained in:
trisua 2025-05-21 23:32:45 -04:00
parent 2b91422d18
commit d7e800fcb4
6 changed files with 128 additions and 4 deletions

View file

@ -5,7 +5,7 @@ use crate::{
}; };
use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{auth::IpBan, permissions::FinePermission}; use tetratto_core::model::{addr::RemoteAddr, auth::IpBan, permissions::FinePermission};
/// Create a new IP ban. /// Create a new IP ban.
pub async fn create_request( pub async fn create_request(
@ -24,7 +24,14 @@ pub async fn create_request(
return Json(Error::NotAllowed.into()); return Json(Error::NotAllowed.into());
} }
match data.create_ipban(IpBan::new(ip, user.id, req.reason)).await { match data
.create_ipban(IpBan::new(
RemoteAddr::from(ip.as_str()).prefix(None),
user.id,
req.reason,
))
.await
{
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "IP ban created".to_string(), message: "IP ban created".to_string(),

View file

@ -18,6 +18,7 @@ use axum::{
}; };
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::model::addr::RemoteAddr;
use tetratto_shared::hash::hash; use tetratto_shared::hash::hash;
use cf_turnstile::{SiteVerifyRequest, TurnstileClient}; use cf_turnstile::{SiteVerifyRequest, TurnstileClient};
@ -52,7 +53,11 @@ pub async fn register_request(
.to_string(); .to_string();
// check for ip ban // check for ip ban
if data.get_ipban_by_ip(&real_ip).await.is_ok() { if data
.get_ipban_by_addr(RemoteAddr::from(real_ip.as_str()))
.await
.is_ok()
{
return (None, Json(Error::NotAllowed.into())); return (None, Json(Error::NotAllowed.into()));
} }

View file

@ -1,6 +1,12 @@
use axum::{extract::Path, response::IntoResponse, Extension, Json}; use axum::{
extract::Path,
http::{HeaderMap, HeaderValue},
response::IntoResponse,
Extension, Json,
};
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
addr::RemoteAddr,
communities::Post, communities::Post,
permissions::FinePermission, permissions::FinePermission,
uploads::{MediaType, MediaUpload}, uploads::{MediaType, MediaUpload},
@ -18,6 +24,7 @@ pub const MAXIMUM_FILE_SIZE: usize = 4194304;
pub async fn create_request( pub async fn create_request(
jar: CookieJar, jar: CookieJar,
headers: HeaderMap,
Extension(data): Extension<State>, Extension(data): Extension<State>,
JsonMultipart(images, req): JsonMultipart<CreatePost>, JsonMultipart(images, req): JsonMultipart<CreatePost>,
) -> impl IntoResponse { ) -> impl IntoResponse {
@ -42,6 +49,24 @@ pub async fn create_request(
); );
} }
// get real ip
let real_ip = headers
.get(data.0.security.real_ip_header.to_owned())
.unwrap_or(&HeaderValue::from_static(""))
.to_str()
.unwrap_or("")
.to_string();
// check for ip ban
if data
.get_ipban_by_addr(RemoteAddr::from(real_ip.as_str()))
.await
.is_ok()
{
return Json(Error::NotAllowed.into());
}
// ...
let mut props = Post::new( let mut props = Post::new(
req.content, req.content,
match req.community.parse::<usize>() { match req.community.parse::<usize>() {

View file

@ -1,5 +1,6 @@
use super::*; use super::*;
use crate::cache::Cache; use crate::cache::Cache;
use crate::model::addr::RemoteAddr;
use crate::model::moderation::AuditLogEntry; use crate::model::moderation::AuditLogEntry;
use crate::model::{Error, Result, auth::IpBan, auth::User, permissions::FinePermission}; use crate::model::{Error, Result, auth::IpBan, auth::User, permissions::FinePermission};
use crate::{auto_method, execute, get, query_row, query_rows, params}; use crate::{auto_method, execute, get, query_row, query_rows, params};
@ -26,6 +27,30 @@ impl DataManager {
auto_method!(get_ipban_by_ip(&str)@get_ipban_from_row -> "SELECT * FROM ipbans WHERE ip = $1" --name="ip ban" --returns=IpBan --cache-key-tmpl="atto.ipban:{}"); auto_method!(get_ipban_by_ip(&str)@get_ipban_from_row -> "SELECT * FROM ipbans WHERE ip = $1" --name="ip ban" --returns=IpBan --cache-key-tmpl="atto.ipban:{}");
/// Get an IP ban as a [`RemoteAddr`].
///
/// # Arguments
/// * `prefix`
pub async fn get_ipban_by_addr(&self, addr: RemoteAddr) -> Result<IpBan> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM ipbans WHERE ip LIKE $1",
&[&format!("{}%", addr.prefix(None))],
|x| { Ok(Self::get_ipban_from_row(x)) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("ip ban".to_string()));
}
Ok(res.unwrap())
}
/// Get all IP bans (paginated). /// Get all IP bans (paginated).
/// ///
/// # Arguments /// # Arguments

View file

@ -0,0 +1,61 @@
use std::net::SocketAddr;
/// How many bytes should be chopped off the end of an IPV6 address to get its prefix.
pub(crate) const IPV6_PREFIX_BYTES: usize = 8;
/// The protocol of a [`RemoteAddr`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AddrProto {
IPV4,
IPV6,
}
/// A representation of a remote IP address.
#[derive(Clone, Debug)]
pub struct RemoteAddr(pub SocketAddr, pub AddrProto);
impl From<&str> for RemoteAddr {
fn from(value: &str) -> Self {
if value.len() >= 16 {
// ipv6 (16 bytes; 128 bits)
if !(value.starts_with("[") | value.contains("]:1000")) {
Self(format!("[{value}]:1000").parse().unwrap(), AddrProto::IPV6)
} else {
Self(value.parse().unwrap(), AddrProto::IPV6)
}
} else {
// ipv4 (4 bytes; 32 bits)
Self(value.parse().unwrap(), AddrProto::IPV4)
}
}
}
impl RemoteAddr {
/// Get the address' prefix (returns the entire address for IPV4 addresses).
///
/// Operates on the IP address **without** colon characters.
pub fn prefix(&self, chop: Option<usize>) -> String {
if self.1 == AddrProto::IPV4 {
return self.0.to_string();
}
// we're adding 2 bytes to the chop bytes because we also
// need to remove 2 colons
let as_string = self.ipv6_inner();
(&as_string[0..(match chop {
Some(c) => c,
None => IPV6_PREFIX_BYTES,
} + (IPV6_PREFIX_BYTES / 4))])
.to_string()
}
/// [`Self::prefix`], but it returns another [`RemoteAddr`].
pub fn to_prefix(self, chop: Option<usize>) -> Self {
Self::from(self.prefix(chop).as_str())
}
/// Get the innter content from the address (as ipv6).
pub fn ipv6_inner(&self) -> String {
self.0.to_string().replace("[", "").replace("]:1000", "")
}
}

View file

@ -1,3 +1,4 @@
pub mod addr;
pub mod auth; pub mod auth;
pub mod communities; pub mod communities;
pub mod communities_permissions; pub mod communities_permissions;