diff --git a/README.md b/README.md index e06c8f6..7606e1b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,54 @@ # PeerBan -> WIP +零配置全自动全平台吸血客户端封禁器,现处于早期开发阶段,欢迎 PR。 -全自动吸血客户端封禁器,开发中。 +```shell +Usage: peerban [OPTIONS] + +Options: + -b, --backend [default: qb] + -e, --endpoint [default: http://127.0.0.1:8080] + -a, --auth [default: admin:admin] + -s, --scan Scan interval in seconds. [default: 5] + -c, --clear Clear all bans before start. + -h, --help Print help +``` + +![SnapShot](./res/snapshot.png) + +## Features + +- 零配置,只需通过命令行传递连接参数 +- 高性能,一般内存占用小于 3 MB +- 启发式封锁,除预置规则外,会自动封禁 `假进度上报/超量下载` 客户端 +- 支持多种后端,目前支持 `qBittorrent` +- 永封,你也可以在启动时加入 `-c` 参数解封全部重新封 + +## Installation + +```shell +cargo install --git https://github.com/jerrita/peerban +``` + +> Docker & Binary is WIP + +## Backend Supports + +- [x] qBittorrent + +## Contributes + +> 欢迎 PR,如果你有任何问题或建议,欢迎提出 Issue。 + +- 后端适配参考 `backend/qb.rs`,你只需要实现 `Backend` trait 中的四个函数即可。 +- 新增内置规则参考 `rules/preload.rs`,新增一行即可。 ## RoadMap - [x] ProtoType -- [ ] Persistence +- [ ] Container +- [ ] WebUI +- [ ] Rule Hot-Update ## Similar Projects diff --git a/res/snapshot.png b/res/snapshot.png new file mode 100644 index 0000000..efba5c6 Binary files /dev/null and b/res/snapshot.png differ diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 744d846..47d99e1 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -11,5 +11,6 @@ pub trait Backend { async fn describe(&mut self) -> Result; async fn get_uploading_torrents(&self) -> Result>; async fn get_peers(&self, hash: &str) -> Result>; + async fn ban_clear(&self) -> Result<()>; async fn ban_peer(&self, peer: &Peer) -> Result<()>; } \ No newline at end of file diff --git a/src/backend/qb.rs b/src/backend/qb.rs index 7756b68..dc2bd19 100644 --- a/src/backend/qb.rs +++ b/src/backend/qb.rs @@ -122,4 +122,16 @@ impl Backend for QBitBackend { .await?; Ok(()) } + + async fn ban_clear(&self) -> Result<()> { + let mut form = HashMap::new(); + form.insert("json", serde_json::json!({"banned_IPs": ""}).to_string()); + reqwest::Client::new() + .post(&format!("{}/app/setPreferences", self.endpoint)) + .header("Cookie", self.cookie.clone().unwrap()) + .form(&form) + .send() + .await?; + Ok(()) + } } \ No newline at end of file diff --git a/src/conf.rs b/src/conf.rs deleted file mode 100644 index 41dee01..0000000 --- a/src/conf.rs +++ /dev/null @@ -1,25 +0,0 @@ -pub struct PeerBanConfig { - pub scan_time: u64, - pub block_time: u64, - - // 进度倒退检测 - pub block_progress_fallback: bool, - pub block_progress_fallback_threshold: f64, - - // 超量下载检测 - pub block_excessive_clients: bool, - pub block_excessive_clients_threshold: f64, -} - -impl Default for PeerBanConfig { - fn default() -> Self { - PeerBanConfig { - scan_time: 3, - block_time: 24 * 60 * 60, - block_progress_fallback: true, - block_progress_fallback_threshold: 0.08, - block_excessive_clients: true, - block_excessive_clients_threshold: 1.5, - } - } -} \ No newline at end of file diff --git a/src/daemon.rs b/src/daemon.rs index 34622e8..d3c7cc9 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -4,57 +4,48 @@ use anyhow::Result; use log::{debug, info, warn}; use crate::backend::Backend; -use crate::conf::PeerBanConfig; use crate::peer::BannedPeer; -use crate::rules::{Rule, RuleType}; use crate::rules::preload::PREDEFINED_RULES; +use crate::rules::Rule; struct Statistic { pub torrents: u64, pub peers: u64, pub banned: u64, - pub released: u64, } pub struct Daemon { backend: Box, - conf: PeerBanConfig, banned: Vec, rules: Vec, + scan_time: u64, + clear: bool, } impl Daemon { - pub fn new(backend: Box, conf: PeerBanConfig) -> Self { - let mut rules = PREDEFINED_RULES.clone(); - if conf.block_progress_fallback { - rules.push(Rule { - class: RuleType::ProgressProbe, - value: conf.block_progress_fallback_threshold.into(), - }); - } - if conf.block_excessive_clients { - rules.push(Rule { - class: RuleType::ExcessiveProbe, - value: conf.block_excessive_clients_threshold.into(), - }); - } + pub fn new(backend: Box, scan: u64, clear: bool) -> Self { + let rules = PREDEFINED_RULES.clone(); Daemon { backend, - conf, banned: Vec::new(), rules, + scan_time: scan, + clear, } } pub async fn run(&mut self) -> Result<()> { info!("Backend: {}", self.backend.describe().await?); - info!("[interval] scan: {}s, block: {}s", self.conf.scan_time, self.conf.block_time); + info!("[interval] scan: {}s", self.scan_time); let mut stat = Statistic { torrents: 0, peers: 0, banned: 0, - released: 0, }; + if self.clear { + self.backend.ban_clear().await?; + info!("[start] jail cleared."); + } loop { let mut flag = false; let torrents = self.backend.get_uploading_torrents().await?; @@ -87,24 +78,11 @@ impl Daemon { } } } - - // Remove expired banned peers - let now = Instant::now(); - self.banned.retain(|banned| { - if now.duration_since(banned.time).as_secs() > self.conf.block_time { - flag = true; - stat.released += 1; - info!("Released {}({}) {:?}.", banned.peer.address, banned.peer.id, banned.rule); - false - } else { - true - } - }); } if flag { - info!("[active] torrents: {}, peers: {}, banned: {}, released: {}", stat.torrents, stat.peers, stat.banned, stat.released); + info!("[active] torrents: {}, peers: {}, banned: {}", stat.torrents, stat.peers, stat.banned); } - tokio::time::sleep(tokio::time::Duration::from_secs(self.conf.scan_time)).await; + tokio::time::sleep(tokio::time::Duration::from_secs(self.scan_time)).await; } } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b4f1206..cd87d2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ mod backend; mod torrent; mod peer; mod rules; -mod conf; mod daemon; #[derive(Parser, Debug)] @@ -19,6 +18,10 @@ struct Args { endpoint: String, #[arg(short, long, default_value = "admin:admin")] auth: String, + #[arg(short, long, default_value = "5", help = "Scan interval in seconds.")] + scan: u64, + #[arg(short, long, default_value = "false", help = "Clear all bans before start.")] + clear: bool, } #[tokio::main] @@ -34,8 +37,7 @@ async fn main() -> Result<(), Box> { } let qb = QBitBackend::new(args.endpoint, args.auth); - let conf = conf::PeerBanConfig::default(); - let mut daemon = Daemon::new(Box::new(qb), conf); + let mut daemon = Daemon::new(Box::new(qb), args.scan, args.clear); loop { match daemon.run().await { Ok(_) => (), diff --git a/src/rules/mod.rs b/src/rules/mod.rs index b695552..7f3d04a 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -4,7 +4,7 @@ use crate::peer::Peer; pub mod preload; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum RuleType { IDPrefixMatch, IDContains, @@ -57,9 +57,13 @@ impl From<&str> for Rule { if split.clone().count() != 2 { panic!("Invalid rule string, use class@value format."); } + let class = split.next().unwrap().into(); Rule { - class: split.next().unwrap().into(), - value: split.next().unwrap().into(), + class, + value: match class { + RuleType::ProgressProbe | RuleType::ExcessiveProbe => Value::Number(split.next().unwrap().parse().unwrap()), + _ => Value::String(split.next().unwrap().to_string()), + }, } } } \ No newline at end of file diff --git a/src/rules/preload.rs b/src/rules/preload.rs index 969df32..dfc51ca 100644 --- a/src/rules/preload.rs +++ b/src/rules/preload.rs @@ -4,38 +4,41 @@ use crate::rules::Rule; lazy_static!( pub static ref PREDEFINED_RULES: Vec = vec![ - "idStartsWith@-XL", - "idStartsWith@-SD", - "idStartsWith@-XF", - "idStartsWith@-QD", - "idStartsWith@-BN", - "idStartsWith@-DL", - "idStartsWith@-TS", - "idStartsWith@-FG", - "idStartsWith@-TT", - "idStartsWith@-NX", - "idStartsWith@-SP", - "idStartsWith@-GT0002", - "idStartsWith@-GT0003", - "idStartsWith@-DT", - "idStartsWith@-HP", - "idContains@cacao", + "idStartsWith@-XL", + "idStartsWith@-SD", + "idStartsWith@-XF", + "idStartsWith@-QD", + "idStartsWith@-BN", + "idStartsWith@-DL", + "idStartsWith@-TS", + "idStartsWith@-FG", + "idStartsWith@-TT", + "idStartsWith@-NX", + "idStartsWith@-SP", + "idStartsWith@-GT0002", + "idStartsWith@-GT0003", + "idStartsWith@-DT", + "idStartsWith@-HP", + "idContains@cacao", - "nameStartsWith@-XL", - "nameContains@Xunlei", - "nameStartsWith@TaiPei-Torrent", - "nameStartsWith@Xfplay", - "nameStartsWith@BitSpirit", - "nameContains@FlashGet", - "nameContains@TuDou", - "nameContains@TorrentStorm", - "nameContains@QQDownload", - "nameContains@github.com/anacrolix/torrent", - "nameStartsWith@qBittorrent/3.3.15", - "nameStartsWith@dt/torrent", - "nameStartsWith@hp/torrent", - "nameStartsWith@DT", - "nameStartsWith@go.torrent.dev", - "nameStartsWith@github.com/thank423/trafficConsume", + "nameStartsWith@-XL", + "nameContains@Xunlei", + "nameStartsWith@TaiPei-Torrent", + "nameStartsWith@Xfplay", + "nameStartsWith@BitSpirit", + "nameContains@FlashGet", + "nameContains@TuDou", + "nameContains@TorrentStorm", + "nameContains@QQDownload", + "nameContains@github.com/anacrolix/torrent", + "nameStartsWith@qBittorrent/3.3.15", + "nameStartsWith@dt/torrent", + "nameStartsWith@hp/torrent", + "nameStartsWith@DT", + "nameStartsWith@go.torrent.dev", + "nameStartsWith@github.com/thank423/trafficConsume", + + "progressProbe@0.08", // 进度倒退检测 + "excessiveProbe@1.5", // 超量下载检测 ].iter().map(|&s| Rule::from(s)).collect(); ); \ No newline at end of file