2025-09-18 02:56:59 +03:00
|
|
|
use anyhow::Result;
|
|
|
|
|
use serde_json::Value;
|
2025-09-19 18:31:35 +03:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio::sync::RwLock;
|
2025-10-24 18:11:34 +03:00
|
|
|
use tokio::time::{timeout, Duration, Instant};
|
2025-10-19 04:13:36 +03:00
|
|
|
use tracing::{error, warn};
|
2025-10-24 18:11:34 +03:00
|
|
|
use uuid::Uuid;
|
2025-09-18 02:56:59 +03:00
|
|
|
|
|
|
|
|
pub mod client;
|
|
|
|
|
pub mod config;
|
|
|
|
|
pub mod inbounds;
|
2025-10-24 18:11:34 +03:00
|
|
|
pub mod stats;
|
2025-09-18 02:56:59 +03:00
|
|
|
pub mod users;
|
|
|
|
|
|
|
|
|
|
pub use client::XrayClient;
|
|
|
|
|
pub use config::XrayConfig;
|
|
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
/// Cached connection with TTL
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
struct CachedConnection {
|
|
|
|
|
client: XrayClient,
|
|
|
|
|
created_at: Instant,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CachedConnection {
|
|
|
|
|
fn new(client: XrayClient) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
client,
|
|
|
|
|
created_at: Instant::now(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
fn is_expired(&self, ttl: Duration) -> bool {
|
|
|
|
|
self.created_at.elapsed() > ttl
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 02:56:59 +03:00
|
|
|
/// Service for managing Xray servers via gRPC
|
|
|
|
|
#[derive(Clone)]
|
2025-09-21 16:38:10 +01:00
|
|
|
pub struct XrayService {
|
|
|
|
|
connection_cache: Arc<RwLock<HashMap<String, CachedConnection>>>,
|
|
|
|
|
connection_ttl: Duration,
|
|
|
|
|
}
|
2025-09-18 02:56:59 +03:00
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
impl XrayService {
|
|
|
|
|
pub fn new() -> Self {
|
2025-09-21 16:38:10 +01:00
|
|
|
Self {
|
|
|
|
|
connection_cache: Arc::new(RwLock::new(HashMap::new())),
|
|
|
|
|
connection_ttl: Duration::from_secs(300), // 5 minutes TTL
|
|
|
|
|
}
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
/// Get or create cached client for endpoint
|
|
|
|
|
async fn get_or_create_client(&self, endpoint: &str) -> Result<XrayClient> {
|
|
|
|
|
// Check cache first
|
|
|
|
|
{
|
|
|
|
|
let cache = self.connection_cache.read().await;
|
|
|
|
|
if let Some(cached) = cache.get(endpoint) {
|
|
|
|
|
if !cached.is_expired(self.connection_ttl) {
|
|
|
|
|
return Ok(cached.client.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
// Create new connection
|
|
|
|
|
let client = XrayClient::connect(endpoint).await?;
|
|
|
|
|
let cached_connection = CachedConnection::new(client.clone());
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
// Update cache
|
|
|
|
|
{
|
|
|
|
|
let mut cache = self.connection_cache.write().await;
|
|
|
|
|
cache.insert(endpoint.to_string(), cached_connection);
|
|
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
Ok(client)
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:13:36 +03:00
|
|
|
/// Test connection to Xray server with timeout
|
2025-09-18 02:56:59 +03:00
|
|
|
pub async fn test_connection(&self, _server_id: Uuid, endpoint: &str) -> Result<bool> {
|
2025-10-19 04:13:36 +03:00
|
|
|
// Apply a 3-second timeout to the entire test operation
|
|
|
|
|
match timeout(Duration::from_secs(3), self.get_or_create_client(endpoint)).await {
|
|
|
|
|
Ok(Ok(_client)) => {
|
|
|
|
|
// Connection successful
|
2025-09-18 02:56:59 +03:00
|
|
|
Ok(true)
|
2025-10-24 18:11:34 +03:00
|
|
|
}
|
2025-10-19 04:13:36 +03:00
|
|
|
Ok(Err(e)) => {
|
|
|
|
|
// Connection failed with error
|
|
|
|
|
warn!("Failed to connect to Xray at {}: {}", endpoint, e);
|
|
|
|
|
Ok(false)
|
2025-10-24 18:11:34 +03:00
|
|
|
}
|
2025-10-19 04:13:36 +03:00
|
|
|
Err(_) => {
|
|
|
|
|
// Operation timed out
|
|
|
|
|
warn!("Connection test to Xray at {} timed out", endpoint);
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Apply full configuration to Xray server
|
2025-10-24 18:11:34 +03:00
|
|
|
pub async fn apply_config(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
config: &XrayConfig,
|
|
|
|
|
) -> Result<()> {
|
2025-09-21 16:38:10 +01:00
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-09-18 02:56:59 +03:00
|
|
|
client.restart_with_config(config).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create inbound from template
|
|
|
|
|
pub async fn create_inbound(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
tag: &str,
|
|
|
|
|
port: i32,
|
|
|
|
|
protocol: &str,
|
|
|
|
|
base_settings: Value,
|
|
|
|
|
stream_settings: Value,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
// Build inbound configuration from template
|
|
|
|
|
let inbound_config = serde_json::json!({
|
|
|
|
|
"tag": tag,
|
|
|
|
|
"port": port,
|
|
|
|
|
"protocol": protocol,
|
|
|
|
|
"settings": base_settings,
|
|
|
|
|
"streamSettings": stream_settings
|
|
|
|
|
});
|
2025-10-24 18:11:34 +03:00
|
|
|
|
|
|
|
|
self.add_inbound(_server_id, endpoint, &inbound_config)
|
|
|
|
|
.await
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create inbound from template with TLS certificate
|
|
|
|
|
pub async fn create_inbound_with_certificate(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
tag: &str,
|
|
|
|
|
port: i32,
|
|
|
|
|
protocol: &str,
|
|
|
|
|
base_settings: Value,
|
|
|
|
|
stream_settings: Value,
|
|
|
|
|
cert_pem: Option<&str>,
|
|
|
|
|
key_pem: Option<&str>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
// Build inbound configuration from template
|
|
|
|
|
let inbound_config = serde_json::json!({
|
|
|
|
|
"tag": tag,
|
|
|
|
|
"port": port,
|
|
|
|
|
"protocol": protocol,
|
|
|
|
|
"settings": base_settings,
|
|
|
|
|
"streamSettings": stream_settings
|
|
|
|
|
});
|
2025-10-24 18:11:34 +03:00
|
|
|
|
|
|
|
|
self.add_inbound_with_certificate(_server_id, endpoint, &inbound_config, cert_pem, key_pem)
|
|
|
|
|
.await
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add inbound to running Xray instance
|
2025-10-24 18:11:34 +03:00
|
|
|
pub async fn add_inbound(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
inbound: &Value,
|
|
|
|
|
) -> Result<()> {
|
2025-09-21 16:38:10 +01:00
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-09-18 02:56:59 +03:00
|
|
|
client.add_inbound(inbound).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add inbound with certificate to running Xray instance
|
2025-10-24 18:11:34 +03:00
|
|
|
pub async fn add_inbound_with_certificate(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
inbound: &Value,
|
|
|
|
|
cert_pem: Option<&str>,
|
|
|
|
|
key_pem: Option<&str>,
|
|
|
|
|
) -> Result<()> {
|
2025-09-21 16:38:10 +01:00
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-10-24 18:11:34 +03:00
|
|
|
client
|
|
|
|
|
.add_inbound_with_certificate(inbound, cert_pem, key_pem)
|
|
|
|
|
.await
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add inbound with users and certificate to running Xray instance
|
2025-10-24 18:11:34 +03:00
|
|
|
pub async fn add_inbound_with_users_and_certificate(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
inbound: &Value,
|
|
|
|
|
users: &[Value],
|
|
|
|
|
cert_pem: Option<&str>,
|
|
|
|
|
key_pem: Option<&str>,
|
|
|
|
|
) -> Result<()> {
|
2025-09-21 16:38:10 +01:00
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-10-24 18:11:34 +03:00
|
|
|
client
|
|
|
|
|
.add_inbound_with_users_and_certificate(inbound, users, cert_pem, key_pem)
|
|
|
|
|
.await
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Remove inbound from running Xray instance
|
|
|
|
|
pub async fn remove_inbound(&self, _server_id: Uuid, endpoint: &str, tag: &str) -> Result<()> {
|
2025-09-21 16:38:10 +01:00
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-09-18 02:56:59 +03:00
|
|
|
client.remove_inbound(tag).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add user to inbound by recreating the inbound with updated user list
|
2025-10-24 18:11:34 +03:00
|
|
|
pub async fn add_user(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
inbound_tag: &str,
|
|
|
|
|
user: &Value,
|
|
|
|
|
) -> Result<()> {
|
2025-09-18 02:56:59 +03:00
|
|
|
// TODO: Implement inbound recreation approach:
|
|
|
|
|
// 1. Get current inbound configuration from database
|
2025-10-24 18:11:34 +03:00
|
|
|
// 2. Get existing users from database
|
2025-09-18 02:56:59 +03:00
|
|
|
// 3. Remove old inbound from xray
|
|
|
|
|
// 4. Create new inbound with all users (existing + new)
|
|
|
|
|
// For now, return error to indicate this needs to be implemented
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-18 02:56:59 +03:00
|
|
|
Err(anyhow::anyhow!("User addition requires inbound recreation - not yet implemented. Use web interface to recreate inbound with users."))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create inbound with users list (for inbound recreation approach)
|
|
|
|
|
pub async fn create_inbound_with_users(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
tag: &str,
|
|
|
|
|
port: i32,
|
|
|
|
|
protocol: &str,
|
|
|
|
|
base_settings: Value,
|
|
|
|
|
stream_settings: Value,
|
|
|
|
|
users: &[Value],
|
|
|
|
|
cert_pem: Option<&str>,
|
|
|
|
|
key_pem: Option<&str>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
// Build inbound configuration with users
|
|
|
|
|
let mut inbound_config = serde_json::json!({
|
|
|
|
|
"tag": tag,
|
|
|
|
|
"port": port,
|
|
|
|
|
"protocol": protocol,
|
|
|
|
|
"settings": base_settings,
|
|
|
|
|
"streamSettings": stream_settings
|
|
|
|
|
});
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-18 02:56:59 +03:00
|
|
|
// Add users to settings based on protocol
|
|
|
|
|
if !users.is_empty() {
|
|
|
|
|
let mut settings = inbound_config["settings"].clone();
|
|
|
|
|
match protocol {
|
|
|
|
|
"vless" | "vmess" => {
|
|
|
|
|
settings["clients"] = serde_json::Value::Array(users.to_vec());
|
2025-10-24 18:11:34 +03:00
|
|
|
}
|
2025-09-18 02:56:59 +03:00
|
|
|
"trojan" => {
|
|
|
|
|
settings["clients"] = serde_json::Value::Array(users.to_vec());
|
2025-10-24 18:11:34 +03:00
|
|
|
}
|
2025-09-18 02:56:59 +03:00
|
|
|
"shadowsocks" => {
|
|
|
|
|
// For shadowsocks, users are handled differently
|
|
|
|
|
if let Some(user) = users.first() {
|
|
|
|
|
settings["password"] = user["password"].clone();
|
|
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
}
|
2025-09-18 02:56:59 +03:00
|
|
|
_ => {
|
2025-10-24 18:11:34 +03:00
|
|
|
return Err(anyhow::anyhow!(
|
|
|
|
|
"Unsupported protocol for users: {}",
|
|
|
|
|
protocol
|
|
|
|
|
));
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
inbound_config["settings"] = settings;
|
|
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-18 02:56:59 +03:00
|
|
|
// Use the new method with users support
|
2025-10-24 18:11:34 +03:00
|
|
|
self.add_inbound_with_users_and_certificate(
|
|
|
|
|
_server_id,
|
|
|
|
|
endpoint,
|
|
|
|
|
&inbound_config,
|
|
|
|
|
users,
|
|
|
|
|
cert_pem,
|
|
|
|
|
key_pem,
|
|
|
|
|
)
|
|
|
|
|
.await
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Remove user from inbound
|
2025-10-24 18:11:34 +03:00
|
|
|
pub async fn remove_user(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
inbound_tag: &str,
|
|
|
|
|
email: &str,
|
|
|
|
|
) -> Result<()> {
|
2025-09-21 16:38:10 +01:00
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-09-18 02:56:59 +03:00
|
|
|
client.remove_user(inbound_tag, email).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get server statistics
|
|
|
|
|
pub async fn get_stats(&self, _server_id: Uuid, endpoint: &str) -> Result<Value> {
|
2025-09-21 16:38:10 +01:00
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-09-18 02:56:59 +03:00
|
|
|
client.get_stats().await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Query specific statistics
|
2025-10-24 18:11:34 +03:00
|
|
|
pub async fn query_stats(
|
|
|
|
|
&self,
|
|
|
|
|
_server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
pattern: &str,
|
|
|
|
|
reset: bool,
|
|
|
|
|
) -> Result<Value> {
|
2025-09-21 16:38:10 +01:00
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-09-18 02:56:59 +03:00
|
|
|
client.query_stats(pattern, reset).await
|
|
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
/// Sync entire server with batch operations using single client
|
|
|
|
|
pub async fn sync_server_inbounds_optimized(
|
|
|
|
|
&self,
|
|
|
|
|
server_id: Uuid,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
desired_inbounds: &HashMap<String, crate::services::tasks::DesiredInbound>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
// Get single client for all operations
|
|
|
|
|
let client = self.get_or_create_client(endpoint).await?;
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
// Perform all operations with the same client
|
|
|
|
|
for (tag, desired) in desired_inbounds {
|
|
|
|
|
// Always try to remove inbound first (ignore errors if it doesn't exist)
|
|
|
|
|
let _ = client.remove_inbound(tag).await;
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
// Create inbound with users
|
2025-10-24 18:11:34 +03:00
|
|
|
let users_json: Vec<Value> = desired
|
|
|
|
|
.users
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|user| {
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"id": user.id,
|
|
|
|
|
"email": user.email,
|
|
|
|
|
"level": user.level
|
|
|
|
|
})
|
2025-09-21 16:38:10 +01:00
|
|
|
})
|
2025-10-24 18:11:34 +03:00
|
|
|
.collect();
|
|
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
// Build inbound config
|
|
|
|
|
let inbound_config = serde_json::json!({
|
|
|
|
|
"tag": desired.tag,
|
|
|
|
|
"port": desired.port,
|
|
|
|
|
"protocol": desired.protocol,
|
|
|
|
|
"settings": desired.settings,
|
|
|
|
|
"streamSettings": desired.stream_settings
|
|
|
|
|
});
|
2025-10-24 18:11:34 +03:00
|
|
|
|
|
|
|
|
match client
|
|
|
|
|
.add_inbound_with_users_and_certificate(
|
|
|
|
|
&inbound_config,
|
|
|
|
|
&users_json,
|
|
|
|
|
desired.cert_pem.as_deref(),
|
|
|
|
|
desired.key_pem.as_deref(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-09-21 16:38:10 +01:00
|
|
|
Err(e) => {
|
|
|
|
|
error!("Failed to create inbound {}: {}", tag, e);
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
|
2025-09-21 16:38:10 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-09-18 02:56:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for XrayService {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::new()
|
|
|
|
|
}
|
2025-10-24 18:11:34 +03:00
|
|
|
}
|