Skip to main content

xui_rs/server/
models.rs

1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize};
4use serde_json::Value;
5
6fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
7where
8    D: Deserializer<'de>,
9    T: Deserialize<'de> + Default,
10{
11    Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
12}
13
14/// Current state of the managed Xray process.
15#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
16#[serde(rename_all = "lowercase")]
17#[non_exhaustive]
18pub enum ProcessState {
19    /// Xray is running normally.
20    Running,
21    /// Xray is stopped.
22    Stop,
23    /// Xray failed to start or reload.
24    Error,
25    /// A newer server returned a state unknown to this SDK version.
26    #[default]
27    #[serde(other)]
28    Unknown,
29}
30
31/// Used and total bytes for memory, swap, or disk capacity.
32#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
33#[serde(default)]
34pub struct ResourceUsage {
35    /// Bytes currently used.
36    pub current: u64,
37    /// Total available bytes.
38    pub total: u64,
39}
40
41/// Cumulative disk IO counters.
42#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
43#[serde(default)]
44pub struct DiskIo {
45    /// Bytes read.
46    pub read: u64,
47    /// Bytes written.
48    pub write: u64,
49}
50
51/// Current network IO rates and packet rates.
52#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
53#[serde(default, rename_all = "camelCase")]
54pub struct NetworkIo {
55    /// Upload bytes per sample interval.
56    pub up: u64,
57    /// Download bytes per sample interval.
58    pub down: u64,
59    /// Uploaded packets per sample interval.
60    pub pkt_up: u64,
61    /// Downloaded packets per sample interval.
62    pub pkt_down: u64,
63}
64
65/// Cumulative network traffic counters.
66#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
67#[serde(default, rename_all = "camelCase")]
68pub struct NetworkTraffic {
69    /// Bytes sent.
70    pub sent: u64,
71    /// Bytes received.
72    pub recv: u64,
73    /// Packets sent.
74    pub pkt_sent: u64,
75    /// Packets received.
76    pub pkt_recv: u64,
77}
78
79/// Managed Xray process state included in [`ServerStatus`].
80#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
81#[serde(default, rename_all = "camelCase")]
82pub struct XrayStatus {
83    /// Process state.
84    pub state: ProcessState,
85    /// Last startup or reload error.
86    pub error_msg: String,
87    /// Installed Xray version.
88    pub version: String,
89}
90
91/// Live activity for one peer of an embedded `AmneziaWG` interface.
92#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
93#[serde(default, rename_all = "camelCase")]
94pub struct AmneziaWgPeerActivity {
95    /// Kernel interface name.
96    #[serde(rename = "interface")]
97    pub interface_name: String,
98    /// Xray-compatible inbound tag.
99    pub tag: String,
100    /// Parent inbound identifier.
101    pub inbound_id: i64,
102    /// Client email/name.
103    pub email: String,
104    /// Most recently observed remote endpoint.
105    pub endpoint: String,
106    /// Peer tunnel addresses.
107    #[serde(rename = "allowedIPs")]
108    pub allowed_ips: String,
109    /// Latest handshake timestamp in Unix milliseconds.
110    pub handshake: i64,
111    /// Received bytes, exposed as upload traffic by 3x-ui.
112    pub up: i64,
113    /// Transmitted bytes, exposed as download traffic by 3x-ui.
114    pub down: i64,
115    /// Whether the handshake is within the panel's three-minute window.
116    pub online: bool,
117}
118
119/// `AmneziaWG` peer activity and recent panel lifecycle events.
120#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
121#[serde(default)]
122pub struct AmneziaWgLogs {
123    /// Live peer rows, newest handshake first.
124    pub peers: Vec<AmneziaWgPeerActivity>,
125    /// Recent panel log lines related to `AmneziaWG`.
126    pub events: Vec<String>,
127    /// Whether at least one embedded interface is running.
128    pub running: bool,
129}
130
131/// Embedded `AmneziaWG` subsystem state included in [`ServerStatus`].
132#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
133#[serde(default)]
134pub struct AmneziaWgStatus {
135    /// At least one `AmneziaWG` inbound exists.
136    pub configured: bool,
137    /// At least one embedded `AmneziaWG` interface is running.
138    pub running: bool,
139}
140
141/// Complete assembled Xray configuration.
142///
143/// The inner JSON remains open-ended because it depends on the installed
144/// xray-core version. `Debug` is deliberately redacted because the document can
145/// contain client credentials, private keys, and certificate material.
146#[derive(Clone, Deserialize, PartialEq, Serialize)]
147#[serde(transparent)]
148pub struct XrayConfig(Value);
149
150impl XrayConfig {
151    /// Borrows the complete Xray JSON document.
152    pub const fn as_value(&self) -> &Value {
153        &self.0
154    }
155
156    /// Consumes the wrapper and returns the complete Xray JSON document.
157    pub fn into_value(self) -> Value {
158        self.0
159    }
160}
161
162impl From<Value> for XrayConfig {
163    fn from(value: Value) -> Self {
164        Self(value)
165    }
166}
167
168impl fmt::Debug for XrayConfig {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        formatter
171            .debug_tuple("XrayConfig")
172            .field(&"[REDACTED]")
173            .finish()
174    }
175}
176
177/// Public addresses detected by the panel.
178#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
179#[serde(default)]
180pub struct PublicAddresses {
181    /// Detected IPv4 address.
182    pub ipv4: String,
183    /// Detected IPv6 address.
184    pub ipv6: String,
185}
186
187/// Runtime resource use of the panel process itself.
188#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
189#[serde(default)]
190pub struct AppStats {
191    /// Number of Go runtime threads.
192    pub threads: u32,
193    /// Resident memory in bytes.
194    pub mem: u64,
195    /// Panel process uptime in seconds.
196    pub uptime: u64,
197}
198
199/// Complete cached machine and Xray status snapshot.
200#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
201#[serde(default, rename_all = "camelCase")]
202pub struct ServerStatus {
203    /// CPU use percentage.
204    pub cpu: f64,
205    /// Physical CPU core count.
206    pub cpu_cores: i32,
207    /// Logical processor count.
208    pub logical_pro: i32,
209    /// Reported CPU speed in MHz.
210    pub cpu_speed_mhz: f64,
211    /// Memory usage.
212    pub mem: ResourceUsage,
213    /// Swap usage.
214    pub swap: ResourceUsage,
215    /// Disk usage.
216    pub disk: ResourceUsage,
217    /// Cumulative disk IO.
218    #[serde(rename = "diskIO")]
219    pub disk_io: DiskIo,
220    /// Disk throughput since the previous sample.
221    pub disk_traffic: DiskIo,
222    /// Xray state.
223    pub xray: XrayStatus,
224    /// Embedded `AmneziaWG` subsystem state.
225    pub amneziawg: AmneziaWgStatus,
226    /// Running panel version.
227    pub panel_version: String,
228    /// Stable panel identifier.
229    pub panel_guid: String,
230    /// Host uptime in seconds.
231    pub uptime: u64,
232    /// Load averages reported by the host.
233    #[serde(deserialize_with = "deserialize_null_default")]
234    pub loads: Vec<f64>,
235    /// Open TCP connection count.
236    pub tcp_count: i32,
237    /// Open UDP connection count.
238    pub udp_count: i32,
239    /// Current network IO.
240    #[serde(rename = "netIO")]
241    pub net_io: NetworkIo,
242    /// Cumulative network traffic.
243    pub net_traffic: NetworkTraffic,
244    /// Detected public addresses.
245    #[serde(rename = "publicIP")]
246    pub public_ip: PublicAddresses,
247    /// Panel process statistics.
248    pub app_stats: AppStats,
249}
250
251/// History windows accepted by v3.8.5 history endpoints.
252///
253/// Every response contains at most 60 points. The wire value is the bucket size
254/// in seconds, while variant names describe the resulting history window shown
255/// by the panel UI.
256#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
257#[non_exhaustive]
258pub enum HistoryBucket {
259    /// Approximately two minutes at two-second resolution.
260    Minutes2,
261    /// Approximately thirty minutes at thirty-second resolution.
262    Minutes30,
263    /// Approximately one hour at one-minute resolution.
264    Hour1,
265    /// Approximately three hours at three-minute resolution.
266    Hours3,
267    /// Approximately six hours at six-minute resolution.
268    Hours6,
269    /// Approximately twelve hours at twelve-minute resolution.
270    Hours12,
271    /// Approximately one day at 24-minute resolution.
272    Day1,
273    /// Approximately two days at 48-minute resolution.
274    Days2,
275    /// Approximately seven days at 168-minute resolution.
276    Days7,
277}
278
279impl HistoryBucket {
280    pub(crate) const fn seconds(self) -> u32 {
281        match self {
282            Self::Minutes2 => 2,
283            Self::Minutes30 => 30,
284            Self::Hour1 => 60,
285            Self::Hours3 => 180,
286            Self::Hours6 => 360,
287            Self::Hours12 => 720,
288            Self::Day1 => 1_440,
289            Self::Days2 => 2_880,
290            Self::Days7 => 10_080,
291        }
292    }
293}
294
295/// Host metric accepted by the uniform system history endpoint.
296#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
297#[non_exhaustive]
298pub enum SystemMetric {
299    /// CPU percentage.
300    Cpu,
301    /// Memory usage.
302    Memory,
303    /// Swap usage.
304    Swap,
305    /// Upload rate.
306    NetworkUp,
307    /// Download rate.
308    NetworkDown,
309    /// Uploaded packet rate.
310    PacketUp,
311    /// Downloaded packet rate.
312    PacketDown,
313    /// Disk read throughput.
314    DiskRead,
315    /// Disk write throughput.
316    DiskWrite,
317    /// Disk usage.
318    DiskUsage,
319    /// Open TCP connection count.
320    TcpCount,
321    /// Open UDP connection count.
322    UdpCount,
323    /// Online client count.
324    Online,
325    /// One-minute load average.
326    Load1,
327    /// Five-minute load average.
328    Load5,
329    /// Fifteen-minute load average.
330    Load15,
331}
332
333impl SystemMetric {
334    pub(crate) const fn as_str(self) -> &'static str {
335        match self {
336            Self::Cpu => "cpu",
337            Self::Memory => "mem",
338            Self::Swap => "swap",
339            Self::NetworkUp => "netUp",
340            Self::NetworkDown => "netDown",
341            Self::PacketUp => "pktUp",
342            Self::PacketDown => "pktDown",
343            Self::DiskRead => "diskRead",
344            Self::DiskWrite => "diskWrite",
345            Self::DiskUsage => "diskUsage",
346            Self::TcpCount => "tcpCount",
347            Self::UdpCount => "udpCount",
348            Self::Online => "online",
349            Self::Load1 => "load1",
350            Self::Load5 => "load5",
351            Self::Load15 => "load15",
352        }
353    }
354}
355
356/// Xray expvar metric accepted by its history endpoint.
357#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
358#[non_exhaustive]
359pub enum XrayMetric {
360    /// Allocated heap bytes.
361    Alloc,
362    /// Bytes obtained from the operating system.
363    Sys,
364    /// Live heap object count.
365    HeapObjects,
366    /// Completed garbage collection cycles.
367    NumGc,
368    /// Garbage collector pause time.
369    PauseNs,
370}
371
372impl XrayMetric {
373    pub(crate) const fn as_str(self) -> &'static str {
374        match self {
375            Self::Alloc => "xrAlloc",
376            Self::Sys => "xrSys",
377            Self::HeapObjects => "xrHeapObjects",
378            Self::NumGc => "xrNumGC",
379            Self::PauseNs => "xrPauseNs",
380        }
381    }
382}
383
384/// One uniform time-series sample.
385#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
386pub struct MetricPoint {
387    /// Unix timestamp in seconds.
388    #[serde(rename = "t")]
389    pub timestamp: i64,
390    /// Aggregated value.
391    #[serde(rename = "v")]
392    pub value: f64,
393}
394
395/// One sample returned by the legacy CPU-only history route.
396#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
397pub struct LegacyCpuPoint {
398    /// Unix timestamp in seconds.
399    #[serde(rename = "t")]
400    pub timestamp: i64,
401    /// Aggregated CPU percentage.
402    pub cpu: f64,
403}
404
405/// Xray metrics discovery and collection state.
406#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
407#[serde(default)]
408pub struct XrayMetricsState {
409    /// Whether Xray metrics are configured and reachable.
410    pub enabled: bool,
411    /// Configured metrics listen address.
412    pub listen: String,
413    /// Explanation when metrics are unavailable.
414    pub reason: String,
415}
416
417/// Latest observatory health snapshot for one outbound.
418#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
419#[serde(default, rename_all = "camelCase")]
420pub struct XrayObservatoryEntry {
421    /// Outbound tag.
422    pub tag: String,
423    /// Whether the latest probe succeeded.
424    pub alive: bool,
425    /// Probe delay in milliseconds.
426    pub delay: i64,
427    /// Last successful probe timestamp.
428    pub last_seen_time: i64,
429    /// Last attempted probe timestamp.
430    pub last_try_time: i64,
431    /// Snapshot update timestamp.
432    pub updated_at: i64,
433}
434
435/// Available panel update information.
436#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
437#[serde(default, rename_all = "camelCase")]
438pub struct PanelUpdateInfo {
439    /// `stable` or `dev`.
440    pub channel: String,
441    /// Installed version label.
442    pub current_version: String,
443    /// Latest available version label.
444    pub latest_version: String,
445    /// Installed commit for the dev channel.
446    pub current_commit: String,
447    /// Latest commit for the dev channel.
448    pub latest_commit: String,
449    /// Whether an update is available.
450    pub update_available: bool,
451}
452
453/// Identifier returned after starting a panel self-update.
454#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
455#[serde(default, rename_all = "camelCase")]
456pub struct PanelUpdateRun {
457    /// Decimal update run identifier.
458    pub run_id: String,
459}
460
461/// State of a panel self-update.
462#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
463#[serde(rename_all = "lowercase")]
464#[non_exhaustive]
465pub enum PanelUpdateState {
466    /// Update is still running.
467    Pending,
468    /// Update completed successfully.
469    Success,
470    /// Update failed.
471    Failed,
472    /// A newer panel returned an unknown state.
473    #[default]
474    #[serde(other)]
475    Unknown,
476}
477
478/// Outcome of the most recently started panel self-update.
479#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
480#[serde(default, rename_all = "camelCase")]
481pub struct PanelUpdateStatus {
482    /// Decimal update run identifier.
483    pub run_id: String,
484    /// Current outcome.
485    pub state: PanelUpdateState,
486    /// Updater process exit code.
487    pub exit_code: i32,
488    /// Completion timestamp in Unix seconds, or zero while pending.
489    pub finished_at: i64,
490}
491
492/// Whether IP-limit enforcement is available on this host.
493#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
494#[serde(default)]
495#[allow(clippy::struct_excessive_bools)]
496pub struct Fail2banStatus {
497    /// Whether integration is enabled by environment configuration.
498    pub enabled: bool,
499    /// Whether Fail2ban is installed.
500    pub installed: bool,
501    /// Whether client IP limits can be enforced.
502    pub usable: bool,
503    /// Whether the panel is running on Windows.
504    pub windows: bool,
505}
506
507/// Read-only identity and health of a descendant panel node.
508#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
509#[serde(default, rename_all = "camelCase")]
510pub struct NodeSummary {
511    /// Stable node GUID.
512    pub guid: String,
513    /// Stable GUID of its managing panel.
514    pub parent_guid: String,
515    /// Display name.
516    pub name: String,
517    /// Host or IP address.
518    pub address: String,
519    /// HTTP scheme.
520    pub scheme: String,
521    /// HTTP port.
522    pub port: i32,
523    /// Connectivity state.
524    pub status: String,
525    /// Last heartbeat in Unix seconds.
526    pub last_heartbeat: i64,
527    /// Last heartbeat latency in milliseconds.
528    pub latency_ms: i32,
529    /// Panel version.
530    pub panel_version: String,
531    /// Xray version.
532    pub xray_version: String,
533    /// Xray process state.
534    pub xray_state: String,
535    /// Last Xray error.
536    pub xray_error: String,
537}
538
539/// One recently observed client source IP.
540#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
541pub struct ClientIpObservation {
542    /// IP address.
543    pub ip: String,
544    /// Last-seen Unix timestamp in seconds.
545    pub timestamp: i64,
546}
547
548/// Cluster-wide IP observations for one client.
549#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
550#[serde(default, rename_all = "camelCase")]
551pub struct ClientIpRecord {
552    /// Database row identifier. Remote identifiers are ignored during merge.
553    pub id: i32,
554    /// Unique client email/name.
555    pub client_email: String,
556    /// Recently observed source IPs; old/invalid panel rows may return `null`.
557    #[serde(deserialize_with = "deserialize_null_default")]
558    pub ips: Vec<ClientIpObservation>,
559}
560
561/// Generated X25519 Reality keypair.
562#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
563#[serde(rename_all = "camelCase")]
564pub struct X25519KeyPair {
565    /// Private key.
566    pub private_key: String,
567    /// Public key.
568    pub public_key: String,
569}
570
571impl fmt::Debug for X25519KeyPair {
572    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
573        formatter
574            .debug_struct("X25519KeyPair")
575            .field("private_key", &"[REDACTED]")
576            .field("public_key", &self.public_key)
577            .finish()
578    }
579}
580
581/// Generated ML-DSA-65 material.
582#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
583pub struct Mldsa65KeyPair {
584    /// Signing seed.
585    pub seed: String,
586    /// Verification key.
587    pub verify: String,
588}
589
590impl fmt::Debug for Mldsa65KeyPair {
591    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
592        formatter
593            .debug_struct("Mldsa65KeyPair")
594            .field("seed", &"[REDACTED]")
595            .field("verify", &self.verify)
596            .finish()
597    }
598}
599
600/// Generated ML-KEM-768 material.
601#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
602pub struct Mlkem768KeyPair {
603    /// Server seed.
604    pub seed: String,
605    /// Client key.
606    pub client: String,
607}
608
609impl fmt::Debug for Mlkem768KeyPair {
610    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
611        formatter
612            .debug_struct("Mlkem768KeyPair")
613            .field("seed", &"[REDACTED]")
614            .field("client", &self.client)
615            .finish()
616    }
617}
618
619/// Generated ECH server key and public config list.
620#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
621#[serde(rename_all = "camelCase")]
622pub struct EchKeyPair {
623    /// Private ECH server keys.
624    pub ech_server_keys: String,
625    /// Public ECH configuration list.
626    pub ech_config_list: String,
627}
628
629impl fmt::Debug for EchKeyPair {
630    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
631        formatter
632            .debug_struct("EchKeyPair")
633            .field("ech_server_keys", &"[REDACTED]")
634            .field("ech_config_list", &self.ech_config_list)
635            .finish()
636    }
637}
638
639/// One generated VLESS encryption choice.
640#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
641pub struct VlessEncryptionAuth {
642    /// Stable choice identifier.
643    pub id: String,
644    /// Display label.
645    pub label: String,
646    /// Xray encryption value.
647    pub encryption: String,
648    /// Xray decryption value.
649    pub decryption: String,
650}
651
652impl fmt::Debug for VlessEncryptionAuth {
653    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
654        formatter
655            .debug_struct("VlessEncryptionAuth")
656            .field("id", &self.id)
657            .field("label", &self.label)
658            .field("encryption", &"[REDACTED]")
659            .field("decryption", &"[REDACTED]")
660            .finish()
661    }
662}
663
664/// Generated VLESS encryption choices.
665#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
666#[serde(default)]
667pub struct VlessEncryptionOptions {
668    /// Available authentication/encryption combinations.
669    #[serde(deserialize_with = "deserialize_null_default")]
670    pub auths: Vec<VlessEncryptionAuth>,
671}
672
673/// TLS certificate and key paths configured for the panel web server.
674#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
675#[serde(default, rename_all = "camelCase")]
676pub struct WebCertificateFiles {
677    /// Certificate chain path.
678    pub web_cert_file: String,
679    /// Private key path.
680    pub web_key_file: String,
681}
682
683/// Structured result of probing a candidate REALITY target.
684#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
685#[serde(default, rename_all = "camelCase")]
686#[allow(clippy::struct_excessive_bools)]
687pub struct RealityScanResult {
688    /// Original target.
689    pub target: String,
690    /// TLS server name.
691    pub host: String,
692    /// Resolved address.
693    #[serde(rename = "ip")]
694    pub ip_address: String,
695    /// Target port.
696    pub port: i32,
697    /// Overall REALITY feasibility verdict.
698    pub feasible: bool,
699    /// Whether TLS 1.3 was negotiated.
700    #[serde(rename = "tls13", alias = "TLS13")]
701    pub tls13: bool,
702    /// Negotiated TLS version.
703    #[serde(rename = "tlsVersion", alias = "TLSVersion")]
704    pub tls_version: String,
705    /// Whether HTTP/2 was negotiated.
706    #[serde(rename = "h2", alias = "H2")]
707    pub h2: bool,
708    /// Negotiated ALPN.
709    #[serde(rename = "alpn", alias = "ALPN")]
710    pub alpn: String,
711    /// Whether X25519 was negotiated.
712    #[serde(rename = "x25519", alias = "X25519")]
713    pub x25519: bool,
714    /// Negotiated curve identifier.
715    #[serde(rename = "curveID", alias = "curveId")]
716    pub curve_id: String,
717    /// Whether the target resolved to a private address.
718    pub private_target: bool,
719    /// Whether the presented certificate chain is trusted, ignoring its name.
720    pub cert_chain_valid: bool,
721    /// Total DER certificate-chain size in bytes (ML-DSA-65 requires at least 3500).
722    pub cert_chain_bytes: i32,
723    /// Whether the certificate validated.
724    pub cert_valid: bool,
725    /// Leaf certificate subject.
726    pub cert_subject: String,
727    /// Leaf certificate issuer.
728    pub cert_issuer: String,
729    /// Certificate expiration in RFC 3339 form.
730    pub not_after: String,
731    /// Usable certificate server names.
732    #[serde(deserialize_with = "deserialize_null_default")]
733    pub server_names: Vec<String>,
734    /// End-to-end probe latency in milliseconds.
735    pub latency_ms: i32,
736    /// Failure or infeasibility reason.
737    pub reason: String,
738}
739
740/// Input for probing one REALITY target.
741#[derive(Clone, Debug, Eq, PartialEq)]
742pub struct RealityScanRequest {
743    /// Host, host:port, IP, or IP:port.
744    pub target: String,
745    /// Optional Xray version compatibility selector used by the panel.
746    pub xray_version: i32,
747}
748
749impl RealityScanRequest {
750    /// Creates a scan request using the panel's default compatibility mode.
751    pub fn new(target: impl Into<String>) -> Self {
752        Self {
753            target: target.into(),
754            xray_version: 0,
755        }
756    }
757}
758
759/// Panel application log level filter.
760#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
761#[non_exhaustive]
762pub enum LogLevel {
763    /// No level filtering.
764    #[default]
765    All,
766    /// Debug and above.
767    Debug,
768    /// Informational and above.
769    Info,
770    /// Warnings and errors.
771    Warning,
772    /// Errors only.
773    Error,
774}
775
776impl LogLevel {
777    pub(crate) const fn as_str(self) -> &'static str {
778        match self {
779            Self::All => "",
780            Self::Debug => "debug",
781            Self::Info => "info",
782            Self::Warning => "warning",
783            Self::Error => "error",
784        }
785    }
786}
787
788/// Filters for reading panel logs.
789#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
790pub struct PanelLogRequest {
791    /// Maximum number of trailing lines.
792    pub count: u32,
793    /// Minimum log level.
794    pub level: LogLevel,
795    /// Read the system journal instead of the panel log file.
796    pub syslog: bool,
797}
798
799/// Filters for reading Xray logs.
800#[derive(Clone, Debug, Default, Eq, PartialEq)]
801pub struct XrayLogRequest {
802    /// Maximum number of trailing lines.
803    pub count: u32,
804    /// Free-text filter.
805    pub filter: String,
806    /// Include direct/freedom traffic.
807    pub show_direct: bool,
808    /// Include blocked/blackhole traffic.
809    pub show_blocked: bool,
810    /// Include proxy traffic.
811    pub show_proxy: bool,
812}
813
814/// Traffic classification assigned to one parsed Xray access-log entry.
815#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
816#[non_exhaustive]
817pub enum XrayLogEvent {
818    /// Direct/freedom outbound.
819    #[default]
820    Direct,
821    /// Blocked/blackhole outbound.
822    Blocked,
823    /// Proxied outbound.
824    Proxied,
825    /// A newer panel returned an unknown numeric classification.
826    Unknown,
827}
828
829impl From<i32> for XrayLogEvent {
830    fn from(value: i32) -> Self {
831        match value {
832            0 => Self::Direct,
833            1 => Self::Blocked,
834            2 => Self::Proxied,
835            _ => Self::Unknown,
836        }
837    }
838}
839
840impl From<XrayLogEvent> for i32 {
841    fn from(value: XrayLogEvent) -> Self {
842        match value {
843            XrayLogEvent::Direct => 0,
844            XrayLogEvent::Blocked => 1,
845            XrayLogEvent::Proxied => 2,
846            XrayLogEvent::Unknown => -1,
847        }
848    }
849}
850
851impl<'de> Deserialize<'de> for XrayLogEvent {
852    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
853    where
854        D: Deserializer<'de>,
855    {
856        i32::deserialize(deserializer).map(Self::from)
857    }
858}
859
860impl Serialize for XrayLogEvent {
861    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
862    where
863        S: serde::Serializer,
864    {
865        i32::from(*self).serialize(serializer)
866    }
867}
868
869/// One structured entry parsed from the Xray access log.
870#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
871#[serde(default, rename_all = "PascalCase")]
872pub struct XrayLogEntry {
873    /// UTC timestamp in RFC 3339 form.
874    pub date_time: String,
875    /// Client source address.
876    pub from_address: String,
877    /// Requested destination address.
878    pub to_address: String,
879    /// Inbound tag.
880    pub inbound: String,
881    /// Outbound tag.
882    pub outbound: String,
883    /// Client email/name when present.
884    pub email: String,
885    /// Direct, blocked, or proxied classification.
886    pub event: XrayLogEvent,
887}
888
889/// Binary database backup or migration downloaded from the panel.
890#[derive(Clone, Default, Eq, PartialEq)]
891pub struct DatabaseFile {
892    /// Suggested attachment filename, when supplied by the panel.
893    pub filename: Option<String>,
894    /// Response content type, when supplied by the panel.
895    pub content_type: Option<String>,
896    /// Complete file contents.
897    pub bytes: Vec<u8>,
898}
899
900impl fmt::Debug for DatabaseFile {
901    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
902        formatter
903            .debug_struct("DatabaseFile")
904            .field("filename", &self.filename)
905            .field("content_type", &self.content_type)
906            .field(
907                "bytes",
908                &format_args!("[REDACTED; {} bytes]", self.bytes.len()),
909            )
910            .finish()
911    }
912}
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917
918    #[test]
919    fn history_variants_match_the_source_allowlists() {
920        let buckets = [
921            HistoryBucket::Minutes2,
922            HistoryBucket::Minutes30,
923            HistoryBucket::Hour1,
924            HistoryBucket::Hours3,
925            HistoryBucket::Hours6,
926            HistoryBucket::Hours12,
927            HistoryBucket::Day1,
928            HistoryBucket::Days2,
929            HistoryBucket::Days7,
930        ]
931        .map(HistoryBucket::seconds);
932        assert_eq!(buckets, [2, 30, 60, 180, 360, 720, 1_440, 2_880, 10_080]);
933
934        let metrics = [
935            SystemMetric::Cpu,
936            SystemMetric::Memory,
937            SystemMetric::Swap,
938            SystemMetric::NetworkUp,
939            SystemMetric::NetworkDown,
940            SystemMetric::PacketUp,
941            SystemMetric::PacketDown,
942            SystemMetric::DiskRead,
943            SystemMetric::DiskWrite,
944            SystemMetric::DiskUsage,
945            SystemMetric::TcpCount,
946            SystemMetric::UdpCount,
947            SystemMetric::Online,
948            SystemMetric::Load1,
949            SystemMetric::Load5,
950            SystemMetric::Load15,
951        ]
952        .map(SystemMetric::as_str);
953        assert_eq!(
954            metrics,
955            [
956                "cpu",
957                "mem",
958                "swap",
959                "netUp",
960                "netDown",
961                "pktUp",
962                "pktDown",
963                "diskRead",
964                "diskWrite",
965                "diskUsage",
966                "tcpCount",
967                "udpCount",
968                "online",
969                "load1",
970                "load5",
971                "load15",
972            ]
973        );
974    }
975
976    #[test]
977    fn open_xray_config_has_explicit_secret_accessors() {
978        let value = serde_json::json!({"privateKey": "xray-private-secret"});
979        let config = XrayConfig::from(value.clone());
980        assert_eq!(config.as_value(), &value);
981        assert!(!format!("{config:?}").contains("xray-private-secret"));
982        assert_eq!(config.into_value(), value);
983    }
984
985    #[test]
986    fn xray_metrics_and_log_filters_match_wire_vocabulary() {
987        assert_eq!(
988            [
989                XrayMetric::Alloc,
990                XrayMetric::Sys,
991                XrayMetric::HeapObjects,
992                XrayMetric::NumGc,
993                XrayMetric::PauseNs,
994            ]
995            .map(XrayMetric::as_str),
996            ["xrAlloc", "xrSys", "xrHeapObjects", "xrNumGC", "xrPauseNs"]
997        );
998        assert_eq!(
999            [
1000                LogLevel::All,
1001                LogLevel::Debug,
1002                LogLevel::Info,
1003                LogLevel::Warning,
1004                LogLevel::Error,
1005            ]
1006            .map(LogLevel::as_str),
1007            ["", "debug", "info", "warning", "error"]
1008        );
1009
1010        for (wire, event) in [
1011            (0, XrayLogEvent::Direct),
1012            (1, XrayLogEvent::Blocked),
1013            (2, XrayLogEvent::Proxied),
1014            (99, XrayLogEvent::Unknown),
1015        ] {
1016            assert_eq!(XrayLogEvent::from(wire), event);
1017            let expected = if event == XrayLogEvent::Unknown {
1018                -1
1019            } else {
1020                wire
1021            };
1022            assert_eq!(i32::from(event), expected);
1023            assert_eq!(serde_json::to_value(event).unwrap(), expected);
1024            assert_eq!(
1025                serde_json::from_value::<XrayLogEvent>(serde_json::json!(wire)).unwrap(),
1026                event
1027            );
1028        }
1029    }
1030
1031    #[test]
1032    fn generated_key_material_is_redacted_selectively() {
1033        let x25519 = X25519KeyPair {
1034            private_key: "x25519-private-secret".into(),
1035            public_key: "x25519-public".into(),
1036        };
1037        let mldsa = Mldsa65KeyPair {
1038            seed: "mldsa-seed-secret".into(),
1039            verify: "mldsa-verify".into(),
1040        };
1041        let mlkem = Mlkem768KeyPair {
1042            seed: "mlkem-seed-secret".into(),
1043            client: "mlkem-client".into(),
1044        };
1045        let ech = EchKeyPair {
1046            ech_server_keys: "ech-private-secret".into(),
1047            ech_config_list: "ech-public".into(),
1048        };
1049        let vless = VlessEncryptionAuth {
1050            id: "option-1".into(),
1051            label: "recommended".into(),
1052            encryption: "vless-encryption-secret".into(),
1053            decryption: "vless-decryption-secret".into(),
1054        };
1055        let cases = [
1056            (
1057                format!("{x25519:?}"),
1058                "x25519-private-secret",
1059                "x25519-public",
1060            ),
1061            (format!("{mldsa:?}"), "mldsa-seed-secret", "mldsa-verify"),
1062            (format!("{mlkem:?}"), "mlkem-seed-secret", "mlkem-client"),
1063            (format!("{ech:?}"), "ech-private-secret", "ech-public"),
1064            (
1065                format!("{vless:?}"),
1066                "vless-encryption-secret",
1067                "recommended",
1068            ),
1069        ];
1070        for (output, secret, visible) in cases {
1071            assert!(!output.contains(secret));
1072            assert!(output.contains(visible));
1073        }
1074        assert!(!format!("{vless:?}").contains("vless-decryption-secret"));
1075    }
1076}