mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-26 18:19:07 +00:00
init rust. WIP: tls for inbounds
This commit is contained in:
91
src/services/xray/client.rs
Normal file
91
src/services/xray/client.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde_json::Value;
|
||||
use xray_core::Client;
|
||||
|
||||
// Import submodules from the same directory
|
||||
use super::stats::StatsClient;
|
||||
use super::inbounds::InboundClient;
|
||||
use super::users::UserClient;
|
||||
|
||||
/// Xray gRPC client wrapper
|
||||
pub struct XrayClient {
|
||||
endpoint: String,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl XrayClient {
|
||||
/// Connect to Xray gRPC server
|
||||
pub async fn connect(endpoint: &str) -> Result<Self> {
|
||||
tracing::info!("Connecting to Xray at {}", endpoint);
|
||||
|
||||
let client = Client::from_url(endpoint).await
|
||||
.map_err(|e| anyhow!("Failed to connect to Xray at {}: {}", endpoint, e))?;
|
||||
|
||||
// Don't clone - we'll use &self.client when calling methods
|
||||
|
||||
Ok(Self {
|
||||
endpoint: endpoint.to_string(),
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get server statistics
|
||||
pub async fn get_stats(&self) -> Result<Value> {
|
||||
let stats_client = StatsClient::new(self.endpoint.clone(), &self.client);
|
||||
stats_client.get_stats().await
|
||||
}
|
||||
|
||||
/// Query specific statistics with pattern
|
||||
pub async fn query_stats(&self, pattern: &str, reset: bool) -> Result<Value> {
|
||||
let stats_client = StatsClient::new(self.endpoint.clone(), &self.client);
|
||||
stats_client.query_stats(pattern, reset).await
|
||||
}
|
||||
|
||||
/// Restart Xray with new configuration
|
||||
pub async fn restart_with_config(&self, config: &crate::services::xray::XrayConfig) -> Result<()> {
|
||||
let inbound_client = InboundClient::new(self.endpoint.clone(), &self.client);
|
||||
inbound_client.restart_with_config(config).await
|
||||
}
|
||||
|
||||
/// Add inbound configuration
|
||||
pub async fn add_inbound(&self, inbound: &Value) -> Result<()> {
|
||||
let inbound_client = InboundClient::new(self.endpoint.clone(), &self.client);
|
||||
inbound_client.add_inbound(inbound).await
|
||||
}
|
||||
|
||||
/// Add inbound configuration with TLS certificate
|
||||
pub async fn add_inbound_with_certificate(&self, inbound: &Value, cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
let inbound_client = InboundClient::new(self.endpoint.clone(), &self.client);
|
||||
inbound_client.add_inbound_with_certificate(inbound, None, cert_pem, key_pem).await
|
||||
}
|
||||
|
||||
/// Add inbound configuration with users and TLS certificate
|
||||
pub async fn add_inbound_with_users_and_certificate(&self, inbound: &Value, users: &[Value], cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
let inbound_client = InboundClient::new(self.endpoint.clone(), &self.client);
|
||||
inbound_client.add_inbound_with_certificate(inbound, Some(users), cert_pem, key_pem).await
|
||||
}
|
||||
|
||||
/// Remove inbound by tag
|
||||
pub async fn remove_inbound(&self, tag: &str) -> Result<()> {
|
||||
let inbound_client = InboundClient::new(self.endpoint.clone(), &self.client);
|
||||
inbound_client.remove_inbound(tag).await
|
||||
}
|
||||
|
||||
/// Add user to inbound
|
||||
pub async fn add_user(&self, inbound_tag: &str, user: &Value) -> Result<()> {
|
||||
let user_client = UserClient::new(self.endpoint.clone(), &self.client);
|
||||
user_client.add_user(inbound_tag, user).await
|
||||
}
|
||||
|
||||
/// Remove user from inbound
|
||||
pub async fn remove_user(&self, inbound_tag: &str, email: &str) -> Result<()> {
|
||||
let user_client = UserClient::new(self.endpoint.clone(), &self.client);
|
||||
user_client.remove_user(inbound_tag, email).await
|
||||
}
|
||||
|
||||
/// Get connection endpoint
|
||||
pub fn endpoint(&self) -> &str {
|
||||
&self.endpoint
|
||||
}
|
||||
}
|
||||
285
src/services/xray/config.rs
Normal file
285
src/services/xray/config.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Xray configuration structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct XrayConfig {
|
||||
pub log: LogConfig,
|
||||
pub api: ApiConfig,
|
||||
pub dns: Option<DnsConfig>,
|
||||
pub routing: Option<RoutingConfig>,
|
||||
pub policy: Option<PolicyConfig>,
|
||||
pub inbounds: Vec<InboundConfig>,
|
||||
pub outbounds: Vec<OutboundConfig>,
|
||||
pub transport: Option<TransportConfig>,
|
||||
pub stats: Option<StatsConfig>,
|
||||
pub reverse: Option<ReverseConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogConfig {
|
||||
pub access: Option<String>,
|
||||
pub error: Option<String>,
|
||||
#[serde(rename = "loglevel")]
|
||||
pub log_level: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApiConfig {
|
||||
pub tag: String,
|
||||
pub listen: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DnsConfig {
|
||||
pub servers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoutingConfig {
|
||||
#[serde(rename = "domainStrategy")]
|
||||
pub domain_strategy: Option<String>,
|
||||
pub rules: Vec<RoutingRule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoutingRule {
|
||||
#[serde(rename = "type")]
|
||||
pub rule_type: String,
|
||||
pub domain: Option<Vec<String>>,
|
||||
pub ip: Option<Vec<String>>,
|
||||
pub port: Option<String>,
|
||||
#[serde(rename = "outboundTag")]
|
||||
pub outbound_tag: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PolicyConfig {
|
||||
pub levels: HashMap<String, PolicyLevel>,
|
||||
pub system: Option<SystemPolicy>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PolicyLevel {
|
||||
#[serde(rename = "handshakeTimeout")]
|
||||
pub handshake_timeout: Option<u32>,
|
||||
#[serde(rename = "connIdle")]
|
||||
pub conn_idle: Option<u32>,
|
||||
#[serde(rename = "uplinkOnly")]
|
||||
pub uplink_only: Option<u32>,
|
||||
#[serde(rename = "downlinkOnly")]
|
||||
pub downlink_only: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemPolicy {
|
||||
#[serde(rename = "statsInboundUplink")]
|
||||
pub stats_inbound_uplink: Option<bool>,
|
||||
#[serde(rename = "statsInboundDownlink")]
|
||||
pub stats_inbound_downlink: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InboundConfig {
|
||||
pub tag: String,
|
||||
pub port: u16,
|
||||
pub listen: Option<String>,
|
||||
pub protocol: String,
|
||||
pub settings: Value,
|
||||
#[serde(rename = "streamSettings")]
|
||||
pub stream_settings: Option<Value>,
|
||||
pub sniffing: Option<SniffingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutboundConfig {
|
||||
pub tag: String,
|
||||
pub protocol: String,
|
||||
pub settings: Value,
|
||||
#[serde(rename = "streamSettings")]
|
||||
pub stream_settings: Option<Value>,
|
||||
pub mux: Option<MuxConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SniffingConfig {
|
||||
pub enabled: bool,
|
||||
#[serde(rename = "destOverride")]
|
||||
pub dest_override: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MuxConfig {
|
||||
pub enabled: bool,
|
||||
pub concurrency: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TransportConfig {
|
||||
#[serde(rename = "tcpSettings")]
|
||||
pub tcp_settings: Option<Value>,
|
||||
#[serde(rename = "kcpSettings")]
|
||||
pub kcp_settings: Option<Value>,
|
||||
#[serde(rename = "wsSettings")]
|
||||
pub ws_settings: Option<Value>,
|
||||
#[serde(rename = "httpSettings")]
|
||||
pub http_settings: Option<Value>,
|
||||
#[serde(rename = "dsSettings")]
|
||||
pub ds_settings: Option<Value>,
|
||||
#[serde(rename = "quicSettings")]
|
||||
pub quic_settings: Option<Value>,
|
||||
#[serde(rename = "grpcSettings")]
|
||||
pub grpc_settings: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StatsConfig {}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReverseConfig {
|
||||
pub bridges: Option<Vec<BridgeConfig>>,
|
||||
pub portals: Option<Vec<PortalConfig>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BridgeConfig {
|
||||
pub tag: String,
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PortalConfig {
|
||||
pub tag: String,
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl XrayConfig {
|
||||
/// Create a new basic Xray configuration
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
log: LogConfig {
|
||||
access: Some("/var/log/xray/access.log".to_string()),
|
||||
error: Some("/var/log/xray/error.log".to_string()),
|
||||
log_level: "warning".to_string(),
|
||||
},
|
||||
api: ApiConfig {
|
||||
tag: "api".to_string(),
|
||||
listen: "127.0.0.1:2053".to_string(),
|
||||
},
|
||||
dns: None,
|
||||
routing: Some(RoutingConfig {
|
||||
domain_strategy: Some("IPIfNonMatch".to_string()),
|
||||
rules: vec![
|
||||
RoutingRule {
|
||||
rule_type: "field".to_string(),
|
||||
domain: None,
|
||||
ip: Some(vec!["geoip:private".to_string()]),
|
||||
port: None,
|
||||
outbound_tag: "direct".to_string(),
|
||||
}
|
||||
],
|
||||
}),
|
||||
policy: Some(PolicyConfig {
|
||||
levels: {
|
||||
let mut levels = HashMap::new();
|
||||
levels.insert("0".to_string(), PolicyLevel {
|
||||
handshake_timeout: Some(4),
|
||||
conn_idle: Some(300),
|
||||
uplink_only: Some(2),
|
||||
downlink_only: Some(5),
|
||||
});
|
||||
levels
|
||||
},
|
||||
system: Some(SystemPolicy {
|
||||
stats_inbound_uplink: Some(true),
|
||||
stats_inbound_downlink: Some(true),
|
||||
}),
|
||||
}),
|
||||
inbounds: vec![],
|
||||
outbounds: vec![
|
||||
OutboundConfig {
|
||||
tag: "direct".to_string(),
|
||||
protocol: "freedom".to_string(),
|
||||
settings: serde_json::json!({}),
|
||||
stream_settings: None,
|
||||
mux: None,
|
||||
},
|
||||
OutboundConfig {
|
||||
tag: "blocked".to_string(),
|
||||
protocol: "blackhole".to_string(),
|
||||
settings: serde_json::json!({
|
||||
"response": {
|
||||
"type": "http"
|
||||
}
|
||||
}),
|
||||
stream_settings: None,
|
||||
mux: None,
|
||||
},
|
||||
],
|
||||
transport: None,
|
||||
stats: Some(StatsConfig {}),
|
||||
reverse: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add inbound to configuration
|
||||
pub fn add_inbound(&mut self, inbound: InboundConfig) {
|
||||
self.inbounds.push(inbound);
|
||||
}
|
||||
|
||||
/// Remove inbound by tag
|
||||
pub fn remove_inbound(&mut self, tag: &str) -> bool {
|
||||
let initial_len = self.inbounds.len();
|
||||
self.inbounds.retain(|inbound| inbound.tag != tag);
|
||||
self.inbounds.len() != initial_len
|
||||
}
|
||||
|
||||
/// Find inbound by tag
|
||||
pub fn find_inbound(&self, tag: &str) -> Option<&InboundConfig> {
|
||||
self.inbounds.iter().find(|inbound| inbound.tag == tag)
|
||||
}
|
||||
|
||||
/// Find inbound by tag (mutable)
|
||||
pub fn find_inbound_mut(&mut self, tag: &str) -> Option<&mut InboundConfig> {
|
||||
self.inbounds.iter_mut().find(|inbound| inbound.tag == tag)
|
||||
}
|
||||
|
||||
/// Convert to JSON Value
|
||||
pub fn to_json(&self) -> Value {
|
||||
serde_json::to_value(self).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
/// Create from JSON Value
|
||||
pub fn from_json(value: &Value) -> Result<Self, serde_json::Error> {
|
||||
serde_json::from_value(value.clone())
|
||||
}
|
||||
|
||||
/// Validate configuration
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
// Check for duplicate inbound tags
|
||||
let mut tags = std::collections::HashSet::new();
|
||||
for inbound in &self.inbounds {
|
||||
if !tags.insert(&inbound.tag) {
|
||||
return Err(format!("Duplicate inbound tag: {}", inbound.tag));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate outbound tags
|
||||
tags.clear();
|
||||
for outbound in &self.outbounds {
|
||||
if !tags.insert(&outbound.tag) {
|
||||
return Err(format!("Duplicate outbound tag: {}", outbound.tag));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for XrayConfig {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
325
src/services/xray/inbounds.rs
Normal file
325
src/services/xray/inbounds.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde_json::Value;
|
||||
use xray_core::{
|
||||
tonic::Request,
|
||||
app::proxyman::command::{AddInboundRequest, RemoveInboundRequest},
|
||||
core::InboundHandlerConfig,
|
||||
common::serial::TypedMessage,
|
||||
common::protocol::User,
|
||||
app::proxyman::ReceiverConfig,
|
||||
common::net::{PortList, PortRange},
|
||||
transport::internet::StreamConfig,
|
||||
transport::internet::tls::{Config as TlsConfig, Certificate as TlsCertificate},
|
||||
proxy::vless::inbound::Config as VlessInboundConfig,
|
||||
proxy::vless::Account as VlessAccount,
|
||||
proxy::vmess::inbound::Config as VmessInboundConfig,
|
||||
proxy::vmess::Account as VmessAccount,
|
||||
proxy::trojan::ServerConfig as TrojanServerConfig,
|
||||
proxy::trojan::Account as TrojanAccount,
|
||||
proxy::shadowsocks::ServerConfig as ShadowsocksServerConfig,
|
||||
proxy::shadowsocks::Account as ShadowsocksAccount,
|
||||
Client,
|
||||
prost_types,
|
||||
};
|
||||
use prost::Message;
|
||||
|
||||
pub struct InboundClient<'a> {
|
||||
endpoint: String,
|
||||
client: &'a Client,
|
||||
}
|
||||
|
||||
impl<'a> InboundClient<'a> {
|
||||
pub fn new(endpoint: String, client: &'a Client) -> Self {
|
||||
Self { endpoint, client }
|
||||
}
|
||||
|
||||
/// Add inbound configuration
|
||||
pub async fn add_inbound(&self, inbound: &Value) -> Result<()> {
|
||||
self.add_inbound_with_certificate(inbound, None, None, None).await
|
||||
}
|
||||
|
||||
/// Add inbound configuration with TLS certificate and users
|
||||
pub async fn add_inbound_with_certificate(&self, inbound: &Value, users: Option<&[Value]>, cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
tracing::info!("Adding inbound to Xray server at {}", self.endpoint);
|
||||
tracing::debug!("Inbound config: {}", serde_json::to_string_pretty(inbound)?);
|
||||
|
||||
let tag = inbound["tag"].as_str().unwrap_or("").to_string();
|
||||
let port = inbound["port"].as_u64().unwrap_or(8080) as u32;
|
||||
let protocol = inbound["protocol"].as_str().unwrap_or("vless");
|
||||
|
||||
tracing::debug!("Creating inbound: tag={}, port={}, protocol={}", tag, port, protocol);
|
||||
|
||||
// Create receiver configuration (port binding) - use simple port number
|
||||
let port_list = PortList {
|
||||
range: vec![PortRange {
|
||||
from: port,
|
||||
to: port,
|
||||
}],
|
||||
};
|
||||
|
||||
// Create stream settings with TLS if certificates are provided
|
||||
let stream_settings = if cert_pem.is_some() && key_pem.is_some() {
|
||||
let cert_pem = cert_pem.unwrap();
|
||||
let key_pem = key_pem.unwrap();
|
||||
|
||||
tracing::info!("Creating TLS stream settings for inbound");
|
||||
tracing::debug!("Certificate length: {}, Key length: {}", cert_pem.len(), key_pem.len());
|
||||
|
||||
// Create TLS certificate with OneTimeLoading = true
|
||||
// Convert PEM strings to byte vectors (certificate should be raw bytes, not PEM string)
|
||||
let tls_cert = TlsCertificate {
|
||||
certificate: cert_pem.as_bytes().to_vec(), // PEM as bytes
|
||||
key: key_pem.as_bytes().to_vec(), // PEM key as bytes
|
||||
usage: 0, // Default usage
|
||||
ocsp_stapling: 0, // Default OCSP
|
||||
one_time_loading: true, // OneTimeLoading = true as in example
|
||||
build_chain: false,
|
||||
certificate_path: "".to_string(),
|
||||
key_path: "".to_string(),
|
||||
};
|
||||
|
||||
// Create TLS config using Default and set only necessary fields
|
||||
let mut tls_config = TlsConfig::default();
|
||||
tls_config.certificate = vec![tls_cert];
|
||||
|
||||
// Create TLS security settings using prost_types::Any instead of TypedMessage
|
||||
let tls_any = prost_types::Any::from_msg(&tls_config)
|
||||
.map_err(|e| anyhow!("Failed to serialize TLS config: {}", e))?;
|
||||
|
||||
let tls_message = TypedMessage {
|
||||
r#type: tls_any.type_url,
|
||||
value: tls_any.value,
|
||||
};
|
||||
|
||||
// Create stream config with TLS security settings
|
||||
Some(StreamConfig {
|
||||
address: None,
|
||||
port: port,
|
||||
protocol_name: "tcp".to_string(),
|
||||
transport_settings: vec![],
|
||||
security_type: "tls".to_string(),
|
||||
security_settings: vec![tls_message],
|
||||
socket_settings: None,
|
||||
})
|
||||
} else {
|
||||
tracing::info!("No certificates provided, creating inbound without TLS");
|
||||
None
|
||||
};
|
||||
|
||||
let receiver_config = ReceiverConfig {
|
||||
port_list: Some(port_list),
|
||||
listen: None,
|
||||
allocation_strategy: None,
|
||||
stream_settings: stream_settings,
|
||||
receive_original_destination: false,
|
||||
sniffing_settings: None,
|
||||
};
|
||||
|
||||
let receiver_message = TypedMessage {
|
||||
r#type: "xray.app.proxyman.ReceiverConfig".to_string(),
|
||||
value: receiver_config.encode_to_vec(),
|
||||
};
|
||||
|
||||
// Create proxy configuration based on protocol with users
|
||||
let proxy_message = match protocol {
|
||||
"vless" => {
|
||||
let mut clients = vec![];
|
||||
if let Some(users) = users {
|
||||
for user in users {
|
||||
let user_id = user["id"].as_str().unwrap_or("").to_string();
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
|
||||
if !user_id.is_empty() && !email.is_empty() {
|
||||
let account = VlessAccount {
|
||||
id: user_id,
|
||||
encryption: "none".to_string(),
|
||||
flow: "".to_string(),
|
||||
};
|
||||
clients.push(User {
|
||||
email,
|
||||
level,
|
||||
account: Some(TypedMessage {
|
||||
r#type: "xray.proxy.vless.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let vless_config = VlessInboundConfig {
|
||||
clients,
|
||||
decryption: "none".to_string(),
|
||||
fallbacks: vec![],
|
||||
};
|
||||
TypedMessage {
|
||||
r#type: "xray.proxy.vless.inbound.Config".to_string(),
|
||||
value: vless_config.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
"vmess" => {
|
||||
let mut vmess_users = vec![];
|
||||
if let Some(users) = users {
|
||||
for user in users {
|
||||
let user_id = user["id"].as_str().unwrap_or("").to_string();
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
|
||||
if !user_id.is_empty() && !email.is_empty() {
|
||||
let account = VmessAccount {
|
||||
id: user_id,
|
||||
security_settings: None,
|
||||
tests_enabled: "".to_string(),
|
||||
};
|
||||
vmess_users.push(User {
|
||||
email,
|
||||
level,
|
||||
account: Some(TypedMessage {
|
||||
r#type: "xray.proxy.vmess.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let vmess_config = VmessInboundConfig {
|
||||
user: vmess_users,
|
||||
default: None,
|
||||
detour: None,
|
||||
};
|
||||
TypedMessage {
|
||||
r#type: "xray.proxy.vmess.inbound.Config".to_string(),
|
||||
value: vmess_config.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
"trojan" => {
|
||||
let mut trojan_users = vec![];
|
||||
if let Some(users) = users {
|
||||
for user in users {
|
||||
let password = user["password"].as_str().or_else(|| user["id"].as_str()).unwrap_or("").to_string();
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
|
||||
if !password.is_empty() && !email.is_empty() {
|
||||
let account = TrojanAccount {
|
||||
password,
|
||||
};
|
||||
trojan_users.push(User {
|
||||
email,
|
||||
level,
|
||||
account: Some(TypedMessage {
|
||||
r#type: "xray.proxy.trojan.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let trojan_config = TrojanServerConfig {
|
||||
users: trojan_users,
|
||||
fallbacks: vec![],
|
||||
};
|
||||
TypedMessage {
|
||||
r#type: "xray.proxy.trojan.ServerConfig".to_string(),
|
||||
value: trojan_config.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
"shadowsocks" => {
|
||||
let mut ss_users = vec![];
|
||||
if let Some(users) = users {
|
||||
for user in users {
|
||||
let password = user["password"].as_str().or_else(|| user["id"].as_str()).unwrap_or("").to_string();
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
|
||||
if !password.is_empty() && !email.is_empty() {
|
||||
let account = ShadowsocksAccount {
|
||||
password,
|
||||
cipher_type: 0, // Default cipher
|
||||
iv_check: false, // Default IV check
|
||||
};
|
||||
ss_users.push(User {
|
||||
email,
|
||||
level,
|
||||
account: Some(TypedMessage {
|
||||
r#type: "xray.proxy.shadowsocks.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let shadowsocks_config = ShadowsocksServerConfig {
|
||||
users: ss_users,
|
||||
network: vec![], // Support all networks by default
|
||||
};
|
||||
TypedMessage {
|
||||
r#type: "xray.proxy.shadowsocks.ServerConfig".to_string(),
|
||||
value: shadowsocks_config.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow!("Unsupported protocol: {}", protocol));
|
||||
}
|
||||
};
|
||||
|
||||
let inbound_config = InboundHandlerConfig {
|
||||
tag: tag.clone(),
|
||||
receiver_settings: Some(receiver_message),
|
||||
proxy_settings: Some(proxy_message),
|
||||
};
|
||||
|
||||
let request = Request::new(AddInboundRequest {
|
||||
inbound: Some(inbound_config),
|
||||
});
|
||||
|
||||
tracing::info!("Sending AddInboundRequest for '{}'", tag);
|
||||
let mut handler_client = self.client.handler();
|
||||
match handler_client.add_inbound(request).await {
|
||||
Ok(response) => {
|
||||
let _response_inner = response.into_inner();
|
||||
tracing::info!("Successfully added inbound {}", tag);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to add inbound {}: {}", tag, e);
|
||||
Err(anyhow!("Failed to add inbound {}: {}", tag, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove inbound by tag
|
||||
pub async fn remove_inbound(&self, tag: &str) -> Result<()> {
|
||||
tracing::info!("Removing inbound '{}' from Xray server at {}", tag, self.endpoint);
|
||||
|
||||
let mut handler_client = self.client.handler();
|
||||
let request = Request::new(RemoveInboundRequest {
|
||||
tag: tag.to_string(),
|
||||
});
|
||||
|
||||
match handler_client.remove_inbound(request).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Successfully removed inbound");
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove inbound: {}", e);
|
||||
Err(anyhow!("Failed to remove inbound: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Restart Xray with new configuration
|
||||
pub async fn restart_with_config(&self, config: &crate::services::xray::XrayConfig) -> Result<()> {
|
||||
tracing::info!("Restarting Xray server at {} with new config", self.endpoint);
|
||||
tracing::debug!("Config: {}", serde_json::to_string_pretty(&config.to_json())?);
|
||||
|
||||
// TODO: Implement restart with config using xray-core
|
||||
// For now just return success
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
213
src/services/xray/mod.rs
Normal file
213
src/services/xray/mod.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod stats;
|
||||
pub mod inbounds;
|
||||
pub mod users;
|
||||
|
||||
pub use client::XrayClient;
|
||||
pub use config::XrayConfig;
|
||||
|
||||
/// Service for managing Xray servers via gRPC
|
||||
#[derive(Clone)]
|
||||
pub struct XrayService {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl XrayService {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
/// Create a client for the specified server
|
||||
async fn create_client(&self, endpoint: &str) -> Result<XrayClient> {
|
||||
XrayClient::connect(endpoint).await
|
||||
}
|
||||
|
||||
/// Test connection to Xray server
|
||||
pub async fn test_connection(&self, _server_id: Uuid, endpoint: &str) -> Result<bool> {
|
||||
match self.create_client(endpoint).await {
|
||||
Ok(_client) => {
|
||||
// Instead of getting stats (which might fail), just test connection
|
||||
// If we successfully created the client, connection is working
|
||||
Ok(true)
|
||||
},
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply full configuration to Xray server
|
||||
pub async fn apply_config(&self, _server_id: Uuid, endpoint: &str, config: &XrayConfig) -> Result<()> {
|
||||
let client = self.create_client(endpoint).await?;
|
||||
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
|
||||
});
|
||||
|
||||
tracing::info!("Creating inbound with config: {}", inbound_config);
|
||||
self.add_inbound(_server_id, endpoint, &inbound_config).await
|
||||
}
|
||||
|
||||
/// 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
|
||||
});
|
||||
|
||||
tracing::info!("Creating inbound with TLS certificate and config: {}", inbound_config);
|
||||
self.add_inbound_with_certificate(_server_id, endpoint, &inbound_config, cert_pem, key_pem).await
|
||||
}
|
||||
|
||||
/// Add inbound to running Xray instance
|
||||
pub async fn add_inbound(&self, _server_id: Uuid, endpoint: &str, inbound: &Value) -> Result<()> {
|
||||
let client = self.create_client(endpoint).await?;
|
||||
client.add_inbound(inbound).await
|
||||
}
|
||||
|
||||
/// Add inbound with certificate to running Xray instance
|
||||
pub async fn add_inbound_with_certificate(&self, _server_id: Uuid, endpoint: &str, inbound: &Value, cert_pem: Option<&str>, key_pem: Option<&str>) -> Result<()> {
|
||||
let client = self.create_client(endpoint).await?;
|
||||
client.add_inbound_with_certificate(inbound, cert_pem, key_pem).await
|
||||
}
|
||||
|
||||
/// Add inbound with users and certificate to running Xray instance
|
||||
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<()> {
|
||||
let client = self.create_client(endpoint).await?;
|
||||
client.add_inbound_with_users_and_certificate(inbound, users, cert_pem, key_pem).await
|
||||
}
|
||||
|
||||
/// Remove inbound from running Xray instance
|
||||
pub async fn remove_inbound(&self, _server_id: Uuid, endpoint: &str, tag: &str) -> Result<()> {
|
||||
let client = self.create_client(endpoint).await?;
|
||||
client.remove_inbound(tag).await
|
||||
}
|
||||
|
||||
/// Add user to inbound by recreating the inbound with updated user list
|
||||
pub async fn add_user(&self, _server_id: Uuid, endpoint: &str, inbound_tag: &str, user: &Value) -> Result<()> {
|
||||
tracing::info!("XrayService::add_user called for server {} endpoint {} inbound_tag {}", _server_id, endpoint, inbound_tag);
|
||||
tracing::warn!("Dynamic user addition via AlterInboundRequest doesn't work reliably - need to implement inbound recreation");
|
||||
|
||||
// TODO: Implement inbound recreation approach:
|
||||
// 1. Get current inbound configuration from database
|
||||
// 2. Get existing users from database
|
||||
// 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
|
||||
|
||||
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<()> {
|
||||
tracing::info!("Creating inbound '{}' with {} users", tag, users.len());
|
||||
|
||||
// Build inbound configuration with users
|
||||
let mut inbound_config = serde_json::json!({
|
||||
"tag": tag,
|
||||
"port": port,
|
||||
"protocol": protocol,
|
||||
"settings": base_settings,
|
||||
"streamSettings": stream_settings
|
||||
});
|
||||
|
||||
// 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());
|
||||
},
|
||||
"trojan" => {
|
||||
settings["clients"] = serde_json::Value::Array(users.to_vec());
|
||||
},
|
||||
"shadowsocks" => {
|
||||
// For shadowsocks, users are handled differently
|
||||
if let Some(user) = users.first() {
|
||||
settings["password"] = user["password"].clone();
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!("Unsupported protocol for users: {}", protocol));
|
||||
}
|
||||
}
|
||||
inbound_config["settings"] = settings;
|
||||
}
|
||||
|
||||
tracing::info!("Creating inbound with users: {}", serde_json::to_string_pretty(&inbound_config)?);
|
||||
|
||||
// Use the new method with users support
|
||||
self.add_inbound_with_users_and_certificate(_server_id, endpoint, &inbound_config, users, cert_pem, key_pem).await
|
||||
}
|
||||
|
||||
/// Remove user from inbound
|
||||
pub async fn remove_user(&self, _server_id: Uuid, endpoint: &str, inbound_tag: &str, email: &str) -> Result<()> {
|
||||
let client = self.create_client(endpoint).await?;
|
||||
client.remove_user(inbound_tag, email).await
|
||||
}
|
||||
|
||||
/// Get server statistics
|
||||
pub async fn get_stats(&self, _server_id: Uuid, endpoint: &str) -> Result<Value> {
|
||||
let client = self.create_client(endpoint).await?;
|
||||
client.get_stats().await
|
||||
}
|
||||
|
||||
/// Query specific statistics
|
||||
pub async fn query_stats(&self, _server_id: Uuid, endpoint: &str, pattern: &str, reset: bool) -> Result<Value> {
|
||||
let client = self.create_client(endpoint).await?;
|
||||
client.query_stats(pattern, reset).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for XrayService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
70
src/services/xray/stats.rs
Normal file
70
src/services/xray/stats.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde_json::Value;
|
||||
use xray_core::{
|
||||
tonic::Request,
|
||||
app::stats::command::{GetStatsRequest, QueryStatsRequest},
|
||||
Client,
|
||||
};
|
||||
|
||||
pub struct StatsClient<'a> {
|
||||
endpoint: String,
|
||||
client: &'a Client,
|
||||
}
|
||||
|
||||
impl<'a> StatsClient<'a> {
|
||||
pub fn new(endpoint: String, client: &'a Client) -> Self {
|
||||
Self { endpoint, client }
|
||||
}
|
||||
|
||||
/// Get server statistics
|
||||
pub async fn get_stats(&self) -> Result<Value> {
|
||||
tracing::info!("Getting stats from Xray server at {}", self.endpoint);
|
||||
|
||||
let request = Request::new(GetStatsRequest {
|
||||
name: "".to_string(),
|
||||
reset: false,
|
||||
});
|
||||
|
||||
let mut stats_client = self.client.stats();
|
||||
match stats_client.get_stats(request).await {
|
||||
Ok(response) => {
|
||||
let stats = response.into_inner();
|
||||
tracing::debug!("Stats: {:?}", stats);
|
||||
let stats_json = serde_json::json!({
|
||||
"stats": format!("{:?}", stats.stat)
|
||||
});
|
||||
Ok(stats_json)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get stats: {}", e);
|
||||
Err(anyhow!("Failed to get stats: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Query specific statistics with pattern
|
||||
pub async fn query_stats(&self, pattern: &str, reset: bool) -> Result<Value> {
|
||||
tracing::info!("Querying stats with pattern '{}', reset: {} from {}", pattern, reset, self.endpoint);
|
||||
|
||||
let request = Request::new(QueryStatsRequest {
|
||||
pattern: pattern.to_string(),
|
||||
reset,
|
||||
});
|
||||
|
||||
let mut stats_client = self.client.stats();
|
||||
match stats_client.query_stats(request).await {
|
||||
Ok(response) => {
|
||||
let stats = response.into_inner();
|
||||
tracing::debug!("Query stats: {:?}", stats);
|
||||
let stats_json = serde_json::json!({
|
||||
"stat": format!("{:?}", stats.stat)
|
||||
});
|
||||
Ok(stats_json)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to query stats: {}", e);
|
||||
Err(anyhow!("Failed to query stats: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
150
src/services/xray/users.rs
Normal file
150
src/services/xray/users.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde_json::Value;
|
||||
use xray_core::{
|
||||
tonic::Request,
|
||||
app::proxyman::command::{AlterInboundRequest, AddUserOperation, RemoveUserOperation},
|
||||
common::serial::TypedMessage,
|
||||
common::protocol::User,
|
||||
proxy::vless::Account as VlessAccount,
|
||||
proxy::vmess::Account as VmessAccount,
|
||||
proxy::trojan::Account as TrojanAccount,
|
||||
Client,
|
||||
};
|
||||
use prost::Message;
|
||||
|
||||
pub struct UserClient<'a> {
|
||||
endpoint: String,
|
||||
client: &'a Client,
|
||||
}
|
||||
|
||||
impl<'a> UserClient<'a> {
|
||||
pub fn new(endpoint: String, client: &'a Client) -> Self {
|
||||
Self { endpoint, client }
|
||||
}
|
||||
|
||||
/// Add user to inbound (simple version that works)
|
||||
pub async fn add_user(&self, inbound_tag: &str, user: &Value) -> Result<()> {
|
||||
tracing::info!("Adding user to inbound '{}' on Xray server at {}", inbound_tag, self.endpoint);
|
||||
tracing::debug!("User config: {}", serde_json::to_string_pretty(user)?);
|
||||
|
||||
let email = user["email"].as_str().unwrap_or("").to_string();
|
||||
let user_id = user["id"].as_str().unwrap_or("").to_string();
|
||||
let level = user["level"].as_u64().unwrap_or(0) as u32;
|
||||
let protocol = user["protocol"].as_str().unwrap_or("vless");
|
||||
|
||||
tracing::info!("Parsed user data: email={}, id={}, level={}, protocol={}", email, user_id, level, protocol);
|
||||
|
||||
if email.is_empty() || user_id.is_empty() {
|
||||
return Err(anyhow!("User email and id are required"));
|
||||
}
|
||||
|
||||
// Create user account based on protocol
|
||||
let account_message = match protocol {
|
||||
"vless" => {
|
||||
let account = VlessAccount {
|
||||
id: user_id.clone(),
|
||||
encryption: "none".to_string(),
|
||||
flow: "".to_string(), // Empty flow for basic VLESS
|
||||
};
|
||||
TypedMessage {
|
||||
r#type: "xray.proxy.vless.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
"vmess" => {
|
||||
let account = VmessAccount {
|
||||
id: user_id,
|
||||
security_settings: None,
|
||||
tests_enabled: "".to_string(),
|
||||
};
|
||||
TypedMessage {
|
||||
r#type: "xray.proxy.vmess.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
"trojan" => {
|
||||
let account = TrojanAccount {
|
||||
password: user_id, // For trojan, use password instead of UUID
|
||||
};
|
||||
TypedMessage {
|
||||
r#type: "xray.proxy.trojan.Account".to_string(),
|
||||
value: account.encode_to_vec(),
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow!("Unsupported protocol for user: {}", protocol));
|
||||
}
|
||||
};
|
||||
|
||||
// Create user protobuf message
|
||||
let user_proto = User {
|
||||
level: level,
|
||||
email: email.clone(),
|
||||
account: Some(account_message),
|
||||
};
|
||||
|
||||
// Build the AddUserOperation
|
||||
let add_user_op = AddUserOperation {
|
||||
user: Some(user_proto),
|
||||
};
|
||||
|
||||
let typed_message = TypedMessage {
|
||||
r#type: "xray.app.proxyman.command.AddUserOperation".to_string(),
|
||||
value: add_user_op.encode_to_vec(),
|
||||
};
|
||||
|
||||
// Build the AlterInboundRequest
|
||||
let request = Request::new(AlterInboundRequest {
|
||||
tag: inbound_tag.to_string(),
|
||||
operation: Some(typed_message),
|
||||
});
|
||||
|
||||
tracing::info!("Sending AlterInboundRequest to add user '{}' to inbound '{}'", email, inbound_tag);
|
||||
|
||||
let mut handler_client = self.client.handler();
|
||||
match handler_client.alter_inbound(request).await {
|
||||
Ok(response) => {
|
||||
let _response_inner = response.into_inner();
|
||||
tracing::info!("Successfully added user '{}' to inbound '{}'", email, inbound_tag);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("gRPC error adding user '{}' to inbound '{}': status={}, message={}",
|
||||
email, inbound_tag, e.code(), e.message());
|
||||
Err(anyhow!("Failed to add user '{}' to inbound '{}': {}", email, inbound_tag, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove user from inbound
|
||||
pub async fn remove_user(&self, inbound_tag: &str, email: &str) -> Result<()> {
|
||||
tracing::info!("Removing user '{}' from inbound '{}' on Xray server at {}", email, inbound_tag, self.endpoint);
|
||||
|
||||
// Build the RemoveUserOperation
|
||||
let remove_user_op = RemoveUserOperation {
|
||||
email: email.to_string(),
|
||||
};
|
||||
|
||||
let typed_message = TypedMessage {
|
||||
r#type: "xray.app.proxyman.command.RemoveUserOperation".to_string(),
|
||||
value: remove_user_op.encode_to_vec(),
|
||||
};
|
||||
|
||||
let request = Request::new(AlterInboundRequest {
|
||||
tag: inbound_tag.to_string(),
|
||||
operation: Some(typed_message),
|
||||
});
|
||||
|
||||
let mut handler_client = self.client.handler();
|
||||
match handler_client.alter_inbound(request).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Successfully removed user '{}' from inbound '{}'", email, inbound_tag);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove user '{}' from inbound '{}': {}", email, inbound_tag, e);
|
||||
Err(anyhow!("Failed to remove user '{}' from inbound '{}': {}", email, inbound_tag, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user