mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-24 17:29:08 +00:00
325 lines
14 KiB
Rust
325 lines
14 KiB
Rust
|
|
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(())
|
||
|
|
}
|
||
|
|
}
|