mirror of
https://codeberg.org/YGGverse/psocks.git
synced 2026-04-02 01:15:28 +00:00
implement web-api server with stats
This commit is contained in:
parent
ddd7120f61
commit
126185480f
7 changed files with 952 additions and 44 deletions
897
Cargo.lock
generated
897
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "psocks"
|
name = "psocks"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
description = "Experimental async SOCKS5 (TCP/UDP) proxy server based on fast-socks5, featuring allowlist-based access control (drop everything but allowed by user)"
|
description = "Experimental async SOCKS5 (TCP/UDP) proxy server based on fast-socks5, featuring allowlist-based access control (drop everything but allowed by user) with JSON/API based on Rocket framework"
|
||||||
keywords = ["proxy", "whitelist", "privacy"]
|
keywords = ["proxy", "whitelist", "privacy"]
|
||||||
categories = ["network-programming"]
|
categories = ["network-programming"]
|
||||||
repository = "https://codeberg.org/postscriptum/psocks"
|
repository = "https://codeberg.org/postscriptum/psocks"
|
||||||
|
|
@ -15,5 +15,7 @@ env_logger = "0.11.9"
|
||||||
fast-socks5 = "1.0.0"
|
fast-socks5 = "1.0.0"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
reqwest = "0.13.2"
|
reqwest = "0.13.2"
|
||||||
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
structopt = "0.3.26"
|
structopt = "0.3.26"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -1,22 +1,24 @@
|
||||||
# psocks
|
# psocks
|
||||||
|
|
||||||
Experimental async SOCKS5 (TCP/UDP) proxy server based on [fast-socks5](https://github.com/dizda/fast-socks5/blob/master/examples/server.rs), featuring allowlist-based access control (drop everything but allowed by user)
|
Experimental async SOCKS5 (TCP/UDP) proxy server based on [fast-socks5](https://github.com/dizda/fast-socks5/blob/master/examples/server.rs), featuring allowlist-based access control (drop everything but allowed by user) with JSON/API based on [Rocket](https://rocket.rs) framework.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
* [ ] Range support
|
* [ ] Range support
|
||||||
* [ ] Local Web-API
|
* [ ] Local Web-API
|
||||||
* [ ] Block stats
|
* [x] Block stats
|
||||||
* [ ] In-memory list update (without server restart)
|
* [ ] In-memory list update (without server restart)
|
||||||
* [ ] Performance optimization
|
* [ ] Performance optimization
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
RUST_LOG=trace cargo run -- -a=/path/to/allow1.txt \
|
RUST_LOG=psocks=trace cargo run -- -a=/path/to/allow1.txt \
|
||||||
-a=http://localhost/allow2.txt \
|
-a=http://localhost/allow2.txt \
|
||||||
no-auth
|
no-auth
|
||||||
```
|
```
|
||||||
|
* set `socks5://127.0.0.1:1080` proxy in your application
|
||||||
|
* open http://127.0.0.1:8010 in browser for stats & control API
|
||||||
|
|
||||||
### Allow list example
|
### Allow list example
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,4 +48,9 @@ impl List {
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn entries(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Allow(list) => list.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
src/main.rs
57
src/main.rs
|
|
@ -1,28 +1,56 @@
|
||||||
mod list;
|
mod list;
|
||||||
mod opt;
|
mod opt;
|
||||||
|
mod stats;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use fast_socks5::{
|
use fast_socks5::{
|
||||||
ReplyError, Result, Socks5Command, SocksError,
|
ReplyError, Result, Socks5Command, SocksError,
|
||||||
server::{DnsResolveHelper as _, Socks5ServerProtocol, run_tcp_proxy, run_udp_proxy},
|
server::{DnsResolveHelper as _, Socks5ServerProtocol, run_tcp_proxy, run_udp_proxy},
|
||||||
};
|
};
|
||||||
|
use list::List;
|
||||||
use log::*;
|
use log::*;
|
||||||
use opt::{AuthMode, Opt};
|
use opt::{AuthMode, Opt};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use stats::Stats;
|
||||||
use std::{future::Future, sync::Arc};
|
use std::{future::Future, sync::Arc};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use tokio::net::TcpListener;
|
use tokio::{net::TcpListener, sync::RwLock, task};
|
||||||
use tokio::task;
|
|
||||||
|
|
||||||
use crate::list::List;
|
type SharedStats = Arc<RwLock<Stats>>;
|
||||||
|
type SharedList = Arc<List>;
|
||||||
|
|
||||||
#[tokio::main]
|
#[rocket::get("/")]
|
||||||
async fn main() -> Result<()> {
|
async fn index(stats: &rocket::State<SharedStats>) -> Json<Stats> {
|
||||||
env_logger::init();
|
Json(*stats.read().await)
|
||||||
spawn_socks_server().await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_socks_server() -> Result<()> {
|
#[rocket::launch]
|
||||||
|
async fn rocket() -> _ {
|
||||||
|
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 stats = Arc::new(RwLock::new(Stats::default()));
|
||||||
|
|
||||||
|
tokio::spawn({
|
||||||
|
let socks_stats = stats.clone();
|
||||||
|
async move {
|
||||||
|
if let Err(err) = spawn_socks_server(opt, socks_stats).await {
|
||||||
|
error!("SOCKS server failed: `{err}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rocket::build()
|
||||||
|
.configure(rocket::Config {
|
||||||
|
port: opt.api_addr.port(),
|
||||||
|
address: opt.api_addr.ip(),
|
||||||
|
..rocket::Config::release_default()
|
||||||
|
})
|
||||||
|
.manage(stats)
|
||||||
|
.mount("/", rocket::routes![index])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_socks_server(opt: &'static Opt, stats: SharedStats) -> 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",
|
||||||
|
|
@ -38,6 +66,10 @@ async fn spawn_socks_server() -> Result<()> {
|
||||||
error!("Can't parse whitelist: `{err}`");
|
error!("Can't parse whitelist: `{err}`");
|
||||||
SocksError::ArgumentInputError("Can't parse whitelist")
|
SocksError::ArgumentInputError("Can't parse whitelist")
|
||||||
})?);
|
})?);
|
||||||
|
{
|
||||||
|
let mut s = stats.write().await;
|
||||||
|
s.entries = list.entries();
|
||||||
|
}
|
||||||
|
|
||||||
let listener = TcpListener::bind(&opt.listen_addr).await?;
|
let listener = TcpListener::bind(&opt.listen_addr).await?;
|
||||||
|
|
||||||
|
|
@ -46,7 +78,7 @@ async fn spawn_socks_server() -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
Ok((socket, _client_addr)) => {
|
Ok((socket, _client_addr)) => {
|
||||||
spawn_and_log_error(serve_socks5(opt, socket, list.clone()));
|
spawn_and_log_error(serve_socks5(opt, socket, list.clone(), stats.clone()));
|
||||||
}
|
}
|
||||||
Err(err) => error!("accept error = {:?}", err),
|
Err(err) => error!("accept error = {:?}", err),
|
||||||
}
|
}
|
||||||
|
|
@ -56,8 +88,12 @@ async fn spawn_socks_server() -> Result<()> {
|
||||||
async fn serve_socks5(
|
async fn serve_socks5(
|
||||||
opt: &Opt,
|
opt: &Opt,
|
||||||
socket: tokio::net::TcpStream,
|
socket: tokio::net::TcpStream,
|
||||||
list: Arc<List>,
|
list: SharedList,
|
||||||
|
stats: SharedStats,
|
||||||
) -> Result<(), SocksError> {
|
) -> Result<(), SocksError> {
|
||||||
|
let mut s = stats.write().await;
|
||||||
|
s.request += 1;
|
||||||
|
|
||||||
let request = match &opt.auth {
|
let request = match &opt.auth {
|
||||||
AuthMode::NoAuth if opt.skip_auth => {
|
AuthMode::NoAuth if opt.skip_auth => {
|
||||||
Socks5ServerProtocol::skip_auth_this_is_not_rfc_compliant(socket)
|
Socks5ServerProtocol::skip_auth_this_is_not_rfc_compliant(socket)
|
||||||
|
|
@ -78,6 +114,7 @@ async fn serve_socks5(
|
||||||
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.has(&host) && !list.has(&addr.to_string()) {
|
||||||
|
s.blocked += 1;
|
||||||
info!("Blocked connection attempt to: {host}");
|
info!("Blocked connection attempt to: {host}");
|
||||||
proto.reply_error(&ReplyError::ConnectionNotAllowed).await?;
|
proto.reply_error(&ReplyError::ConnectionNotAllowed).await?;
|
||||||
return Err(ReplyError::ConnectionNotAllowed.into());
|
return Err(ReplyError::ConnectionNotAllowed.into());
|
||||||
|
|
|
||||||
10
src/opt.rs
10
src/opt.rs
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{num::ParseFloatError, time::Duration};
|
use std::{net::SocketAddr, num::ParseFloatError, time::Duration};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
/// # How to use it:
|
/// # How to use it:
|
||||||
|
|
@ -18,8 +18,12 @@ use structopt::StructOpt;
|
||||||
)]
|
)]
|
||||||
pub struct Opt {
|
pub struct Opt {
|
||||||
/// Bind on address address. eg. `127.0.0.1:1080`
|
/// Bind on address address. eg. `127.0.0.1:1080`
|
||||||
#[structopt(short, long, default_value = "127.0.0.1:1080")]
|
#[structopt(short = "l", long, default_value = "127.0.0.1:1080")]
|
||||||
pub listen_addr: String,
|
pub listen_addr: SocketAddr,
|
||||||
|
|
||||||
|
/// Bind on address address. eg. `127.0.0.1:8010`
|
||||||
|
#[structopt(short = "A", long, default_value = "127.0.0.1:8010")]
|
||||||
|
pub api_addr: SocketAddr,
|
||||||
|
|
||||||
/// Our external IP address to be sent in reply packets (required for UDP)
|
/// Our external IP address to be sent in reply packets (required for UDP)
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
|
|
|
||||||
9
src/stats.rs
Normal file
9
src/stats.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
use rocket::serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct Stats {
|
||||||
|
pub entries: usize,
|
||||||
|
pub blocked: usize,
|
||||||
|
pub request: usize,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue