mirror of
https://codeberg.org/YGGverse/psocks.git
synced 2026-03-31 16:35:28 +00:00
implement in-memory ruleset update
This commit is contained in:
parent
b03bdd0e3a
commit
30a77072ed
4 changed files with 85 additions and 32 deletions
|
|
@ -7,7 +7,8 @@ Experimental async SOCKS5 (TCP/UDP) proxy server based on [fast-socks5](https://
|
||||||
* [ ] Range support
|
* [ ] Range support
|
||||||
* [ ] Local Web-API
|
* [ ] Local Web-API
|
||||||
* [x] Block stats
|
* [x] Block stats
|
||||||
* [ ] In-memory list update (without server restart)
|
* [x] In-memory list update (without server restart)
|
||||||
|
* [ ] Persist changes option
|
||||||
* [ ] Performance optimization
|
* [ ] Performance optimization
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
@ -18,7 +19,9 @@ RUST_LOG=psocks=trace cargo run -- -a=/path/to/allow1.txt \
|
||||||
no-auth
|
no-auth
|
||||||
```
|
```
|
||||||
* set `socks5://127.0.0.1:1080` proxy in your application
|
* set `socks5://127.0.0.1:1080` proxy in your application
|
||||||
* open http://127.0.0.1:8010 in browser for stats & control API
|
* open http://127.0.0.1:8010 in browser for global stats:
|
||||||
|
* http://127.0.0.1:8010/allow/domain.com - add rule to the current session
|
||||||
|
* http://127.0.0.1:8010/block/domain.com - delete rule from the current session
|
||||||
|
|
||||||
### Allow list example
|
### Allow list example
|
||||||
|
|
||||||
|
|
|
||||||
54
src/list.rs
54
src/list.rs
|
|
@ -1,6 +1,7 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use log::*;
|
use log::*;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Hash)]
|
#[derive(PartialEq, Eq, Hash)]
|
||||||
pub enum Item {
|
pub enum Item {
|
||||||
|
|
@ -8,8 +9,20 @@ pub enum Item {
|
||||||
Exact(String),
|
Exact(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
pub fn from_line(rule: &str) -> Self {
|
||||||
|
if let Some(item) = rule.strip_prefix(".") {
|
||||||
|
debug!("Add `{rule}` to the whitelist");
|
||||||
|
Self::Ending(item.to_string())
|
||||||
|
} else {
|
||||||
|
debug!("Add `{rule}` exact match to the whitelist");
|
||||||
|
Self::Exact(rule.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum List {
|
pub enum List {
|
||||||
Allow(HashSet<Item>),
|
Allow(RwLock<HashSet<Item>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl List {
|
impl List {
|
||||||
|
|
@ -26,31 +39,40 @@ impl List {
|
||||||
if line.starts_with("/") || line.starts_with("#") || line.is_empty() {
|
if line.starts_with("/") || line.starts_with("#") || line.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !this.insert(if let Some(item) = line.strip_prefix(".") {
|
if !this.insert(Item::from_line(line)) {
|
||||||
debug!("Add `{line}` to the whitelist");
|
|
||||||
Item::Ending(item.to_string())
|
|
||||||
} else {
|
|
||||||
debug!("Add `{line}` exact match to the whitelist");
|
|
||||||
Item::Exact(line.to_string())
|
|
||||||
}) {
|
|
||||||
warn!("Duplicated whitelist record: `{line}`")
|
warn!("Duplicated whitelist record: `{line}`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!("Total whitelist entries parsed: {}", this.len());
|
info!("Total whitelist entries parsed: {}", this.len());
|
||||||
Ok(Self::Allow(this))
|
Ok(Self::Allow(RwLock::new(this)))
|
||||||
}
|
}
|
||||||
pub fn has(&self, value: &str) -> bool {
|
pub async fn any(&self, values: &[&str]) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Allow(list) => list.iter().any(|item| match item {
|
Self::Allow(list) => {
|
||||||
Item::Exact(s) => s == value,
|
let guard = list.read().await;
|
||||||
Item::Ending(s) => value.ends_with(s),
|
values.iter().any(|&value| {
|
||||||
}),
|
guard.iter().any(|item| match item {
|
||||||
|
Item::Exact(v) => v == value,
|
||||||
|
Item::Ending(v) => value.ends_with(v),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn entries(&self) -> usize {
|
pub async fn entries(&self) -> u64 {
|
||||||
match self {
|
match self {
|
||||||
Self::Allow(list) => list.len(),
|
Self::Allow(list) => list.read().await.len() as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn allow(&self, rule: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
List::Allow(list) => list.write().await.insert(Item::from_line(rule)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn block(&self, rule: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
List::Allow(list) => list.write().await.remove(&Item::from_line(rule)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
src/main.rs
50
src/main.rs
|
|
@ -10,28 +10,56 @@ use fast_socks5::{
|
||||||
use list::List;
|
use list::List;
|
||||||
use log::*;
|
use log::*;
|
||||||
use opt::{AuthMode, Opt};
|
use opt::{AuthMode, Opt};
|
||||||
use rocket::serde::json::Json;
|
use rocket::{State, serde::json::Json};
|
||||||
use stats::{Snapshot, Total};
|
use stats::{Snapshot, Total};
|
||||||
use std::{future::Future, sync::Arc};
|
use std::{future::Future, sync::Arc};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use tokio::{net::TcpListener, task};
|
use tokio::{net::TcpListener, task};
|
||||||
|
|
||||||
#[rocket::get("/")]
|
#[rocket::get("/")]
|
||||||
async fn index(totals: &rocket::State<Arc<Total>>) -> Json<Snapshot> {
|
async fn index(totals: &State<Arc<Total>>) -> Json<Snapshot> {
|
||||||
Json(totals.inner().snapshot())
|
Json(totals.inner().snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/allow/<rule>")]
|
||||||
|
async fn allow(rule: &str, list: &State<Arc<List>>, totals: &State<Arc<Total>>) -> Json<bool> {
|
||||||
|
let result = list.allow(rule).await;
|
||||||
|
totals.set_entries(list.entries().await);
|
||||||
|
info!("Delete `{rule}` from the in-memory rules (operation status: {result:?})");
|
||||||
|
Json(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/block/<rule>")]
|
||||||
|
async fn block(rule: &str, list: &State<Arc<List>>, totals: &State<Arc<Total>>) -> Json<bool> {
|
||||||
|
let result = list.block(rule).await;
|
||||||
|
totals.set_entries(list.entries().await);
|
||||||
|
info!("Add `{rule}` to the in-memory rules (operation status: {result:?})");
|
||||||
|
Json(result)
|
||||||
|
}
|
||||||
|
|
||||||
#[rocket::launch]
|
#[rocket::launch]
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let opt: &'static Opt = Box::leak(Box::new(Opt::from_args()));
|
let opt: &'static Opt = Box::leak(Box::new(Opt::from_args()));
|
||||||
let totals = Arc::new(Total::default());
|
|
||||||
|
let list = Arc::new(
|
||||||
|
List::from_opt(&opt.allow_list)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!("Can't parse whitelist: `{err}`");
|
||||||
|
SocksError::ArgumentInputError("Can't parse whitelist")
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let totals = Arc::new(Total::with_entries(list.entries().await));
|
||||||
|
|
||||||
tokio::spawn({
|
tokio::spawn({
|
||||||
|
let socks_list = list.clone();
|
||||||
let socks_totals = totals.clone();
|
let socks_totals = totals.clone();
|
||||||
async move {
|
async move {
|
||||||
if let Err(err) = spawn_socks_server(opt, socks_totals).await {
|
if let Err(err) = spawn_socks_server(opt, socks_list, socks_totals).await {
|
||||||
error!("SOCKS server failed: `{err}`");
|
error!("SOCKS server failed: `{err}`");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,11 +71,12 @@ async fn rocket() -> _ {
|
||||||
address: opt.api_addr.ip(),
|
address: opt.api_addr.ip(),
|
||||||
..rocket::Config::release_default()
|
..rocket::Config::release_default()
|
||||||
})
|
})
|
||||||
|
.manage(list)
|
||||||
.manage(totals)
|
.manage(totals)
|
||||||
.mount("/", rocket::routes![index])
|
.mount("/", rocket::routes![index, allow, block])
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_socks_server(opt: &'static Opt, totals: Arc<Total>) -> Result<()> {
|
async fn spawn_socks_server(opt: &'static Opt, list: Arc<List>, totals: Arc<Total>) -> Result<()> {
|
||||||
if opt.allow_udp && opt.public_addr.is_none() {
|
if opt.allow_udp && opt.public_addr.is_none() {
|
||||||
return Err(SocksError::ArgumentInputError(
|
return Err(SocksError::ArgumentInputError(
|
||||||
"Can't allow UDP if public-addr is not set",
|
"Can't allow UDP if public-addr is not set",
|
||||||
|
|
@ -59,13 +88,6 @@ async fn spawn_socks_server(opt: &'static Opt, totals: Arc<Total>) -> Result<()>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let list = Arc::new(List::from_opt(&opt.allow_list).await.map_err(|err| {
|
|
||||||
error!("Can't parse whitelist: `{err}`");
|
|
||||||
SocksError::ArgumentInputError("Can't parse whitelist")
|
|
||||||
})?);
|
|
||||||
|
|
||||||
totals.set_entries(list.entries() as u64);
|
|
||||||
|
|
||||||
let listener = TcpListener::bind(&opt.listen_addr).await?;
|
let listener = TcpListener::bind(&opt.listen_addr).await?;
|
||||||
|
|
||||||
info!("Listen for socks connections @ {}", &opt.listen_addr);
|
info!("Listen for socks connections @ {}", &opt.listen_addr);
|
||||||
|
|
@ -107,7 +129,7 @@ async fn serve_socks5(
|
||||||
let (host, _) = request.2.clone().into_string_and_port(); // @TODO ref
|
let (host, _) = request.2.clone().into_string_and_port(); // @TODO ref
|
||||||
let (proto, cmd, addr) = request.resolve_dns().await?;
|
let (proto, cmd, addr) = request.resolve_dns().await?;
|
||||||
|
|
||||||
if !list.has(&host) && !list.has(&addr.to_string()) {
|
if !list.any(&[&host, &addr.to_string()]).await {
|
||||||
totals.increase_blocked();
|
totals.increase_blocked();
|
||||||
info!("Blocked connection attempt to: {host}");
|
info!("Blocked connection attempt to: {host}");
|
||||||
proto.reply_error(&ReplyError::ConnectionNotAllowed).await?;
|
proto.reply_error(&ReplyError::ConnectionNotAllowed).await?;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ pub struct Total {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Total {
|
impl Total {
|
||||||
|
pub fn with_entries(entries: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: entries.into(),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn snapshot(&self) -> Snapshot {
|
pub fn snapshot(&self) -> Snapshot {
|
||||||
Snapshot {
|
Snapshot {
|
||||||
request: self.request.load(Ordering::Relaxed),
|
request: self.request.load(Ordering::Relaxed),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue