use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use oxcache::invalidation::PubSubTransport;
use serde::{Deserialize, Serialize};
use tokio::task::JoinHandle;
use crate::error::{LimiteronError, StorageError};
pub const DEFAULT_BAN_SYNC_CHANNEL: &str = "limiteron:ban-sync";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BanSyncKind {
BanApplied,
BanRemoved,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BanSyncMessage {
pub kind: BanSyncKind,
pub target_key: String,
#[serde(default)]
pub reason: String,
pub expires_at_epoch: Option<i64>,
pub origin: String,
}
impl BanSyncMessage {
pub fn ban_applied(
origin: &str,
target_key: &str,
reason: &str,
expires_at_epoch: Option<i64>,
) -> Self {
Self {
kind: BanSyncKind::BanApplied,
target_key: target_key.to_string(),
reason: reason.to_string(),
expires_at_epoch,
origin: origin.to_string(),
}
}
pub fn ban_removed(origin: &str, target_key: &str) -> Self {
Self {
kind: BanSyncKind::BanRemoved,
target_key: target_key.to_string(),
reason: String::new(),
expires_at_epoch: None,
origin: origin.to_string(),
}
}
pub fn encode(&self) -> Result<String, LimiteronError> {
serde_json::to_string(self)
.map_err(|e| StorageError::QueryError(format!("ban-sync encode: {e}")).into())
}
pub fn decode(payload: &str) -> Result<Self, LimiteronError> {
serde_json::from_str(payload)
.map_err(|e| StorageError::QueryError(format!("ban-sync decode: {e}")).into())
}
}
#[derive(Debug, Clone)]
pub struct BanSyncConfig {
pub channel: String,
pub instance_id: String,
}
impl BanSyncConfig {
pub fn new(instance_id: impl Into<String>) -> Self {
Self {
channel: DEFAULT_BAN_SYNC_CHANNEL.to_string(),
instance_id: instance_id.into(),
}
}
}
#[async_trait::async_trait]
pub trait BanSyncApplier: Send + Sync {
async fn on_remote_ban(&self, msg: &BanSyncMessage);
async fn on_remote_unban(&self, msg: &BanSyncMessage);
}
pub struct BanSyncListenerHandle {
join: JoinHandle<()>,
stop: Arc<AtomicBool>,
}
impl BanSyncListenerHandle {
pub fn stop(&self) {
self.stop.store(true, Ordering::SeqCst);
}
pub async fn join(self) {
let _ = self.join.await;
}
}
pub struct BanSyncBus {
transport: Arc<dyn PubSubTransport>,
config: BanSyncConfig,
}
impl BanSyncBus {
pub fn new(transport: Arc<dyn PubSubTransport>, config: BanSyncConfig) -> Self {
Self { transport, config }
}
pub fn instance_id(&self) -> &str {
&self.config.instance_id
}
pub async fn publish(&self, msg: BanSyncMessage) -> Result<(), LimiteronError> {
self.transport
.publish(&self.config.channel, &msg.encode()?)
.await
.map_err(|e| StorageError::ConnectionError(e.to_string()))?;
Ok(())
}
pub async fn spawn_listener(
&self,
applier: Arc<dyn BanSyncApplier>,
) -> Result<BanSyncListenerHandle, LimiteronError> {
let mut rx = self
.transport
.subscribe(&self.config.channel)
.await
.map_err(|e| StorageError::ConnectionError(e.to_string()))?;
let instance_id = self.config.instance_id.clone();
let stop = Arc::new(AtomicBool::new(false));
let stop_flag = stop.clone();
let join = tokio::spawn(async move {
loop {
if stop_flag.load(Ordering::SeqCst) {
break;
}
let payload =
match tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv())
.await
{
Ok(Some(payload)) => payload,
Ok(None) => break, Err(_) => continue, };
let msg = match BanSyncMessage::decode(&payload) {
Ok(m) => m,
Err(e) => {
log::warn!(target: "limiteron", "dropping undecodable ban-sync message: {e}");
continue;
}
};
if msg.origin == instance_id {
continue;
}
match msg.kind {
BanSyncKind::BanApplied => applier.on_remote_ban(&msg).await,
BanSyncKind::BanRemoved => applier.on_remote_unban(&msg).await,
}
}
});
Ok(BanSyncListenerHandle { join, stop })
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxcache::invalidation::InMemoryPubSubTransport;
use parking_lot::Mutex;
#[derive(Default)]
struct RecordingApplier {
bans: Mutex<Vec<BanSyncMessage>>,
unbans: Mutex<Vec<BanSyncMessage>>,
}
#[async_trait::async_trait]
impl BanSyncApplier for RecordingApplier {
async fn on_remote_ban(&self, msg: &BanSyncMessage) {
self.bans.lock().push(msg.clone());
}
async fn on_remote_unban(&self, msg: &BanSyncMessage) {
self.unbans.lock().push(msg.clone());
}
}
async fn wait_for(cond: impl Fn() -> bool) {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while !cond() {
assert!(
tokio::time::Instant::now() < deadline,
"5s 内未收到同步消息"
);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
}
#[tokio::test]
async fn test_t616_ban_visible_on_other_instance() {
let transport = Arc::new(InMemoryPubSubTransport::new());
let bus_a = BanSyncBus::new(transport.clone(), BanSyncConfig::new("inst-a"));
let bus_b = BanSyncBus::new(transport.clone(), BanSyncConfig::new("inst-b"));
let applier_b = Arc::new(RecordingApplier::default());
let listener_b = bus_b.spawn_listener(applier_b.clone()).await.unwrap();
bus_a
.publish(BanSyncMessage::ban_applied(
"inst-a",
"ip:192.0.2.7",
"abuse",
Some(1_800_000_000),
))
.await
.unwrap();
wait_for(|| !applier_b.bans.lock().is_empty()).await;
let got = applier_b.bans.lock()[0].clone();
assert_eq!(got.target_key, "ip:192.0.2.7");
assert_eq!(got.reason, "abuse");
assert_eq!(got.expires_at_epoch, Some(1_800_000_000));
bus_a
.publish(BanSyncMessage::ban_removed("inst-a", "ip:192.0.2.7"))
.await
.unwrap();
wait_for(|| !applier_b.unbans.lock().is_empty()).await;
assert_eq!(applier_b.unbans.lock()[0].target_key, "ip:192.0.2.7");
listener_b.stop();
listener_b.join().await;
}
#[tokio::test]
async fn test_t616_self_message_is_exempt() {
let transport = Arc::new(InMemoryPubSubTransport::new());
let bus_a = BanSyncBus::new(transport.clone(), BanSyncConfig::new("inst-a"));
let applier_a = Arc::new(RecordingApplier::default());
let listener_a = bus_a.spawn_listener(applier_a.clone()).await.unwrap();
bus_a
.publish(BanSyncMessage::ban_applied(
"inst-a", "user:u1", "self", None,
))
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
assert!(applier_a.bans.lock().is_empty(), "自身实例的消息必须被豁免");
listener_a.stop();
listener_a.join().await;
}
#[tokio::test]
async fn test_t616_fanout_to_multiple_instances() {
let transport = Arc::new(InMemoryPubSubTransport::new());
let bus_a = BanSyncBus::new(transport.clone(), BanSyncConfig::new("inst-a"));
let bus_b = BanSyncBus::new(transport.clone(), BanSyncConfig::new("inst-b"));
let bus_c = BanSyncBus::new(transport.clone(), BanSyncConfig::new("inst-c"));
let applier_b = Arc::new(RecordingApplier::default());
let applier_c = Arc::new(RecordingApplier::default());
let lb = bus_b.spawn_listener(applier_b.clone()).await.unwrap();
let lc = bus_c.spawn_listener(applier_c.clone()).await.unwrap();
bus_a
.publish(BanSyncMessage::ban_applied(
"inst-a",
"cidr:10.0.0.0/8",
"ddos",
None,
))
.await
.unwrap();
wait_for(|| !applier_b.bans.lock().is_empty()).await;
wait_for(|| !applier_c.bans.lock().is_empty()).await;
assert_eq!(applier_c.bans.lock()[0].target_key, "cidr:10.0.0.0/8");
lb.stop();
lc.stop();
lb.join().await;
lc.join().await;
}
#[test]
fn test_t616_message_encode_decode_roundtrip() {
let msg = BanSyncMessage::ban_applied("i", "ip:1.2.3.4", "r", Some(42));
let wire = msg.encode().unwrap();
assert_eq!(BanSyncMessage::decode(&wire).unwrap(), msg);
assert!(BanSyncMessage::decode("not json").is_err());
}
}