feat: unlimited free core and license protection for panel API

This commit is contained in:
ospab 2026-06-17 19:32:59 +03:00
parent 303515cfba
commit 99ff76d595
3 changed files with 31 additions and 18 deletions

View File

@ -54,6 +54,7 @@ pub struct ApiState {
pub dns_server: std::sync::Arc<crate::dns::DnsServer>,
pub audit_logs: Arc<RwLock<Vec<AuditLogEntry>>>,
pub router: std::sync::Arc<crate::router::Router>,
pub is_licensed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -208,7 +209,8 @@ pub fn create_api_router(state: ApiState) -> Router {
.delete(handle_clear_audit),
)
.route("/users/bulk", post(handle_bulk_create_users))
.route("/router/rules", get(handle_get_rules).put(handle_put_rules));
.route("/router/rules", get(handle_get_rules).put(handle_put_rules))
.layer(axum::middleware::from_fn_with_state(state.clone(), license_middleware));
let webpath = state.webpath.clone();
let webpath = webpath.trim_matches('/');
@ -236,6 +238,25 @@ pub fn create_api_router(state: ApiState) -> Router {
.layer(cors)
.with_state(state)
}
async fn license_middleware(
axum::extract::State(state): axum::extract::State<ApiState>,
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
if state.is_licensed {
return next.run(req).await;
}
let path = req.uri().path();
// Allow read-only access to users for relay, and server status
if (path == "/server/status" && req.method() == axum::http::Method::GET) ||
(path == "/users" && req.method() == axum::http::Method::GET)
{
return next.run(req).await;
}
(axum::http::StatusCode::PAYMENT_REQUIRED, "This feature requires an active OSTP license. Get yours at https://ostp.ospab.lol").into_response()
}
/// Start the Management API server on the configured bind address.
pub async fn start_api_server(
@ -247,6 +268,7 @@ pub async fn start_api_server(
config_path: Option<std::path::PathBuf>,
dns_server: std::sync::Arc<crate::dns::DnsServer>,
router: std::sync::Arc<crate::router::Router>,
is_licensed: bool,
) {
let state = ApiState {
access_keys,
@ -263,6 +285,7 @@ pub async fn start_api_server(
dns_server,
audit_logs: Arc::new(RwLock::new(Vec::new())),
router,
is_licensed,
};
let app = create_api_router(state);

View File

@ -81,12 +81,11 @@ pub struct Dispatcher {
replay_cache: std::collections::HashMap<Vec<u8>, u64>,
roaming_tokens: f64,
last_token_regen: std::time::Instant,
max_sessions: Option<usize>,
}
#[allow(dead_code)]
impl Dispatcher {
pub fn new(machine_config: ProtocolConfig, access_keys: Arc<RwLock<HashMap<String, crate::api::UserMeta>>>, max_sessions: Option<usize>) -> Self {
pub fn new(machine_config: ProtocolConfig, access_keys: Arc<RwLock<HashMap<String, crate::api::UserMeta>>>) -> Self {
let mut initial_stats = HashMap::new();
for (key, meta) in access_keys.read().unwrap_or_else(|e| e.into_inner()).iter() {
initial_stats.insert(key.clone(), Arc::new(UserStats::new(meta.limit_bytes)));
@ -100,7 +99,6 @@ impl Dispatcher {
replay_cache: std::collections::HashMap::new(),
roaming_tokens: 50.0,
last_token_regen: std::time::Instant::now(),
max_sessions,
}
}
@ -373,11 +371,6 @@ impl Dispatcher {
tracing::warn!("Replay cache full (100000 entries), rejecting handshake from {}", peer);
return Ok(DispatchOutcome::Unauthorized);
}
let limit = self.max_sessions.unwrap_or(30);
if self.peer_machines.len() >= limit {
tracing::warn!("drop session by {}, for more active clients buy our license here: https://ostp.ospab.lol/license", peer.ip());
return Ok(DispatchOutcome::Unauthorized);
}
self.replay_cache.insert(payload.to_vec(), ts);

View File

@ -117,16 +117,15 @@ pub async fn run_server(
mtu: 1350,
};
let mut max_sessions = Some(30);
let mut is_licensed = false;
if let Some(key) = license_key {
let host = server_public_ip.as_deref().unwrap_or("0.0.0.0");
match crate::license::verify_license(&key, host) {
Ok(payload) => {
tracing::info!("License verified successfully! Features: {:?}", payload.features);
if payload.features.contains(&"unlimited_connections".to_string()) {
max_sessions = None;
tracing::info!("Unlimited connections enabled.");
}
is_licensed = true;
if payload.features.contains(&"control_panel".to_string()) {
tracing::info!("Spawning control panel child process...");
@ -152,11 +151,9 @@ pub async fn run_server(
tracing::error!("Failed to verify license: {:?}", e);
}
}
} else {
tracing::info!("No license key provided. Free version limited to 30 sessions.");
}
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone(), max_sessions);
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone());
// Background config hot-reloader for access keys
let shared_keys_clone = shared_keys.clone();
@ -307,7 +304,7 @@ pub async fn run_server(
let dns_server_api = dns_server.clone();
let router_api = router.clone();
tokio::spawn(async move {
api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, config_path_api, dns_server_api, router_api).await;
api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, config_path_api, dns_server_api, router_api, is_licensed).await;
});
}
}