use std::fmt;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ProcessState {
Running,
Stop,
Error,
#[default]
#[serde(other)]
Unknown,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct ResourceUsage {
pub current: u64,
pub total: u64,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct DiskIo {
pub read: u64,
pub write: u64,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct NetworkIo {
pub up: u64,
pub down: u64,
pub pkt_up: u64,
pub pkt_down: u64,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct NetworkTraffic {
pub sent: u64,
pub recv: u64,
pub pkt_sent: u64,
pub pkt_recv: u64,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct XrayStatus {
pub state: ProcessState,
pub error_msg: String,
pub version: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct AmneziaWgPeerActivity {
#[serde(rename = "interface")]
pub interface_name: String,
pub tag: String,
pub inbound_id: i64,
pub email: String,
pub endpoint: String,
#[serde(rename = "allowedIPs")]
pub allowed_ips: String,
pub handshake: i64,
pub up: i64,
pub down: i64,
pub online: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct AmneziaWgLogs {
pub peers: Vec<AmneziaWgPeerActivity>,
pub events: Vec<String>,
pub running: bool,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct AmneziaWgStatus {
pub configured: bool,
pub running: bool,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(transparent)]
pub struct XrayConfig(Value);
impl XrayConfig {
pub const fn as_value(&self) -> &Value {
&self.0
}
pub fn into_value(self) -> Value {
self.0
}
}
impl From<Value> for XrayConfig {
fn from(value: Value) -> Self {
Self(value)
}
}
impl fmt::Debug for XrayConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("XrayConfig")
.field(&"[REDACTED]")
.finish()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct PublicAddresses {
pub ipv4: String,
pub ipv6: String,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct AppStats {
pub threads: u32,
pub mem: u64,
pub uptime: u64,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ServerStatus {
pub cpu: f64,
pub cpu_cores: i32,
pub logical_pro: i32,
pub cpu_speed_mhz: f64,
pub mem: ResourceUsage,
pub swap: ResourceUsage,
pub disk: ResourceUsage,
#[serde(rename = "diskIO")]
pub disk_io: DiskIo,
pub disk_traffic: DiskIo,
pub xray: XrayStatus,
pub amneziawg: AmneziaWgStatus,
pub panel_version: String,
pub panel_guid: String,
pub uptime: u64,
#[serde(deserialize_with = "deserialize_null_default")]
pub loads: Vec<f64>,
pub tcp_count: i32,
pub udp_count: i32,
#[serde(rename = "netIO")]
pub net_io: NetworkIo,
pub net_traffic: NetworkTraffic,
#[serde(rename = "publicIP")]
pub public_ip: PublicAddresses,
pub app_stats: AppStats,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum HistoryBucket {
Minutes2,
Minutes30,
Hour1,
Hours3,
Hours6,
Hours12,
Day1,
Days2,
Days7,
}
impl HistoryBucket {
pub(crate) const fn seconds(self) -> u32 {
match self {
Self::Minutes2 => 2,
Self::Minutes30 => 30,
Self::Hour1 => 60,
Self::Hours3 => 180,
Self::Hours6 => 360,
Self::Hours12 => 720,
Self::Day1 => 1_440,
Self::Days2 => 2_880,
Self::Days7 => 10_080,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum SystemMetric {
Cpu,
Memory,
Swap,
NetworkUp,
NetworkDown,
PacketUp,
PacketDown,
DiskRead,
DiskWrite,
DiskUsage,
TcpCount,
UdpCount,
Online,
Load1,
Load5,
Load15,
}
impl SystemMetric {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Cpu => "cpu",
Self::Memory => "mem",
Self::Swap => "swap",
Self::NetworkUp => "netUp",
Self::NetworkDown => "netDown",
Self::PacketUp => "pktUp",
Self::PacketDown => "pktDown",
Self::DiskRead => "diskRead",
Self::DiskWrite => "diskWrite",
Self::DiskUsage => "diskUsage",
Self::TcpCount => "tcpCount",
Self::UdpCount => "udpCount",
Self::Online => "online",
Self::Load1 => "load1",
Self::Load5 => "load5",
Self::Load15 => "load15",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum XrayMetric {
Alloc,
Sys,
HeapObjects,
NumGc,
PauseNs,
}
impl XrayMetric {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Alloc => "xrAlloc",
Self::Sys => "xrSys",
Self::HeapObjects => "xrHeapObjects",
Self::NumGc => "xrNumGC",
Self::PauseNs => "xrPauseNs",
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct MetricPoint {
#[serde(rename = "t")]
pub timestamp: i64,
#[serde(rename = "v")]
pub value: f64,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct LegacyCpuPoint {
#[serde(rename = "t")]
pub timestamp: i64,
pub cpu: f64,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct XrayMetricsState {
pub enabled: bool,
pub listen: String,
pub reason: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct XrayObservatoryEntry {
pub tag: String,
pub alive: bool,
pub delay: i64,
pub last_seen_time: i64,
pub last_try_time: i64,
pub updated_at: i64,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PanelUpdateInfo {
pub channel: String,
pub current_version: String,
pub latest_version: String,
pub current_commit: String,
pub latest_commit: String,
pub update_available: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PanelUpdateRun {
pub run_id: String,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum PanelUpdateState {
Pending,
Success,
Failed,
#[default]
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PanelUpdateStatus {
pub run_id: String,
pub state: PanelUpdateState,
pub exit_code: i32,
pub finished_at: i64,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
#[allow(clippy::struct_excessive_bools)]
pub struct Fail2banStatus {
pub enabled: bool,
pub installed: bool,
pub usable: bool,
pub windows: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct NodeSummary {
pub guid: String,
pub parent_guid: String,
pub name: String,
pub address: String,
pub scheme: String,
pub port: i32,
pub status: String,
pub last_heartbeat: i64,
pub latency_ms: i32,
pub panel_version: String,
pub xray_version: String,
pub xray_state: String,
pub xray_error: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ClientIpObservation {
pub ip: String,
pub timestamp: i64,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ClientIpRecord {
pub id: i32,
pub client_email: String,
#[serde(deserialize_with = "deserialize_null_default")]
pub ips: Vec<ClientIpObservation>,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct X25519KeyPair {
pub private_key: String,
pub public_key: String,
}
impl fmt::Debug for X25519KeyPair {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("X25519KeyPair")
.field("private_key", &"[REDACTED]")
.field("public_key", &self.public_key)
.finish()
}
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct Mldsa65KeyPair {
pub seed: String,
pub verify: String,
}
impl fmt::Debug for Mldsa65KeyPair {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Mldsa65KeyPair")
.field("seed", &"[REDACTED]")
.field("verify", &self.verify)
.finish()
}
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct Mlkem768KeyPair {
pub seed: String,
pub client: String,
}
impl fmt::Debug for Mlkem768KeyPair {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Mlkem768KeyPair")
.field("seed", &"[REDACTED]")
.field("client", &self.client)
.finish()
}
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EchKeyPair {
pub ech_server_keys: String,
pub ech_config_list: String,
}
impl fmt::Debug for EchKeyPair {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EchKeyPair")
.field("ech_server_keys", &"[REDACTED]")
.field("ech_config_list", &self.ech_config_list)
.finish()
}
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct VlessEncryptionAuth {
pub id: String,
pub label: String,
pub encryption: String,
pub decryption: String,
}
impl fmt::Debug for VlessEncryptionAuth {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VlessEncryptionAuth")
.field("id", &self.id)
.field("label", &self.label)
.field("encryption", &"[REDACTED]")
.field("decryption", &"[REDACTED]")
.finish()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct VlessEncryptionOptions {
#[serde(deserialize_with = "deserialize_null_default")]
pub auths: Vec<VlessEncryptionAuth>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct WebCertificateFiles {
pub web_cert_file: String,
pub web_key_file: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
#[allow(clippy::struct_excessive_bools)]
pub struct RealityScanResult {
pub target: String,
pub host: String,
#[serde(rename = "ip")]
pub ip_address: String,
pub port: i32,
pub feasible: bool,
#[serde(rename = "tls13", alias = "TLS13")]
pub tls13: bool,
#[serde(rename = "tlsVersion", alias = "TLSVersion")]
pub tls_version: String,
#[serde(rename = "h2", alias = "H2")]
pub h2: bool,
#[serde(rename = "alpn", alias = "ALPN")]
pub alpn: String,
#[serde(rename = "x25519", alias = "X25519")]
pub x25519: bool,
#[serde(rename = "curveID", alias = "curveId")]
pub curve_id: String,
pub private_target: bool,
pub cert_chain_valid: bool,
pub cert_chain_bytes: i32,
pub cert_valid: bool,
pub cert_subject: String,
pub cert_issuer: String,
pub not_after: String,
#[serde(deserialize_with = "deserialize_null_default")]
pub server_names: Vec<String>,
pub latency_ms: i32,
pub reason: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RealityScanRequest {
pub target: String,
pub xray_version: i32,
}
impl RealityScanRequest {
pub fn new(target: impl Into<String>) -> Self {
Self {
target: target.into(),
xray_version: 0,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum LogLevel {
#[default]
All,
Debug,
Info,
Warning,
Error,
}
impl LogLevel {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::All => "",
Self::Debug => "debug",
Self::Info => "info",
Self::Warning => "warning",
Self::Error => "error",
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct PanelLogRequest {
pub count: u32,
pub level: LogLevel,
pub syslog: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct XrayLogRequest {
pub count: u32,
pub filter: String,
pub show_direct: bool,
pub show_blocked: bool,
pub show_proxy: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum XrayLogEvent {
#[default]
Direct,
Blocked,
Proxied,
Unknown,
}
impl From<i32> for XrayLogEvent {
fn from(value: i32) -> Self {
match value {
0 => Self::Direct,
1 => Self::Blocked,
2 => Self::Proxied,
_ => Self::Unknown,
}
}
}
impl From<XrayLogEvent> for i32 {
fn from(value: XrayLogEvent) -> Self {
match value {
XrayLogEvent::Direct => 0,
XrayLogEvent::Blocked => 1,
XrayLogEvent::Proxied => 2,
XrayLogEvent::Unknown => -1,
}
}
}
impl<'de> Deserialize<'de> for XrayLogEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
i32::deserialize(deserializer).map(Self::from)
}
}
impl Serialize for XrayLogEvent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
i32::from(*self).serialize(serializer)
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "PascalCase")]
pub struct XrayLogEntry {
pub date_time: String,
pub from_address: String,
pub to_address: String,
pub inbound: String,
pub outbound: String,
pub email: String,
pub event: XrayLogEvent,
}
#[derive(Clone, Default, Eq, PartialEq)]
pub struct DatabaseFile {
pub filename: Option<String>,
pub content_type: Option<String>,
pub bytes: Vec<u8>,
}
impl fmt::Debug for DatabaseFile {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DatabaseFile")
.field("filename", &self.filename)
.field("content_type", &self.content_type)
.field(
"bytes",
&format_args!("[REDACTED; {} bytes]", self.bytes.len()),
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn history_variants_match_the_source_allowlists() {
let buckets = [
HistoryBucket::Minutes2,
HistoryBucket::Minutes30,
HistoryBucket::Hour1,
HistoryBucket::Hours3,
HistoryBucket::Hours6,
HistoryBucket::Hours12,
HistoryBucket::Day1,
HistoryBucket::Days2,
HistoryBucket::Days7,
]
.map(HistoryBucket::seconds);
assert_eq!(buckets, [2, 30, 60, 180, 360, 720, 1_440, 2_880, 10_080]);
let metrics = [
SystemMetric::Cpu,
SystemMetric::Memory,
SystemMetric::Swap,
SystemMetric::NetworkUp,
SystemMetric::NetworkDown,
SystemMetric::PacketUp,
SystemMetric::PacketDown,
SystemMetric::DiskRead,
SystemMetric::DiskWrite,
SystemMetric::DiskUsage,
SystemMetric::TcpCount,
SystemMetric::UdpCount,
SystemMetric::Online,
SystemMetric::Load1,
SystemMetric::Load5,
SystemMetric::Load15,
]
.map(SystemMetric::as_str);
assert_eq!(
metrics,
[
"cpu",
"mem",
"swap",
"netUp",
"netDown",
"pktUp",
"pktDown",
"diskRead",
"diskWrite",
"diskUsage",
"tcpCount",
"udpCount",
"online",
"load1",
"load5",
"load15",
]
);
}
#[test]
fn open_xray_config_has_explicit_secret_accessors() {
let value = serde_json::json!({"privateKey": "xray-private-secret"});
let config = XrayConfig::from(value.clone());
assert_eq!(config.as_value(), &value);
assert!(!format!("{config:?}").contains("xray-private-secret"));
assert_eq!(config.into_value(), value);
}
#[test]
fn xray_metrics_and_log_filters_match_wire_vocabulary() {
assert_eq!(
[
XrayMetric::Alloc,
XrayMetric::Sys,
XrayMetric::HeapObjects,
XrayMetric::NumGc,
XrayMetric::PauseNs,
]
.map(XrayMetric::as_str),
["xrAlloc", "xrSys", "xrHeapObjects", "xrNumGC", "xrPauseNs"]
);
assert_eq!(
[
LogLevel::All,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warning,
LogLevel::Error,
]
.map(LogLevel::as_str),
["", "debug", "info", "warning", "error"]
);
for (wire, event) in [
(0, XrayLogEvent::Direct),
(1, XrayLogEvent::Blocked),
(2, XrayLogEvent::Proxied),
(99, XrayLogEvent::Unknown),
] {
assert_eq!(XrayLogEvent::from(wire), event);
let expected = if event == XrayLogEvent::Unknown {
-1
} else {
wire
};
assert_eq!(i32::from(event), expected);
assert_eq!(serde_json::to_value(event).unwrap(), expected);
assert_eq!(
serde_json::from_value::<XrayLogEvent>(serde_json::json!(wire)).unwrap(),
event
);
}
}
#[test]
fn generated_key_material_is_redacted_selectively() {
let x25519 = X25519KeyPair {
private_key: "x25519-private-secret".into(),
public_key: "x25519-public".into(),
};
let mldsa = Mldsa65KeyPair {
seed: "mldsa-seed-secret".into(),
verify: "mldsa-verify".into(),
};
let mlkem = Mlkem768KeyPair {
seed: "mlkem-seed-secret".into(),
client: "mlkem-client".into(),
};
let ech = EchKeyPair {
ech_server_keys: "ech-private-secret".into(),
ech_config_list: "ech-public".into(),
};
let vless = VlessEncryptionAuth {
id: "option-1".into(),
label: "recommended".into(),
encryption: "vless-encryption-secret".into(),
decryption: "vless-decryption-secret".into(),
};
let cases = [
(
format!("{x25519:?}"),
"x25519-private-secret",
"x25519-public",
),
(format!("{mldsa:?}"), "mldsa-seed-secret", "mldsa-verify"),
(format!("{mlkem:?}"), "mlkem-seed-secret", "mlkem-client"),
(format!("{ech:?}"), "ech-private-secret", "ech-public"),
(
format!("{vless:?}"),
"vless-encryption-secret",
"recommended",
),
];
for (output, secret, visible) in cases {
assert!(!output.contains(secret));
assert!(output.contains(visible));
}
assert!(!format!("{vless:?}").contains("vless-decryption-secret"));
}
}