Skip to main content

trojan_client/
config.rs

1//! Client configuration.
2
3use serde::Deserialize;
4use trojan_config::{LoggingConfig, TcpConfig};
5
6use crate::error::ClientError;
7
8/// Top-level client configuration.
9#[derive(Debug, Clone, Deserialize)]
10pub struct ClientConfig {
11    pub client: ClientSettings,
12    #[serde(default)]
13    pub logging: LoggingConfig,
14}
15
16/// Core client settings.
17#[derive(Debug, Clone, Deserialize)]
18pub struct ClientSettings {
19    /// Local SOCKS5 listen address, e.g. "127.0.0.1:1080".
20    pub listen: String,
21
22    /// Remote trojan server address, e.g. "example.com:443".
23    pub remote: String,
24
25    /// Password (plaintext, SHA-224 computed at runtime).
26    pub password: String,
27
28    /// TLS configuration.
29    #[serde(default)]
30    pub tls: ClientTlsConfig,
31
32    /// TCP socket options.
33    #[serde(default)]
34    pub tcp: TcpConfig,
35
36    /// DNS resolver configuration.
37    #[serde(default)]
38    pub dns: trojan_dns::DnsConfig,
39}
40
41/// Client-side TLS configuration.
42#[derive(Debug, Clone, Deserialize)]
43pub struct ClientTlsConfig {
44    /// TLS SNI hostname. Defaults to the host portion of `remote`.
45    pub sni: Option<String>,
46
47    /// ALPN protocol list.
48    #[serde(default = "default_alpn")]
49    pub alpn: Vec<String>,
50
51    /// Skip certificate verification (for testing only).
52    #[serde(default)]
53    pub skip_verify: bool,
54
55    /// Custom CA certificate path (PEM).
56    pub ca: Option<String>,
57}
58
59impl Default for ClientTlsConfig {
60    fn default() -> Self {
61        Self {
62            sni: None,
63            alpn: default_alpn(),
64            skip_verify: false,
65            ca: None,
66        }
67    }
68}
69
70fn default_alpn() -> Vec<String> {
71    vec!["h2".into(), "http/1.1".into()]
72}
73
74/// Load client configuration from a file path.
75///
76/// Supports TOML, JSON, and JSONC formats (detected by extension).
77pub fn load_client_config(path: &std::path::Path) -> Result<ClientConfig, ClientError> {
78    let content = std::fs::read_to_string(path)
79        .map_err(|e| ClientError::Config(format!("failed to read config: {e}")))?;
80
81    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("toml");
82
83    match ext {
84        "toml" => toml::from_str(&content)
85            .map_err(|e| ClientError::Config(format!("TOML parse error: {e}"))),
86        "json" | "jsonc" => {
87            // Strip single-line comments for JSONC support
88            let stripped: String = content
89                .lines()
90                .map(|line| {
91                    if let Some(idx) = line.find("//") {
92                        // Only strip if not inside a string (simple heuristic)
93                        let before = &line[..idx];
94                        let quotes = before.chars().filter(|&c| c == '"').count();
95                        if quotes % 2 == 0 { before } else { line }
96                    } else {
97                        line
98                    }
99                })
100                .collect::<Vec<_>>()
101                .join("\n");
102            serde_json::from_str(&stripped)
103                .map_err(|e| ClientError::Config(format!("JSON parse error: {e}")))
104        }
105        _ => toml::from_str(&content)
106            .map_err(|e| ClientError::Config(format!("config parse error: {e}"))),
107    }
108}