mirror of https://github.com/ospab/ostp.git
Implement config management API, token generation, and update wiki
This commit is contained in:
parent
951e597d46
commit
cba7be4b75
|
|
@ -32,5 +32,5 @@ wintun.dll
|
||||||
# Dev notes (not for repo)
|
# Dev notes (not for repo)
|
||||||
.ai-rules.md
|
.ai-rules.md
|
||||||
turn-harvesting-idea.md
|
turn-harvesting-idea.md
|
||||||
ostp-wiki/
|
ostp-control/
|
||||||
ostp-flutter/
|
ostp-flutter/
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
127.0.0.1
|
|
||||||
|
|
@ -1097,6 +1097,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hmac",
|
"hmac",
|
||||||
|
"json_comments",
|
||||||
"ostp-core",
|
"ostp-core",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,16 @@ async fn start_tun_via_helper(
|
||||||
guard: &mut AppStateInner,
|
guard: &mut AppStateInner,
|
||||||
raw: &ClientConfigRaw,
|
raw: &ClientConfigRaw,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Kill any existing helper processes to prevent os error 10048 (port already in use)
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
let _ = std::process::Command::new("taskkill")
|
||||||
|
.args(["/F", "/IM", "ostp-tun-helper.exe"])
|
||||||
|
.creation_flags(0x08000000)
|
||||||
|
.output();
|
||||||
|
}
|
||||||
|
|
||||||
let helper_exe = find_helper_exe().ok_or_else(|| "ostp-tun-helper.exe not found.".to_string())?;
|
let helper_exe = find_helper_exe().ok_or_else(|| "ostp-tun-helper.exe not found.".to_string())?;
|
||||||
launch_as_admin(&helper_exe).map_err(|e| format!("Failed to launch helper: {}", e))?;
|
launch_as_admin(&helper_exe).map_err(|e| format!("Failed to launch helper: {}", e))?;
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ tracing.workspace = true
|
||||||
ostp-core = { path = "../ostp-core" }
|
ostp-core = { path = "../ostp-core" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
json_comments = "0.2"
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
socket2 = "0.6.3"
|
socket2 = "0.6.3"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ use crate::dispatcher::{UserStats, UserStatsSnapshot};
|
||||||
/// API server shared state. Held behind Arc for axum handlers.
|
/// API server shared state. Held behind Arc for axum handlers.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ApiState {
|
pub struct ApiState {
|
||||||
pub access_keys: Arc<RwLock<HashMap<String, ()>>>,
|
pub access_keys: Arc<RwLock<HashMap<String, UserMeta>>>,
|
||||||
pub user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
pub user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
||||||
pub start_time: Instant,
|
pub start_time: Instant,
|
||||||
pub api_token: String,
|
pub api_token: String,
|
||||||
|
|
@ -44,6 +44,7 @@ pub struct ApiState {
|
||||||
pub server_host: String,
|
pub server_host: String,
|
||||||
pub server_port: u16,
|
pub server_port: u16,
|
||||||
pub reality_query: String,
|
pub reality_query: String,
|
||||||
|
pub config_path: Option<std::path::PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API configuration ────────────────────────────────────────────────────────
|
// ── API configuration ────────────────────────────────────────────────────────
|
||||||
|
|
@ -65,6 +66,12 @@ impl Default for ApiConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct UserMeta {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub limit_bytes: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Request/Response types ───────────────────────────────────────────────────
|
// ── Request/Response types ───────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -78,6 +85,13 @@ struct ServerStatus {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateUserRequest {
|
pub struct CreateUserRequest {
|
||||||
pub access_key: Option<String>,
|
pub access_key: Option<String>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub limit_bytes: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateUserRequest {
|
||||||
|
pub name: Option<String>,
|
||||||
pub limit_bytes: Option<u64>,
|
pub limit_bytes: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,11 +133,13 @@ pub fn create_api_router(state: ApiState) -> Router {
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/server/status", get(handle_status))
|
.route("/api/server/status", get(handle_status))
|
||||||
|
.route("/api/server/config", get(handle_get_config).put(handle_put_config))
|
||||||
.route("/api/users", get(handle_list_users))
|
.route("/api/users", get(handle_list_users))
|
||||||
.route("/api/users", post(handle_create_user))
|
.route("/api/users", post(handle_create_user))
|
||||||
.route("/api/users/{key}", get(handle_get_user))
|
.route("/api/users/:key", get(handle_get_user))
|
||||||
.route("/api/users/{key}", delete(handle_delete_user))
|
.route("/api/users/:key", delete(delete_user))
|
||||||
.route("/api/users/{key}/limit", put(handle_set_limit))
|
.route("/api/users/:key", put(update_user))
|
||||||
|
.route("/api/users/:key/limit", put(handle_set_limit))
|
||||||
.route("/api/users/{key}/reset", post(handle_reset_stats))
|
.route("/api/users/{key}/reset", post(handle_reset_stats))
|
||||||
.route("/api/subscribe/{key}", get(handle_subscribe))
|
.route("/api/subscribe/{key}", get(handle_subscribe))
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
|
|
@ -133,11 +149,12 @@ pub fn create_api_router(state: ApiState) -> Router {
|
||||||
/// Start the Management API server on the configured bind address.
|
/// Start the Management API server on the configured bind address.
|
||||||
pub async fn start_api_server(
|
pub async fn start_api_server(
|
||||||
config: ApiConfig,
|
config: ApiConfig,
|
||||||
access_keys: Arc<RwLock<HashMap<String, ()>>>,
|
access_keys: Arc<RwLock<HashMap<String, UserMeta>>>,
|
||||||
user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
||||||
server_host: String,
|
server_host: String,
|
||||||
server_port: u16,
|
server_port: u16,
|
||||||
reality_query: String,
|
reality_query: String,
|
||||||
|
config_path: Option<std::path::PathBuf>,
|
||||||
) {
|
) {
|
||||||
let state = ApiState {
|
let state = ApiState {
|
||||||
access_keys,
|
access_keys,
|
||||||
|
|
@ -147,6 +164,7 @@ pub async fn start_api_server(
|
||||||
server_host,
|
server_host,
|
||||||
server_port,
|
server_port,
|
||||||
reality_query,
|
reality_query,
|
||||||
|
config_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = create_api_router(state);
|
let app = create_api_router(state);
|
||||||
|
|
@ -181,8 +199,116 @@ fn check_token(state: &ApiState, headers: &axum::http::HeaderMap) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save_config_keys(state: &ApiState) -> Result<(), String> {
|
||||||
|
let Some(ref path) = state.config_path else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(path)
|
||||||
|
.map_err(|e| format!("failed to read config file: {}", e))?;
|
||||||
|
|
||||||
|
let mut stripped = json_comments::StripComments::new(content.as_bytes());
|
||||||
|
let mut content_str = String::new();
|
||||||
|
use std::io::Read;
|
||||||
|
stripped.read_to_string(&mut content_str)
|
||||||
|
.map_err(|e| format!("failed to strip comments from config: {}", e))?;
|
||||||
|
|
||||||
|
let mut json_val: serde_json::Value = serde_json::from_str(&content_str)
|
||||||
|
.map_err(|e| format!("failed to parse config JSON: {}", e))?;
|
||||||
|
|
||||||
|
let keys = state.access_keys.read().unwrap();
|
||||||
|
let mut access_keys_json = Vec::new();
|
||||||
|
for (k, m) in keys.iter() {
|
||||||
|
if m.name.is_none() && m.limit_bytes.is_none() {
|
||||||
|
access_keys_json.push(serde_json::Value::String(k.clone()));
|
||||||
|
} else {
|
||||||
|
let mut obj = serde_json::Map::new();
|
||||||
|
obj.insert("access_key".to_string(), serde_json::Value::String(k.clone()));
|
||||||
|
if let Some(ref name) = m.name {
|
||||||
|
obj.insert("name".to_string(), serde_json::Value::String(name.clone()));
|
||||||
|
}
|
||||||
|
if let Some(limit) = m.limit_bytes {
|
||||||
|
obj.insert("limit_bytes".to_string(), serde_json::Value::Number(limit.into()));
|
||||||
|
}
|
||||||
|
access_keys_json.push(serde_json::Value::Object(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(obj) = json_val.as_object_mut() {
|
||||||
|
obj.insert("access_keys".to_string(), serde_json::Value::Array(access_keys_json));
|
||||||
|
} else {
|
||||||
|
return Err("config root is not an object".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_content = serde_json::to_string_pretty(&json_val)
|
||||||
|
.map_err(|e| format!("failed to serialize config JSON: {}", e))?;
|
||||||
|
|
||||||
|
std::fs::write(path, new_content)
|
||||||
|
.map_err(|e| format!("failed to write config file: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// ── Handlers ─────────────────────────────────────────────────────────────────
|
// ── Handlers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn handle_get_config(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !check_token(&state, &headers) {
|
||||||
|
return api_unauthorized::<serde_json::Value>();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(ref path) = state.config_path else {
|
||||||
|
return api_error("No config path registered (run-time only)");
|
||||||
|
};
|
||||||
|
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(content) => {
|
||||||
|
let mut stripped = json_comments::StripComments::new(content.as_bytes());
|
||||||
|
let mut content_str = String::new();
|
||||||
|
use std::io::Read;
|
||||||
|
if let Err(e) = stripped.read_to_string(&mut content_str) {
|
||||||
|
return api_error(&format!("Failed to strip comments: {}", e));
|
||||||
|
}
|
||||||
|
match serde_json::from_str::<serde_json::Value>(&content_str) {
|
||||||
|
Ok(val) => (StatusCode::OK, ApiResponse::success(val)),
|
||||||
|
Err(e) => api_error(&format!("Failed to parse config JSON: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => api_error(&format!("Failed to read config file: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_put_config(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Json(body): Json<serde_json::Value>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !check_token(&state, &headers) {
|
||||||
|
return api_unauthorized::<bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(ref path) = state.config_path else {
|
||||||
|
return api_error("No config path registered (run-time only)");
|
||||||
|
};
|
||||||
|
|
||||||
|
if body.get("mode").is_none() {
|
||||||
|
return api_error("Invalid config: missing 'mode' field");
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_content = match serde_json::to_string_pretty(&body) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return api_error(&format!("Failed to format config JSON: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = std::fs::write(path, new_content) {
|
||||||
|
return api_error(&format!("Failed to write config file: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
(StatusCode::OK, ApiResponse::success(true))
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_status(
|
async fn handle_status(
|
||||||
State(state): State<ApiState>,
|
State(state): State<ApiState>,
|
||||||
headers: axum::http::HeaderMap,
|
headers: axum::http::HeaderMap,
|
||||||
|
|
@ -304,39 +430,91 @@ async fn handle_create_user(
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut keys = state.access_keys.write().unwrap();
|
let mut keys = state.access_keys.write().unwrap();
|
||||||
keys.insert(key.clone(), ());
|
keys.insert(key.clone(), UserMeta { name: body.name.clone(), limit_bytes: body.limit_bytes });
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(limit) = body.limit_bytes {
|
|
||||||
let mut stats = state.user_stats.write().unwrap();
|
let mut stats = state.user_stats.write().unwrap();
|
||||||
stats.insert(key.clone(), Arc::new(UserStats::new(Some(limit))));
|
stats.insert(key.clone(), Arc::new(UserStats::new(body.limit_bytes)));
|
||||||
|
drop(stats);
|
||||||
|
|
||||||
|
if let Err(e) = save_config_keys(&state) {
|
||||||
|
tracing::error!("Failed to save config after creating user: {}", e);
|
||||||
|
return api_error::<String>("failed to save configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("API: created user key {}", &key[..8.min(key.len())]);
|
tracing::info!("API: created user key {}", &key[..8.min(key.len())]);
|
||||||
(StatusCode::OK, ApiResponse::success(key))
|
(StatusCode::OK, ApiResponse::success(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_delete_user(
|
async fn delete_user(
|
||||||
State(state): State<ApiState>,
|
State(state): State<ApiState>,
|
||||||
headers: axum::http::HeaderMap,
|
|
||||||
Path(key): Path<String>,
|
Path(key): Path<String>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if !check_token(&state, &headers) {
|
if !check_token(&state, &headers) {
|
||||||
return api_unauthorized::<bool>();
|
return api_unauthorized::<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
let removed = {
|
{
|
||||||
let mut keys = state.access_keys.write().unwrap();
|
let mut keys = state.access_keys.write().unwrap();
|
||||||
keys.remove(&key).is_some()
|
if keys.remove(&key).is_none() {
|
||||||
};
|
return api_error::<String>("User not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if removed {
|
{
|
||||||
let mut stats = state.user_stats.write().unwrap();
|
let mut stats = state.user_stats.write().unwrap();
|
||||||
stats.remove(&key);
|
stats.remove(&key);
|
||||||
tracing::info!("API: deleted user key {}", &key[..8.min(key.len())]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(StatusCode::OK, ApiResponse::success(removed))
|
if let Err(e) = save_config_keys(&state) {
|
||||||
|
tracing::error!("Failed to save config after removing user: {}", e);
|
||||||
|
return api_error::<String>("failed to save configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
(StatusCode::OK, ApiResponse::success("User removed".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_user(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Path(key): Path<String>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Json(body): Json<UpdateUserRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !check_token(&state, &headers) {
|
||||||
|
return api_unauthorized::<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut keys = state.access_keys.write().unwrap();
|
||||||
|
if let Some(meta) = keys.get_mut(&key) {
|
||||||
|
meta.name = body.name.clone();
|
||||||
|
meta.limit_bytes = body.limit_bytes;
|
||||||
|
} else {
|
||||||
|
return api_error::<String>("User not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut stats = state.user_stats.write().unwrap();
|
||||||
|
let entry = stats.entry(key.clone())
|
||||||
|
.or_insert_with(|| Arc::new(UserStats::new(body.limit_bytes)));
|
||||||
|
|
||||||
|
*entry = Arc::new(UserStats {
|
||||||
|
bytes_up: AtomicU64::new(entry.bytes_up.load(Ordering::Relaxed)),
|
||||||
|
bytes_down: AtomicU64::new(entry.bytes_down.load(Ordering::Relaxed)),
|
||||||
|
connections: AtomicU64::new(entry.connections.load(Ordering::Relaxed)),
|
||||||
|
limit_bytes: body.limit_bytes,
|
||||||
|
created_at: entry.created_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = save_config_keys(&state) {
|
||||||
|
tracing::error!("Failed to save config after updating user: {}", e);
|
||||||
|
return api_error::<String>("failed to save configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
(StatusCode::OK, ApiResponse::success("User updated".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_set_limit(
|
async fn handle_set_limit(
|
||||||
|
|
@ -349,11 +527,14 @@ async fn handle_set_limit(
|
||||||
return api_unauthorized::<bool>();
|
return api_unauthorized::<bool>();
|
||||||
}
|
}
|
||||||
|
|
||||||
let keys = state.access_keys.read().unwrap();
|
{
|
||||||
if !keys.contains_key(&key) {
|
let mut keys = state.access_keys.write().unwrap();
|
||||||
|
if let Some(meta) = keys.get_mut(&key) {
|
||||||
|
meta.limit_bytes = body.limit_bytes;
|
||||||
|
} else {
|
||||||
return api_error("user not found");
|
return api_error("user not found");
|
||||||
}
|
}
|
||||||
drop(keys);
|
}
|
||||||
|
|
||||||
let mut stats = state.user_stats.write().unwrap();
|
let mut stats = state.user_stats.write().unwrap();
|
||||||
let entry = stats.entry(key.clone())
|
let entry = stats.entry(key.clone())
|
||||||
|
|
@ -366,6 +547,12 @@ async fn handle_set_limit(
|
||||||
limit_bytes: body.limit_bytes,
|
limit_bytes: body.limit_bytes,
|
||||||
created_at: entry.created_at,
|
created_at: entry.created_at,
|
||||||
});
|
});
|
||||||
|
drop(stats);
|
||||||
|
|
||||||
|
if let Err(e) = save_config_keys(&state) {
|
||||||
|
tracing::error!("Failed to save config after setting user limit: {}", e);
|
||||||
|
return api_error("failed to save configuration");
|
||||||
|
}
|
||||||
|
|
||||||
(StatusCode::OK, ApiResponse::success(true))
|
(StatusCode::OK, ApiResponse::success(true))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ pub struct Dispatcher {
|
||||||
peer_machines: HashMap<u32, PeerState>,
|
peer_machines: HashMap<u32, PeerState>,
|
||||||
addr_to_session: HashMap<SocketAddr, u32>,
|
addr_to_session: HashMap<SocketAddr, u32>,
|
||||||
machine_config: ProtocolConfig,
|
machine_config: ProtocolConfig,
|
||||||
access_keys: Arc<RwLock<HashMap<String, ()>>>,
|
access_keys: Arc<RwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||||
user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
||||||
replay_cache: std::collections::HashMap<Vec<u8>, u64>,
|
replay_cache: std::collections::HashMap<Vec<u8>, u64>,
|
||||||
roaming_tokens: f64,
|
roaming_tokens: f64,
|
||||||
|
|
@ -83,13 +83,17 @@ pub struct Dispatcher {
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl Dispatcher {
|
impl Dispatcher {
|
||||||
pub fn new(machine_config: ProtocolConfig, access_keys: Arc<RwLock<HashMap<String, ()>>>) -> 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().iter() {
|
||||||
|
initial_stats.insert(key.clone(), Arc::new(UserStats::new(meta.limit_bytes)));
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
peer_machines: HashMap::new(),
|
peer_machines: HashMap::new(),
|
||||||
addr_to_session: HashMap::new(),
|
addr_to_session: HashMap::new(),
|
||||||
machine_config,
|
machine_config,
|
||||||
access_keys,
|
access_keys,
|
||||||
user_stats: Arc::new(RwLock::new(HashMap::new())),
|
user_stats: Arc::new(RwLock::new(initial_stats)),
|
||||||
replay_cache: std::collections::HashMap::new(),
|
replay_cache: std::collections::HashMap::new(),
|
||||||
roaming_tokens: 50.0,
|
roaming_tokens: 50.0,
|
||||||
last_token_regen: std::time::Instant::now(),
|
last_token_regen: std::time::Instant::now(),
|
||||||
|
|
@ -124,9 +128,12 @@ impl Dispatcher {
|
||||||
return existing.clone();
|
return existing.clone();
|
||||||
}
|
}
|
||||||
drop(stats);
|
drop(stats);
|
||||||
|
|
||||||
|
let limit_bytes = self.access_keys.read().unwrap().get(key).and_then(|m| m.limit_bytes);
|
||||||
|
|
||||||
let mut stats = self.user_stats.write().unwrap();
|
let mut stats = self.user_stats.write().unwrap();
|
||||||
stats.entry(key.to_string())
|
stats.entry(key.to_string())
|
||||||
.or_insert_with(|| Arc::new(UserStats::new(None)))
|
.or_insert_with(|| Arc::new(UserStats::new(limit_bytes)))
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +207,21 @@ impl Dispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(session_id) = session_id_opt {
|
if let Some(session_id) = session_id_opt {
|
||||||
|
let key_opt = self.peer_machines.get(&session_id).map(|ps| ps.access_key.clone());
|
||||||
|
if let Some(access_key) = key_opt {
|
||||||
|
// Check if key is still valid and not over limit
|
||||||
|
let key_valid = self.access_keys.read().unwrap().contains_key(&access_key);
|
||||||
|
let user_stats = self.get_or_create_user_stats(&access_key);
|
||||||
|
if !key_valid || user_stats.is_over_limit() {
|
||||||
|
tracing::info!("Dropping session {} for key {} (valid={}, over_limit={})",
|
||||||
|
session_id, access_key, key_valid, user_stats.is_over_limit());
|
||||||
|
self.drop_session(session_id);
|
||||||
|
return Ok(DispatchOutcome::Unauthorized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(peer_state) = self.peer_machines.get_mut(&session_id) {
|
if let Some(peer_state) = self.peer_machines.get_mut(&session_id) {
|
||||||
|
|
||||||
if peer_state.last_addr != peer {
|
if peer_state.last_addr != peer {
|
||||||
tracing::info!("Client roamed: session {} from {} to {}", session_id, peer_state.last_addr, peer);
|
tracing::info!("Client roamed: session {} from {} to {}", session_id, peer_state.last_addr, peer);
|
||||||
self.addr_to_session.remove(&peer_state.last_addr);
|
self.addr_to_session.remove(&peer_state.last_addr);
|
||||||
|
|
@ -211,7 +232,7 @@ impl Dispatcher {
|
||||||
|
|
||||||
// Track inbound bytes per user
|
// Track inbound bytes per user
|
||||||
let key = peer_state.access_key.clone();
|
let key = peer_state.access_key.clone();
|
||||||
track_user_bytes_up(&self.user_stats, &key, packet.len() as u64);
|
track_user_bytes_up(&self.user_stats, &self.access_keys, &key, packet.len() as u64);
|
||||||
|
|
||||||
let action = match peer_state.machine.on_event(OstpEvent::Inbound(packet)) {
|
let action = match peer_state.machine.on_event(OstpEvent::Inbound(packet)) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
|
|
@ -385,7 +406,7 @@ impl Dispatcher {
|
||||||
match peer_state.machine.on_event(OstpEvent::Outbound(stream_id, payload))? {
|
match peer_state.machine.on_event(OstpEvent::Outbound(stream_id, payload))? {
|
||||||
ProtocolAction::SendDatagram(frame) => {
|
ProtocolAction::SendDatagram(frame) => {
|
||||||
// Track outbound bytes per user
|
// Track outbound bytes per user
|
||||||
track_user_bytes_down(&self.user_stats, &key, frame.len() as u64);
|
track_user_bytes_down(&self.user_stats, &self.access_keys, &key, frame.len() as u64);
|
||||||
Ok(Some((frame, addr)))
|
Ok(Some((frame, addr)))
|
||||||
}
|
}
|
||||||
_ => Ok(None),
|
_ => Ok(None),
|
||||||
|
|
@ -405,16 +426,34 @@ impl Dispatcher {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
let timeout_dur = std::time::Duration::from_secs(300); // 5 minutes session timeout
|
let timeout_dur = std::time::Duration::from_secs(300); // 5 minutes session timeout
|
||||||
|
|
||||||
// Gather expired sessions
|
// Gather expired or invalid sessions
|
||||||
for (&sid, peer_state) in &self.peer_machines {
|
for (&sid, peer_state) in &self.peer_machines {
|
||||||
if now.duration_since(peer_state.last_seen) > timeout_dur {
|
let key_valid = self.access_keys.read().unwrap().contains_key(&peer_state.access_key);
|
||||||
|
let user_stats = self.get_or_create_user_stats(&peer_state.access_key);
|
||||||
|
if now.duration_since(peer_state.last_seen) > timeout_dur || !key_valid || user_stats.is_over_limit() {
|
||||||
expired.push(sid);
|
expired.push(sid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear expired sessions from internal state
|
// Clear expired/invalid sessions from internal state
|
||||||
for sid in &expired {
|
for sid in &expired {
|
||||||
tracing::info!("Session {} expired (inactive >5min), releasing", sid);
|
let peer_state_opt = self.peer_machines.get(sid);
|
||||||
|
let reason = if let Some(ps) = peer_state_opt {
|
||||||
|
let key_valid = self.access_keys.read().unwrap().contains_key(&ps.access_key);
|
||||||
|
let user_stats = self.get_or_create_user_stats(&ps.access_key);
|
||||||
|
if now.duration_since(ps.last_seen) > timeout_dur {
|
||||||
|
"inactive >5min"
|
||||||
|
} else if !key_valid {
|
||||||
|
"key deleted"
|
||||||
|
} else if user_stats.is_over_limit() {
|
||||||
|
"traffic limit exceeded"
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
};
|
||||||
|
tracing::info!("Session {} closed ({}), releasing", sid, reason);
|
||||||
self.drop_session(*sid);
|
self.drop_session(*sid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -459,6 +498,7 @@ impl Dispatcher {
|
||||||
|
|
||||||
fn get_or_create_stats(
|
fn get_or_create_stats(
|
||||||
user_stats: &Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
user_stats: &Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
||||||
|
access_keys: &Arc<RwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||||
key: &str,
|
key: &str,
|
||||||
) -> Arc<UserStats> {
|
) -> Arc<UserStats> {
|
||||||
{
|
{
|
||||||
|
|
@ -467,26 +507,31 @@ fn get_or_create_stats(
|
||||||
return existing.clone();
|
return existing.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let limit_bytes = access_keys.read().unwrap().get(key).and_then(|m| m.limit_bytes);
|
||||||
|
|
||||||
let mut stats = user_stats.write().unwrap();
|
let mut stats = user_stats.write().unwrap();
|
||||||
stats.entry(key.to_string())
|
stats.entry(key.to_string())
|
||||||
.or_insert_with(|| Arc::new(UserStats::new(None)))
|
.or_insert_with(|| Arc::new(UserStats::new(limit_bytes)))
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track_user_bytes_up(
|
fn track_user_bytes_up(
|
||||||
user_stats: &Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
user_stats: &Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
||||||
|
access_keys: &Arc<RwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||||
key: &str,
|
key: &str,
|
||||||
bytes: u64,
|
bytes: u64,
|
||||||
) {
|
) {
|
||||||
let stats = get_or_create_stats(user_stats, key);
|
let stats = get_or_create_stats(user_stats, access_keys, key);
|
||||||
stats.bytes_up.fetch_add(bytes, Ordering::Relaxed);
|
stats.bytes_up.fetch_add(bytes, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track_user_bytes_down(
|
fn track_user_bytes_down(
|
||||||
user_stats: &Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
user_stats: &Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
||||||
|
access_keys: &Arc<RwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||||
key: &str,
|
key: &str,
|
||||||
bytes: u64,
|
bytes: u64,
|
||||||
) {
|
) {
|
||||||
let stats = get_or_create_stats(user_stats, key);
|
let stats = get_or_create_stats(user_stats, access_keys, key);
|
||||||
stats.bytes_down.fetch_add(bytes, Ordering::Relaxed);
|
stats.bytes_down.fetch_add(bytes, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,65 +65,21 @@ pub(crate) struct RemoteState {
|
||||||
|
|
||||||
pub async fn run_server(
|
pub async fn run_server(
|
||||||
bind_addrs: Vec<String>,
|
bind_addrs: Vec<String>,
|
||||||
access_keys: Vec<String>,
|
access_keys: Vec<(String, crate::api::UserMeta)>,
|
||||||
outbound: Option<OutboundConfig>,
|
outbound: Option<OutboundConfig>,
|
||||||
api_config: Option<ApiConfig>,
|
api_config: Option<ApiConfig>,
|
||||||
fallback_config: Option<FallbackConfig>,
|
fallback_config: Option<FallbackConfig>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
reality_query: Option<String>,
|
reality_query: Option<String>,
|
||||||
reality_config: Option<RealityServerConfig>,
|
reality_config: Option<RealityServerConfig>,
|
||||||
|
config_path: Option<std::path::PathBuf>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut keys_map = HashMap::new();
|
let mut keys_map = HashMap::new();
|
||||||
for key in access_keys {
|
for (key, meta) in access_keys {
|
||||||
keys_map.insert(key, ());
|
keys_map.insert(key, meta);
|
||||||
}
|
}
|
||||||
let shared_keys = std::sync::Arc::new(std::sync::RwLock::new(keys_map));
|
let shared_keys = std::sync::Arc::new(std::sync::RwLock::new(keys_map));
|
||||||
|
|
||||||
// Background config hot-reloader for access keys
|
|
||||||
let shared_keys_clone = shared_keys.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut last_mtime = None;
|
|
||||||
let exe = match std::env::current_exe() {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(_) => return,
|
|
||||||
};
|
|
||||||
let dir = match exe.parent() {
|
|
||||||
Some(d) => d,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
let config_path = dir.join("config.json");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
|
||||||
if let Ok(metadata) = std::fs::metadata(&config_path) {
|
|
||||||
if let Ok(mtime) = metadata.modified() {
|
|
||||||
if last_mtime != Some(mtime) {
|
|
||||||
last_mtime = Some(mtime);
|
|
||||||
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct ServerReloadConfig {
|
|
||||||
mode: String,
|
|
||||||
#[serde(default)]
|
|
||||||
access_keys: Vec<String>,
|
|
||||||
}
|
|
||||||
if let Ok(cfg) = serde_json::from_str::<ServerReloadConfig>(&content) {
|
|
||||||
if cfg.mode == "server" {
|
|
||||||
let mut new_keys = HashMap::new();
|
|
||||||
for key in cfg.access_keys {
|
|
||||||
new_keys.insert(key, ());
|
|
||||||
}
|
|
||||||
let mut keys_lock = shared_keys_clone.write().unwrap();
|
|
||||||
*keys_lock = new_keys;
|
|
||||||
tracing::info!("Hot-reloaded {} access keys from config.json", keys_lock.len());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut sockets = Vec::new();
|
let mut sockets = Vec::new();
|
||||||
for bind_addr in &bind_addrs {
|
for bind_addr in &bind_addrs {
|
||||||
let addr = bind_addr.parse::<std::net::SocketAddr>()
|
let addr = bind_addr.parse::<std::net::SocketAddr>()
|
||||||
|
|
@ -164,6 +120,125 @@ pub async fn run_server(
|
||||||
|
|
||||||
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone());
|
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone());
|
||||||
|
|
||||||
|
// Background config hot-reloader for access keys
|
||||||
|
let shared_keys_clone = shared_keys.clone();
|
||||||
|
let user_stats_clone = dispatcher.user_stats_ref();
|
||||||
|
let config_path_clone = config_path.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let path_to_watch = if let Some(p) = config_path_clone {
|
||||||
|
p
|
||||||
|
} else {
|
||||||
|
let exe = match std::env::current_exe() {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let dir = match exe.parent() {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
dir.join("config.json")
|
||||||
|
};
|
||||||
|
|
||||||
|
let path_to_watch = match std::fs::canonicalize(&path_to_watch) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => path_to_watch,
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("Watching configuration file for hot-reload: {:?}", path_to_watch);
|
||||||
|
|
||||||
|
let mut last_mtime = None;
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||||
|
if let Ok(metadata) = std::fs::metadata(&path_to_watch) {
|
||||||
|
if let Ok(mtime) = metadata.modified() {
|
||||||
|
if last_mtime != Some(mtime) {
|
||||||
|
last_mtime = Some(mtime);
|
||||||
|
match std::fs::read_to_string(&path_to_watch) {
|
||||||
|
Ok(content) => {
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum ReloadUser {
|
||||||
|
Detailed { access_key: String, name: Option<String>, limit_bytes: Option<u64> },
|
||||||
|
KeyOnly(String),
|
||||||
|
}
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct ServerReloadConfig {
|
||||||
|
mode: String,
|
||||||
|
#[serde(default)]
|
||||||
|
access_keys: Vec<ReloadUser>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stripped = json_comments::StripComments::new(content.as_bytes());
|
||||||
|
let mut content_str = String::new();
|
||||||
|
use std::io::Read;
|
||||||
|
if let Err(e) = stripped.read_to_string(&mut content_str) {
|
||||||
|
tracing::error!("Failed to strip comments from config during hot-reload: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::from_str::<ServerReloadConfig>(&content_str) {
|
||||||
|
Ok(cfg) => {
|
||||||
|
if cfg.mode == "server" {
|
||||||
|
let mut new_keys = HashMap::new();
|
||||||
|
for uc in cfg.access_keys {
|
||||||
|
let (k, m) = match uc {
|
||||||
|
ReloadUser::Detailed { access_key, name, limit_bytes } => (access_key, crate::api::UserMeta { name, limit_bytes }),
|
||||||
|
ReloadUser::KeyOnly(k) => (k, crate::api::UserMeta { name: None, limit_bytes: None }),
|
||||||
|
};
|
||||||
|
new_keys.insert(k, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Update shared_keys
|
||||||
|
let mut keys_lock = shared_keys_clone.write().unwrap();
|
||||||
|
*keys_lock = new_keys.clone();
|
||||||
|
|
||||||
|
// 2. Synchronize user_stats limits & cleanup deleted keys
|
||||||
|
let mut stats_lock = user_stats_clone.write().unwrap();
|
||||||
|
stats_lock.retain(|k, _| new_keys.contains_key(k));
|
||||||
|
|
||||||
|
for (k, meta) in &new_keys {
|
||||||
|
let entry_info = stats_lock.get(k).map(|e| {
|
||||||
|
(
|
||||||
|
e.limit_bytes,
|
||||||
|
e.bytes_up.load(std::sync::atomic::Ordering::Relaxed),
|
||||||
|
e.bytes_down.load(std::sync::atomic::Ordering::Relaxed),
|
||||||
|
e.connections.load(std::sync::atomic::Ordering::Relaxed),
|
||||||
|
e.created_at,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if let Some((limit_bytes, bytes_up, bytes_down, connections, created_at)) = entry_info {
|
||||||
|
if limit_bytes != meta.limit_bytes {
|
||||||
|
stats_lock.insert(k.clone(), std::sync::Arc::new(dispatcher::UserStats {
|
||||||
|
bytes_up: portable_atomic::AtomicU64::new(bytes_up),
|
||||||
|
bytes_down: portable_atomic::AtomicU64::new(bytes_down),
|
||||||
|
connections: portable_atomic::AtomicU64::new(connections),
|
||||||
|
limit_bytes: meta.limit_bytes,
|
||||||
|
created_at,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stats_lock.insert(k.clone(), std::sync::Arc::new(dispatcher::UserStats::new(meta.limit_bytes)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Hot-reloaded {} access keys from {:?}", keys_lock.len(), path_to_watch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to parse config file during hot-reload: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to read config file during hot-reload: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Spawn Management API if configured
|
// Spawn Management API if configured
|
||||||
if let Some(api_cfg) = api_config {
|
if let Some(api_cfg) = api_config {
|
||||||
if api_cfg.enabled {
|
if api_cfg.enabled {
|
||||||
|
|
@ -175,8 +250,9 @@ pub async fn run_server(
|
||||||
let server_port: u16 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(50000);
|
let server_port: u16 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(50000);
|
||||||
let server_host = parts.get(1).unwrap_or(&"0.0.0.0").to_string();
|
let server_host = parts.get(1).unwrap_or(&"0.0.0.0").to_string();
|
||||||
let rq = reality_query.clone().unwrap_or_default();
|
let rq = reality_query.clone().unwrap_or_default();
|
||||||
|
let config_path_api = config_path.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, rq).await;
|
api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, rq, config_path_api).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +343,7 @@ async fn run_server_loop(
|
||||||
mut dispatcher: Dispatcher,
|
mut dispatcher: Dispatcher,
|
||||||
mut ui_cmd_rx: mpsc::UnboundedReceiver<UiCommand>,
|
mut ui_cmd_rx: mpsc::UnboundedReceiver<UiCommand>,
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, ()>>>,
|
shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||||
outbound: Option<OutboundConfig>,
|
outbound: Option<OutboundConfig>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
tls_config: Option<std::sync::Arc<rustls::ServerConfig>>,
|
tls_config: Option<std::sync::Arc<rustls::ServerConfig>>,
|
||||||
|
|
@ -396,7 +472,7 @@ async fn run_server_loop(
|
||||||
match cmd {
|
match cmd {
|
||||||
Some(UiCommand::CreateClientKey) => {
|
Some(UiCommand::CreateClientKey) => {
|
||||||
let key = format!("ostp_key_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
|
let key = format!("ostp_key_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
|
||||||
shared_keys.write().unwrap().insert(key.clone(), ());
|
shared_keys.write().unwrap().insert(key.clone(), crate::api::UserMeta { name: None, limit_bytes: None });
|
||||||
let _ = ui_event_tx.send(UiEvent::KeyCreated { key });
|
let _ = ui_event_tx.send(UiEvent::KeyCreated { key });
|
||||||
}
|
}
|
||||||
Some(UiCommand::Shutdown) | None => {
|
Some(UiCommand::Shutdown) | None => {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ pub async fn run_relay_node(cfg: RelayConfig) -> Result<()> {
|
||||||
|
|
||||||
/// Синхронизация access_keys с upstream API.
|
/// Синхронизация access_keys с upstream API.
|
||||||
async fn sync_keys(cfg: &RelayConfig, shared_keys: &SharedKeys) -> Result<usize> {
|
async fn sync_keys(cfg: &RelayConfig, shared_keys: &SharedKeys) -> Result<usize> {
|
||||||
let url = format!("{}/api/keys", cfg.upstream_api_url.trim_end_matches('/'));
|
let url = format!("{}/api/users", cfg.upstream_api_url.trim_end_matches('/'));
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(10))
|
.timeout(Duration::from_secs(10))
|
||||||
|
|
@ -99,15 +99,26 @@ async fn sync_keys(cfg: &RelayConfig, shared_keys: &SharedKeys) -> Result<usize>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct KeysResponse {
|
struct UserStatsSnapshot {
|
||||||
keys: Vec<String>,
|
access_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let body: KeysResponse = resp.json().await?;
|
#[derive(serde::Deserialize)]
|
||||||
let count = body.keys.len();
|
struct ApiResponse {
|
||||||
|
ok: bool,
|
||||||
|
data: Option<Vec<UserStatsSnapshot>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: ApiResponse = resp.json().await?;
|
||||||
|
if !body.ok {
|
||||||
|
anyhow::bail!("API returned error ok=false");
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys: Vec<String> = body.data.unwrap_or_default().into_iter().map(|u| u.access_key).collect();
|
||||||
|
let count = keys.len();
|
||||||
{
|
{
|
||||||
let mut lock = shared_keys.write().unwrap();
|
let mut lock = shared_keys.write().unwrap();
|
||||||
*lock = body.keys;
|
*lock = keys;
|
||||||
}
|
}
|
||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use tracing::info;
|
||||||
pub async fn handle_tcp_connection<S>(
|
pub async fn handle_tcp_connection<S>(
|
||||||
mut stream: S,
|
mut stream: S,
|
||||||
peer_addr: SocketAddr,
|
peer_addr: SocketAddr,
|
||||||
shared_keys: Arc<StdRwLock<HashMap<String, ()>>>,
|
shared_keys: Arc<StdRwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||||
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
|
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
|
||||||
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
|
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
|
||||||
) -> Result<()>
|
) -> Result<()>
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 43b4935fd2addc284a5ae8719824652f9063b95d
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# OSTP Wiki
|
||||||
|
|
||||||
|
This repository contains the documentation and wiki pages for the Ospab Stealth Transport Protocol (OSTP).
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
# Справочник API управления OSTP
|
||||||
|
|
||||||
|
Сервер OSTP предоставляет REST API для управления пользователями, просмотра статистики трафика и интерактивного редактирования конфигурации.
|
||||||
|
|
||||||
|
По умолчанию API слушает на порту `9090` (хост настраивается в файле конфигурации).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Авторизация
|
||||||
|
|
||||||
|
Все запросы к API (за исключением подписок) должны содержать заголовок `Authorization` с API-токеном (если токен включен в конфигурационном файле):
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <ваш_api_токен>
|
||||||
|
```
|
||||||
|
|
||||||
|
Или в упрощенном виде:
|
||||||
|
```http
|
||||||
|
Authorization: <ваш_api_токен>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Формат ответов
|
||||||
|
|
||||||
|
Все ответы API возвращаются в формате JSON следующей структуры:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"data": ...,
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
В случае ошибки:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"data": null,
|
||||||
|
"error": "Описание ошибки"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Список эндпоинтов
|
||||||
|
|
||||||
|
### 1. Статус сервера
|
||||||
|
Возвращает текущую версию, аптайм и количество пользователей.
|
||||||
|
|
||||||
|
* **URL**: `/api/server/status`
|
||||||
|
* **Метод**: `GET`
|
||||||
|
* **Формат `data`**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.2.30",
|
||||||
|
"uptime_seconds": 12053,
|
||||||
|
"active_users": 2,
|
||||||
|
"total_users": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Получение текущего конфига
|
||||||
|
Запрашивает полное содержимое файла `config.json` с удалением комментариев для прямой модификации.
|
||||||
|
|
||||||
|
* **URL**: `/api/server/config`
|
||||||
|
* **Метод**: `GET`
|
||||||
|
* **Формат `data`**: Полный JSON-конфиг сервера.
|
||||||
|
|
||||||
|
### 3. Обновление конфига
|
||||||
|
Записывает новый JSON конфигурации сервера в файл `config.json` на диске. Это автоматически вызывает **hot-reload** ядра (применение ключей доступа и лимитов).
|
||||||
|
|
||||||
|
* **URL**: `/api/server/config`
|
||||||
|
* **Метод**: `PUT`
|
||||||
|
* **Тело запроса**: JSON нового конфигурационного файла.
|
||||||
|
* **Формат `data`**: `true` в случае успешного сохранения.
|
||||||
|
|
||||||
|
### 4. Список клиентов и их статистики
|
||||||
|
Возвращает список всех зарегистрированных ключей доступа с их текущей загрузкой, скачиванием, активными сессиями и статусом подключения.
|
||||||
|
|
||||||
|
* **URL**: `/api/users`
|
||||||
|
* **Метод**: `GET`
|
||||||
|
* **Формат `data`**:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"access_key": "ostp_key_sample1",
|
||||||
|
"bytes_up": 2405020,
|
||||||
|
"bytes_down": 491029402,
|
||||||
|
"connections": 2,
|
||||||
|
"limit_bytes": 10737418240,
|
||||||
|
"online": true,
|
||||||
|
"name": "Ноутбук"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Создание клиента
|
||||||
|
Генерирует новый ключ доступа (или регистрирует пользовательский).
|
||||||
|
|
||||||
|
* **URL**: `/api/users`
|
||||||
|
* **Метод**: `POST`
|
||||||
|
* **Тело запроса**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_key": "my_custom_key_optional",
|
||||||
|
"name": "Имя клиента",
|
||||||
|
"limit_bytes": 50000000000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* **Формат `data`**: Строка созданного ключа доступа.
|
||||||
|
|
||||||
|
### 6. Удаление клиента
|
||||||
|
Отзывает ключ доступа и сбрасывает все связанные активные сессии.
|
||||||
|
|
||||||
|
* **URL**: `/api/users/:key`
|
||||||
|
* **Метод**: `DELETE`
|
||||||
|
* **Формат `data`**: `"User removed"`
|
||||||
|
|
||||||
|
### 7. Обновление клиента
|
||||||
|
Редактирует имя или лимит трафика для клиента.
|
||||||
|
|
||||||
|
* **URL**: `/api/users/:key`
|
||||||
|
* **Метод**: `PUT`
|
||||||
|
* **Тело запроса**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Новое имя",
|
||||||
|
"limit_bytes": 100000000000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* **Формат `data`**: `"User updated"`
|
||||||
|
|
||||||
|
### 8. Сброс счетчиков трафика
|
||||||
|
Обнуляет показания загрузки и скачивания для определенного пользователя.
|
||||||
|
|
||||||
|
* **URL**: `/api/users/{key}/reset`
|
||||||
|
* **Метод**: `POST`
|
||||||
|
* **Формат `data`**: `true`
|
||||||
|
|
||||||
|
### 9. Ссылка подписки клиента
|
||||||
|
Возвращает ссылку подписки или конфигурационный файл для клиента. Авторизация по Bearer-токену **не требуется** (ключ авторизуется сам через URL).
|
||||||
|
|
||||||
|
* **URL**: `/api/subscribe/:key`
|
||||||
|
* **Метод**: `GET`
|
||||||
|
* **Заголовки**:
|
||||||
|
- `Accept: text/plain` -> Возвращает текстовую ссылку `ostp://<key>@<host>:<port>?...`
|
||||||
|
- `Accept: application/json` -> Возвращает полный клиентский JSON-конфиг.
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
# Руководство по конфигурации OSTP (`config.json`)
|
||||||
|
|
||||||
|
Файл `config.json` является основным конфигурационным файлом для сервера, клиента и реле.
|
||||||
|
|
||||||
|
Ниже приведено подробное описание структуры для режима работы **Server**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Полный пример конфигурации
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "server",
|
||||||
|
"log_level": "info",
|
||||||
|
"listen": "0.0.0.0:50000",
|
||||||
|
"access_keys": [
|
||||||
|
"some_simple_key",
|
||||||
|
{
|
||||||
|
"access_key": "detailed_key_with_limit",
|
||||||
|
"name": "Рабочий Ноутбук",
|
||||||
|
"limit_bytes": 107374182400
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"bind": "127.0.0.1:9090",
|
||||||
|
"token": "7a3f8b2c4d9e0f1a2b3c4d5e6f7a8b9c"
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"enabled": false,
|
||||||
|
"listen": "0.0.0.0:443",
|
||||||
|
"target": "127.0.0.1:8080"
|
||||||
|
},
|
||||||
|
"reality": {
|
||||||
|
"enabled": false,
|
||||||
|
"dest": "www.microsoft.com:443",
|
||||||
|
"private_key": "...",
|
||||||
|
"pbk": "...",
|
||||||
|
"sid": "...",
|
||||||
|
"sni_list": ["www.microsoft.com"]
|
||||||
|
},
|
||||||
|
"outbound": {
|
||||||
|
"enabled": false,
|
||||||
|
"protocol": "socks5",
|
||||||
|
"address": "127.0.0.1",
|
||||||
|
"port": 9050,
|
||||||
|
"default_action": "proxy",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"domain_suffix": [".onion"],
|
||||||
|
"action": "proxy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"debug": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Описание разделов конфигурации
|
||||||
|
|
||||||
|
### 1. Основные параметры
|
||||||
|
- **`mode`** (строка): Режим работы. Возможные варианты: `"server"`, `"client"`, `"relay"`.
|
||||||
|
- **`log_level`** (строка): Уровень логирования. Варианты: `"debug"`, `"info"`, `"warn"`, `"error"`.
|
||||||
|
- **`listen`** (строка или массив строк): Порт и интерфейсы, на которых сервер слушает входящие UDP (и опционально TCP/UoT) соединения. Примеры:
|
||||||
|
- `"0.0.0.0:50000"` (все IPv4 интерфейсы)
|
||||||
|
- `["0.0.0.0:50000", "[::]:50000"]` (поддержка IPv4 и IPv6 одновременно)
|
||||||
|
- **`debug`** (логический): Включает подробное отладочное логирование протокола.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Ключи доступа (`access_keys`)
|
||||||
|
Раздел содержит массив ключей доступа. Поддерживается два формата записи (для обратной совместимости):
|
||||||
|
1. **Простая строка**: Текст ключа доступа. Лимит трафика отсутствует.
|
||||||
|
```json
|
||||||
|
"my_secure_key"
|
||||||
|
```
|
||||||
|
2. **Объект с метаданными**:
|
||||||
|
- `access_key` (строка, обязательно): Текст ключа для подключения.
|
||||||
|
- `name` (строка, опционально): Человекочитаемое описание клиента.
|
||||||
|
- `limit_bytes` (число, опционально): Лимит трафика в байтах (загрузка + скачивание).
|
||||||
|
|
||||||
|
При достижении `limit_bytes` сессия клиента немедленно сбрасывается и подключение блокируется до обнуления счетчика или расширения лимита.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. REST API Управления (`api`)
|
||||||
|
Используется для интеграции с панелью управления `ostp-control`.
|
||||||
|
- **`enabled`** (логический): Включение встроенного веб-сервера API.
|
||||||
|
- **`bind`** (строка): Интерфейс и порт для прослушивания (например, `"127.0.0.1:9090"`).
|
||||||
|
- **`token`** (строка): Bearer-токен для авторизации администратора. Автоматически генерируется сервером при команде `ostp --init server`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Встроенный TCP Fallback прокси (`fallback`)
|
||||||
|
Позволяет маскировать порт под веб-сервер при сканировании активными DPI-зондами.
|
||||||
|
- **`enabled`** (логический): Включить проксирование TCP.
|
||||||
|
- **`listen`** (строка): Порт прослушивания TCP/TLS (например, `"0.0.0.0:443"`).
|
||||||
|
- **`target`** (строка): Локальный веб-сервер (например, `"127.0.0.1:8080"` на nginx/caddy), куда будут пересылаться все обычные запросы (не-OSTP трафик).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Reality Маскировка (`reality`)
|
||||||
|
Реализует спецификацию XTLS-Reality для бесшовной маскировки трафика под легитимный TLS-сервер.
|
||||||
|
- **`enabled`** (логический): Включение маскировки.
|
||||||
|
- **`dest`** (строка): Целевой домен маскировки (например, `"www.microsoft.com:443"`).
|
||||||
|
- **`private_key`** (строка): Приватный ключ Reality сервера (X25519).
|
||||||
|
- **`pbk`** (строка): Публичный ключ Reality сервера.
|
||||||
|
- **`sid`** (строка, 8 байт hex): Идентификатор сессии.
|
||||||
|
- **`sni_list`** (массив строк): Разрешенные SNI заголовки от клиентов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Правила маршрутизации (`outbound`)
|
||||||
|
Позволяет пересылать часть исходящего трафика клиентов через прокси-сервер (например, SOCKS5/TOR).
|
||||||
|
- **`enabled`** (логический): Включить исходящую маршрутизацию.
|
||||||
|
- **`protocol`** (строка): Протокол прокси. На данный момент поддерживается `"socks5"`.
|
||||||
|
- **`address`** (строка): Хост прокси-сервера.
|
||||||
|
- **`port`** (число): Порт прокси-сервера.
|
||||||
|
- **`default_action`** (строка): Действие для трафика, не попавшего под правила. Варианты: `"direct"` (напрямую с сервера) или `"proxy"` (через прокси).
|
||||||
|
- **`rules`** (массив объектов): Список правил перенаправления:
|
||||||
|
- `domain_suffix` (массив строк): Фильтрация по суффиксу домена.
|
||||||
|
- `ip_cidr` (массив строк): Фильтрация по IP подсетям.
|
||||||
|
- `action` (строка): Действие при совпадении (`"direct"` или `"proxy"`).
|
||||||
|
|
@ -191,10 +191,42 @@ impl UnifiedConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum UserConfig {
|
||||||
|
Detailed {
|
||||||
|
access_key: String,
|
||||||
|
name: Option<String>,
|
||||||
|
limit_bytes: Option<u64>,
|
||||||
|
},
|
||||||
|
KeyOnly(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserConfig {
|
||||||
|
pub fn key(&self) -> String {
|
||||||
|
match self {
|
||||||
|
UserConfig::KeyOnly(k) => k.clone(),
|
||||||
|
UserConfig::Detailed { access_key, .. } => access_key.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn name(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
UserConfig::KeyOnly(_) => None,
|
||||||
|
UserConfig::Detailed { name, .. } => name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn limit(&self) -> Option<u64> {
|
||||||
|
match self {
|
||||||
|
UserConfig::KeyOnly(_) => None,
|
||||||
|
UserConfig::Detailed { limit_bytes, .. } => limit_bytes.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
struct ServerConfig {
|
struct ServerConfig {
|
||||||
listen: ListenConfig,
|
listen: ListenConfig,
|
||||||
access_keys: Vec<String>,
|
access_keys: Vec<UserConfig>,
|
||||||
reality: Option<RealityServerConfigRaw>,
|
reality: Option<RealityServerConfigRaw>,
|
||||||
debug: Option<bool>,
|
debug: Option<bool>,
|
||||||
outbound: Option<OutboundConfig>,
|
outbound: Option<OutboundConfig>,
|
||||||
|
|
@ -450,8 +482,37 @@ async fn run_app() -> Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
if args.generate_key {
|
if args.generate_key {
|
||||||
|
let mut new_keys = Vec::new();
|
||||||
for _ in 0..args.count {
|
for _ in 0..args.count {
|
||||||
println!("{}", generate_secure_key(&args.format));
|
let key = generate_secure_key(&args.format);
|
||||||
|
println!("{}", key);
|
||||||
|
new_keys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Автоматическое добавление ключа в config.json если это сервер
|
||||||
|
if args.config.exists() {
|
||||||
|
if let Ok(content) = fs::read_to_string(&args.config) {
|
||||||
|
let mut stripped = json_comments::StripComments::new(content.as_bytes());
|
||||||
|
let mut content_str = String::new();
|
||||||
|
use std::io::Read;
|
||||||
|
if stripped.read_to_string(&mut content_str).is_ok() {
|
||||||
|
if let Ok(mut json_val) = serde_json::from_str::<serde_json::Value>(&content_str) {
|
||||||
|
if let Some(mode) = json_val.get("mode").and_then(|m| m.as_str()) {
|
||||||
|
if mode == "server" {
|
||||||
|
if let Some(access_keys) = json_val.get_mut("access_keys").and_then(|a| a.as_array_mut()) {
|
||||||
|
for key in new_keys {
|
||||||
|
access_keys.push(serde_json::Value::String(key));
|
||||||
|
}
|
||||||
|
if let Ok(new_content) = serde_json::to_string_pretty(&json_val) {
|
||||||
|
let _ = fs::write(&args.config, new_content);
|
||||||
|
println!("[ostp] Key(s) automatically added to {:?}", args.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -522,6 +583,7 @@ async fn run_app() -> Result<()> {
|
||||||
let key = generate_secure_key("hex");
|
let key = generate_secure_key("hex");
|
||||||
let content = if is_server {
|
let content = if is_server {
|
||||||
let (priv_key, pub_key, sid) = generate_reality_keys();
|
let (priv_key, pub_key, sid) = generate_reality_keys();
|
||||||
|
let api_token = generate_secure_key("hex");
|
||||||
format!(r#"{{
|
format!(r#"{{
|
||||||
// OSTP Server Configuration
|
// OSTP Server Configuration
|
||||||
"mode": "server",
|
"mode": "server",
|
||||||
|
|
@ -556,7 +618,7 @@ async fn run_app() -> Result<()> {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"bind": "127.0.0.1:9090",
|
"bind": "127.0.0.1:9090",
|
||||||
// Set a strong token for authentication. Leave empty to disable auth.
|
// Set a strong token for authentication. Leave empty to disable auth.
|
||||||
"token": ""
|
"token": "{}"
|
||||||
}},
|
}},
|
||||||
|
|
||||||
// Fallback TCP proxy: unrecognized connections are proxied to a web server (anti-DPI).
|
// Fallback TCP proxy: unrecognized connections are proxied to a web server (anti-DPI).
|
||||||
|
|
@ -577,7 +639,19 @@ async fn run_app() -> Result<()> {
|
||||||
"sni_list": ["www.microsoft.com"]
|
"sni_list": ["www.microsoft.com"]
|
||||||
}},
|
}},
|
||||||
"debug": false
|
"debug": false
|
||||||
}}"#, key, priv_key, pub_key, sid)
|
}}"#, key, api_token, priv_key, pub_key, sid)
|
||||||
|
} else if mode_str == "relay" {
|
||||||
|
r#"{
|
||||||
|
// OSTP Relay Node Configuration
|
||||||
|
"mode": "relay",
|
||||||
|
"listen": "0.0.0.0:50000",
|
||||||
|
"upstream_tcp": "TARGET_SERVER_IP:50000",
|
||||||
|
"upstream_udp": "TARGET_SERVER_IP:50000",
|
||||||
|
"upstream_api_url": "http://TARGET_SERVER_IP:9090",
|
||||||
|
"upstream_api_token": "YOUR_API_TOKEN_HERE",
|
||||||
|
"sync_interval_secs": 30,
|
||||||
|
"debug": false
|
||||||
|
}"#.to_string()
|
||||||
} else {
|
} else {
|
||||||
format!(r#"{{
|
format!(r#"{{
|
||||||
// OSTP Client Configuration
|
// OSTP Client Configuration
|
||||||
|
|
@ -645,7 +719,7 @@ async fn run_app() -> Result<()> {
|
||||||
if let AppMode::Server(s) = &config.mode {
|
if let AppMode::Server(s) = &config.mode {
|
||||||
let key = &s.access_keys[0];
|
let key = &s.access_keys[0];
|
||||||
let host = get_or_ask_public_ip(&args.config);
|
let host = get_or_ask_public_ip(&args.config);
|
||||||
let mut link = format!("ostp://{}@{}:50000", key, host);
|
let mut link = format!("ostp://{}@{}:50000", key.key(), host);
|
||||||
let mut query_params = Vec::new();
|
let mut query_params = Vec::new();
|
||||||
|
|
||||||
if let Some(r) = &s.reality {
|
if let Some(r) = &s.reality {
|
||||||
|
|
@ -725,7 +799,7 @@ async fn run_app() -> Result<()> {
|
||||||
|
|
||||||
println!("\n Client share links from {:?}:", args.config);
|
println!("\n Client share links from {:?}:", args.config);
|
||||||
for (idx, key) in server_cfg.access_keys.iter().enumerate() {
|
for (idx, key) in server_cfg.access_keys.iter().enumerate() {
|
||||||
let mut link = format!("ostp://{}@{}:{}", key, host, port);
|
let mut link = format!("ostp://{}@{}:{}", key.key(), host, port);
|
||||||
if let Some(r) = &server_cfg.reality {
|
if let Some(r) = &server_cfg.reality {
|
||||||
link = format!("{}?security=reality&sni={}&pbk={}&sid={}&type=udp", link, r.sni_list.first().unwrap_or(&String::new()), r.pbk, r.sid);
|
link = format!("{}?security=reality&sni={}&pbk={}&sid={}&type=udp", link, r.sni_list.first().unwrap_or(&String::new()), r.pbk, r.sid);
|
||||||
}
|
}
|
||||||
|
|
@ -792,8 +866,14 @@ async fn run_app() -> Result<()> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let access_keys_meta = server_cfg.access_keys.into_iter().map(|uc| {
|
||||||
|
(uc.key(), ostp_server::api::UserMeta {
|
||||||
|
name: uc.name(),
|
||||||
|
limit_bytes: uc.limit(),
|
||||||
|
})
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
// Pass all listen addresses for multi-listener support
|
// Pass all listen addresses for multi-listener support
|
||||||
ostp_server::run_server(listen_addrs, server_cfg.access_keys, outbound, api_config, fallback_config, debug, rq, rc).await?;
|
ostp_server::run_server(listen_addrs, access_keys_meta, outbound, api_config, fallback_config, debug, rq, rc, Some(args.config)).await?;
|
||||||
}
|
}
|
||||||
AppMode::Client(client_cfg) => {
|
AppMode::Client(client_cfg) => {
|
||||||
run_client_directly(client_cfg).await?;
|
run_client_directly(client_cfg).await?;
|
||||||
|
|
|
||||||
56
test.json
56
test.json
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
// OSTP Server Configuration
|
|
||||||
"mode": "server",
|
|
||||||
"log_level": "info",
|
|
||||||
|
|
||||||
// The address and port the server listens on for incoming OSTP connections.
|
|
||||||
"listen": "0.0.0.0:50000",
|
|
||||||
|
|
||||||
// List of valid keys. Clients must use one of these to connect.
|
|
||||||
"access_keys": [
|
|
||||||
"07702b35e2062ac40eb4e46a7708dbf6"
|
|
||||||
],
|
|
||||||
|
|
||||||
// Optional proxy for outbound traffic.
|
|
||||||
"outbound": {
|
|
||||||
"enabled": false,
|
|
||||||
"protocol": "socks5",
|
|
||||||
"address": "127.0.0.1",
|
|
||||||
"port": 9050,
|
|
||||||
// default_action: 'proxy' (all through proxy) or 'direct' (bypass proxy by default).
|
|
||||||
"default_action": "proxy",
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"domain_suffix": [".onion"],
|
|
||||||
"action": "proxy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Management REST API for third-party panels.
|
|
||||||
"api": {
|
|
||||||
"enabled": false,
|
|
||||||
"bind": "127.0.0.1:9090",
|
|
||||||
// Set a strong token for authentication. Leave empty to disable auth.
|
|
||||||
"token": ""
|
|
||||||
},
|
|
||||||
|
|
||||||
// Fallback TCP proxy: unrecognized connections are proxied to a web server (anti-DPI).
|
|
||||||
"fallback": {
|
|
||||||
"enabled": false,
|
|
||||||
"listen": "0.0.0.0:443",
|
|
||||||
// Target web server (e.g., local nginx or caddy)
|
|
||||||
"target": "127.0.0.1:8080"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Reality (XTLS) / UoT Masquerade parameters
|
|
||||||
"reality": {
|
|
||||||
"enabled": false,
|
|
||||||
"dest": "www.microsoft.com:443",
|
|
||||||
"private_key": "",
|
|
||||||
"pbk": "",
|
|
||||||
"sid": "",
|
|
||||||
"sni_list": ["www.microsoft.com"]
|
|
||||||
},
|
|
||||||
"debug": false
|
|
||||||
}
|
|
||||||
56
test2.json
56
test2.json
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
// OSTP Server Configuration
|
|
||||||
"mode": "server",
|
|
||||||
"log_level": "info",
|
|
||||||
|
|
||||||
// The address and port the server listens on for incoming OSTP connections.
|
|
||||||
"listen": "0.0.0.0:50000",
|
|
||||||
|
|
||||||
// List of valid keys. Clients must use one of these to connect.
|
|
||||||
"access_keys": [
|
|
||||||
"faa4ed2bb00f5df012f5708d86f9541c"
|
|
||||||
],
|
|
||||||
|
|
||||||
// Optional proxy for outbound traffic.
|
|
||||||
"outbound": {
|
|
||||||
"enabled": false,
|
|
||||||
"protocol": "socks5",
|
|
||||||
"address": "127.0.0.1",
|
|
||||||
"port": 9050,
|
|
||||||
// default_action: 'proxy' (all through proxy) or 'direct' (bypass proxy by default).
|
|
||||||
"default_action": "proxy",
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"domain_suffix": [".onion"],
|
|
||||||
"action": "proxy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Management REST API for third-party panels.
|
|
||||||
"api": {
|
|
||||||
"enabled": false,
|
|
||||||
"bind": "127.0.0.1:9090",
|
|
||||||
// Set a strong token for authentication. Leave empty to disable auth.
|
|
||||||
"token": ""
|
|
||||||
},
|
|
||||||
|
|
||||||
// Fallback TCP proxy: unrecognized connections are proxied to a web server (anti-DPI).
|
|
||||||
"fallback": {
|
|
||||||
"enabled": false,
|
|
||||||
"listen": "0.0.0.0:443",
|
|
||||||
// Target web server (e.g., local nginx or caddy)
|
|
||||||
"target": "127.0.0.1:8080"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Reality (XTLS) / UoT Masquerade parameters
|
|
||||||
"reality": {
|
|
||||||
"enabled": false,
|
|
||||||
"dest": "www.microsoft.com:443",
|
|
||||||
"private_key": "lAapk0V5dG1xSD3CWZ0Ac3c_GKbJU0hVsRjiQ6VrCEY",
|
|
||||||
"pbk": "Md5jDEfpPmaMW5ggNSL7jA0LZi4eKsyfgMwseFQ0TyQ",
|
|
||||||
"sid": "db806ac3c8bf9417",
|
|
||||||
"sni_list": ["www.microsoft.com"]
|
|
||||||
},
|
|
||||||
"debug": false
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue