libsubconverter/generator/yaml/clash/
clash_output.rs

1use crate::utils::is_empty_option_string;
2use crate::{generator::yaml::clash::output_proxy_types::*, Proxy, ProxyType};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Represents a complete Clash configuration output
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "kebab-case")]
9pub struct ClashYamlOutput {
10    // General settings
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub port: Option<u16>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub socks_port: Option<u16>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub redir_port: Option<u16>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub tproxy_port: Option<u16>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub mixed_port: Option<u16>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub allow_lan: Option<bool>,
23    #[serde(skip_serializing_if = "is_empty_option_string")]
24    pub bind_address: Option<String>,
25    #[serde(skip_serializing_if = "is_empty_option_string")]
26    pub mode: Option<String>,
27    #[serde(skip_serializing_if = "is_empty_option_string")]
28    pub log_level: Option<String>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub ipv6: Option<bool>,
31
32    // DNS settings
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub dns: Option<ClashDns>,
35
36    // Proxy settings
37    #[serde(skip_serializing_if = "Vec::is_empty")]
38    pub proxies: Vec<ClashProxyOutput>,
39
40    #[serde(skip_serializing_if = "Vec::is_empty")]
41    pub proxy_groups: Vec<ClashProxyGroup>,
42
43    #[serde(skip_serializing_if = "Vec::is_empty")]
44    pub rules: Vec<String>,
45
46    // Additional fields (for compatibility with ClashR and other variants)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub tun: Option<ClashTun>,
49
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub profile: Option<ClashProfile>,
52
53    #[serde(flatten)]
54    pub extra_options: HashMap<String, serde_yaml::Value>,
55}
56
57/// DNS configuration for Clash
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "kebab-case")]
60pub struct ClashDns {
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub enable: Option<bool>,
63    #[serde(skip_serializing_if = "is_empty_option_string")]
64    pub listen: Option<String>,
65    #[serde(skip_serializing_if = "is_empty_option_string")]
66    pub enhanced_mode: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub nameserver: Option<Vec<String>>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub fallback: Option<Vec<String>>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub fallback_filter: Option<ClashDnsFallbackFilter>,
73    #[serde(flatten)]
74    pub extra_options: HashMap<String, serde_yaml::Value>,
75}
76
77/// DNS fallback filter configuration
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "kebab-case")]
80pub struct ClashDnsFallbackFilter {
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub geoip: Option<bool>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub ipcidr: Option<Vec<String>>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub domain: Option<Vec<String>>,
87    #[serde(flatten)]
88    pub extra_options: HashMap<String, serde_yaml::Value>,
89}
90
91/// TUN configuration for Clash
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(rename_all = "kebab-case")]
94pub struct ClashTun {
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub enable: Option<bool>,
97    #[serde(skip_serializing_if = "is_empty_option_string")]
98    pub device: Option<String>,
99    #[serde(skip_serializing_if = "is_empty_option_string")]
100    pub stack: Option<String>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub dns_hijack: Option<Vec<String>>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub auto_route: Option<bool>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub auto_detect_interface: Option<bool>,
107    #[serde(flatten)]
108    pub extra_options: HashMap<String, serde_yaml::Value>,
109}
110
111/// Profile settings for Clash
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(rename_all = "kebab-case")]
114pub struct ClashProfile {
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub store_selected: Option<bool>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub store_fake_ip: Option<bool>,
119    #[serde(flatten)]
120    pub extra_options: HashMap<String, serde_yaml::Value>,
121}
122
123/// Represents a single proxy in Clash configuration
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(tag = "type", rename_all = "kebab-case")]
126pub enum ClashProxyOutput {
127    #[serde(rename = "ss")]
128    Shadowsocks(ShadowsocksProxy),
129    #[serde(rename = "ssr")]
130    ShadowsocksR(ShadowsocksRProxy),
131    #[serde(rename = "vmess")]
132    VMess(VmessProxy),
133    #[serde(rename = "trojan")]
134    Trojan(TrojanProxy),
135    #[serde(rename = "http")]
136    Http(HttpProxy),
137    #[serde(rename = "socks5")]
138    Socks5(Socks5Proxy),
139    #[serde(rename = "snell")]
140    Snell(SnellProxy),
141    #[serde(rename = "wireguard")]
142    WireGuard(WireGuardProxy),
143    #[serde(rename = "hysteria")]
144    Hysteria(HysteriaProxy),
145    #[serde(rename = "hysteria2")]
146    Hysteria2(Hysteria2Proxy),
147    #[serde(rename = "vless")]
148    VLess(VLessProxy),
149}
150
151/// Factory methods for creating various proxy types
152impl ClashProxyOutput {
153    /// Create a new Shadowsocks proxy
154    pub fn new_shadowsocks(common: CommonProxyOptions) -> Self {
155        ClashProxyOutput::Shadowsocks(ShadowsocksProxy::new(common))
156    }
157
158    /// Create a new ShadowsocksR proxy
159    pub fn new_shadowsocksr(common: CommonProxyOptions) -> Self {
160        ClashProxyOutput::ShadowsocksR(ShadowsocksRProxy::new(common))
161    }
162
163    /// Create a new VMess proxy
164    pub fn new_vmess(common: CommonProxyOptions) -> Self {
165        ClashProxyOutput::VMess(VmessProxy::new(common))
166    }
167
168    /// Create a new HTTP proxy
169    pub fn new_http(common: CommonProxyOptions) -> Self {
170        ClashProxyOutput::Http(HttpProxy::new(common))
171    }
172
173    /// Create a new Trojan proxy
174    pub fn new_trojan(common: CommonProxyOptions) -> Self {
175        ClashProxyOutput::Trojan(TrojanProxy::new(common))
176    }
177
178    /// Create a new Socks5 proxy
179    pub fn new_socks5(common: CommonProxyOptions) -> Self {
180        ClashProxyOutput::Socks5(Socks5Proxy::new(common))
181    }
182
183    /// Create a new Snell proxy
184    pub fn new_snell(common: CommonProxyOptions) -> Self {
185        ClashProxyOutput::Snell(SnellProxy::new(common))
186    }
187
188    /// Create a new WireGuard proxy
189    pub fn new_wireguard(common: CommonProxyOptions) -> Self {
190        ClashProxyOutput::WireGuard(WireGuardProxy::new(common))
191    }
192
193    /// Create a new Hysteria proxy
194    pub fn new_hysteria(common: CommonProxyOptions) -> Self {
195        ClashProxyOutput::Hysteria(HysteriaProxy::new(common))
196    }
197
198    /// Create a new Hysteria2 proxy
199    pub fn new_hysteria2(common: CommonProxyOptions) -> Self {
200        ClashProxyOutput::Hysteria2(Hysteria2Proxy::new(common))
201    }
202
203    /// Create a new VLESS proxy
204    pub fn new_vless(common: CommonProxyOptions) -> Self {
205        ClashProxyOutput::VLess(VLessProxy::new(common))
206    }
207}
208
209/// Trait for common operations on all ClashProxy variants
210pub trait ClashProxyCommon {
211    /// Get a reference to the common options
212    fn common(&self) -> &CommonProxyOptions;
213
214    /// Get a mutable reference to the common options
215    fn common_mut(&mut self) -> &mut CommonProxyOptions;
216
217    /// Set a TFO (TCP Fast Open) option
218    fn set_tfo(&mut self, value: bool) {
219        self.common_mut().tfo = Some(value);
220    }
221
222    /// Set a UDP option
223    fn set_udp(&mut self, value: bool) {
224        self.common_mut().udp = Some(value);
225    }
226
227    /// Set skip certificate verification option
228    fn set_skip_cert_verify(&mut self, value: bool) {
229        self.common_mut().skip_cert_verify = Some(value);
230    }
231
232    /// Set TLS option
233    fn set_tls(&mut self, value: bool) {
234        self.common_mut().tls = Some(value);
235    }
236
237    /// Set SNI option
238    fn set_sni(&mut self, value: String) {
239        self.common_mut().sni = Some(value);
240    }
241
242    /// Set fingerprint option
243    fn set_fingerprint(&mut self, value: String) {
244        self.common_mut().fingerprint = Some(value);
245    }
246}
247
248impl ClashProxyCommon for ClashProxyOutput {
249    fn common(&self) -> &CommonProxyOptions {
250        match self {
251            ClashProxyOutput::Shadowsocks(proxy) => &proxy.common,
252            ClashProxyOutput::ShadowsocksR(proxy) => &proxy.common,
253            ClashProxyOutput::VMess(proxy) => &proxy.common,
254            ClashProxyOutput::Trojan(proxy) => &proxy.common,
255            ClashProxyOutput::Http(proxy) => &proxy.common,
256            ClashProxyOutput::Socks5(proxy) => &proxy.common,
257            ClashProxyOutput::Snell(proxy) => &proxy.common,
258            ClashProxyOutput::WireGuard(proxy) => &proxy.common,
259            ClashProxyOutput::Hysteria(proxy) => &proxy.common,
260            ClashProxyOutput::Hysteria2(proxy) => &proxy.common,
261            ClashProxyOutput::VLess(proxy) => &proxy.common,
262        }
263    }
264
265    fn common_mut(&mut self) -> &mut CommonProxyOptions {
266        match self {
267            ClashProxyOutput::Shadowsocks(proxy) => &mut proxy.common,
268            ClashProxyOutput::ShadowsocksR(proxy) => &mut proxy.common,
269            ClashProxyOutput::VMess(proxy) => &mut proxy.common,
270            ClashProxyOutput::Trojan(proxy) => &mut proxy.common,
271            ClashProxyOutput::Http(proxy) => &mut proxy.common,
272            ClashProxyOutput::Socks5(proxy) => &mut proxy.common,
273            ClashProxyOutput::Snell(proxy) => &mut proxy.common,
274            ClashProxyOutput::WireGuard(proxy) => &mut proxy.common,
275            ClashProxyOutput::Hysteria(proxy) => &mut proxy.common,
276            ClashProxyOutput::Hysteria2(proxy) => &mut proxy.common,
277            ClashProxyOutput::VLess(proxy) => &mut proxy.common,
278        }
279    }
280}
281
282/// Represents a proxy group in Clash configuration
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(tag = "type", rename_all = "kebab-case")]
285pub enum ClashProxyGroup {
286    #[serde(rename = "select")]
287    Select {
288        name: String,
289        proxies: Vec<String>,
290        #[serde(skip_serializing_if = "Option::is_none")]
291        r#use: Option<Vec<String>>,
292        #[serde(skip_serializing_if = "Option::is_none")]
293        disable_udp: Option<bool>,
294    },
295    #[serde(rename = "url-test")]
296    UrlTest {
297        name: String,
298        proxies: Vec<String>,
299        url: String,
300        #[serde(skip_serializing_if = "Option::is_none")]
301        interval: Option<u32>,
302        #[serde(skip_serializing_if = "Option::is_none")]
303        tolerance: Option<u32>,
304        #[serde(skip_serializing_if = "Option::is_none")]
305        lazy: Option<bool>,
306        #[serde(skip_serializing_if = "Option::is_none")]
307        disable_udp: Option<bool>,
308        #[serde(skip_serializing_if = "Option::is_none")]
309        r#use: Option<Vec<String>>,
310    },
311    #[serde(rename = "fallback")]
312    Fallback {
313        name: String,
314        proxies: Vec<String>,
315        url: String,
316        #[serde(skip_serializing_if = "Option::is_none")]
317        interval: Option<u32>,
318        #[serde(skip_serializing_if = "Option::is_none")]
319        tolerance: Option<u32>,
320        #[serde(skip_serializing_if = "Option::is_none")]
321        disable_udp: Option<bool>,
322        #[serde(skip_serializing_if = "Option::is_none")]
323        r#use: Option<Vec<String>>,
324    },
325    #[serde(rename = "load-balance")]
326    LoadBalance {
327        name: String,
328        proxies: Vec<String>,
329        strategy: String,
330        #[serde(skip_serializing_if = "is_empty_option_string")]
331        url: Option<String>,
332        #[serde(skip_serializing_if = "Option::is_none")]
333        interval: Option<u32>,
334        #[serde(skip_serializing_if = "Option::is_none")]
335        tolerance: Option<u32>,
336        #[serde(skip_serializing_if = "Option::is_none")]
337        lazy: Option<bool>,
338        #[serde(skip_serializing_if = "Option::is_none")]
339        disable_udp: Option<bool>,
340        #[serde(skip_serializing_if = "Option::is_none")]
341        r#use: Option<Vec<String>>,
342        #[serde(skip_serializing_if = "Option::is_none")]
343        persistent: Option<bool>,
344        #[serde(skip_serializing_if = "Option::is_none")]
345        evaluate_before_use: Option<bool>,
346    },
347    #[serde(rename = "relay")]
348    Relay {
349        name: String,
350        proxies: Vec<String>,
351        #[serde(skip_serializing_if = "Option::is_none")]
352        disable_udp: Option<bool>,
353        #[serde(skip_serializing_if = "Option::is_none")]
354        r#use: Option<Vec<String>>,
355    },
356}
357
358// Implement Default trait for ClashYamlOutput
359impl Default for ClashYamlOutput {
360    fn default() -> Self {
361        Self {
362            port: None,
363            socks_port: None,
364            redir_port: None,
365            tproxy_port: None,
366            mixed_port: None,
367            allow_lan: None,
368            bind_address: None,
369            mode: Some("rule".to_string()),
370            log_level: Some("info".to_string()),
371            ipv6: None,
372            dns: None,
373            proxies: Vec::new(),
374            proxy_groups: Vec::new(),
375            rules: Vec::new(),
376            tun: None,
377            profile: None,
378            extra_options: HashMap::new(),
379        }
380    }
381}
382
383/// Implementation of From trait for ClashProxyOutput
384impl From<Proxy> for ClashProxyOutput {
385    fn from(proxy: Proxy) -> Self {
386        match proxy.proxy_type {
387            ProxyType::Shadowsocks => ClashProxyOutput::Shadowsocks(ShadowsocksProxy::from(proxy)),
388            ProxyType::ShadowsocksR => {
389                ClashProxyOutput::ShadowsocksR(ShadowsocksRProxy::from(proxy))
390            }
391            ProxyType::VMess => ClashProxyOutput::VMess(VmessProxy::from(proxy)),
392            ProxyType::Vless => ClashProxyOutput::VLess(VLessProxy::from(proxy)),
393            ProxyType::Trojan => ClashProxyOutput::Trojan(TrojanProxy::from(proxy)),
394            ProxyType::HTTP | ProxyType::HTTPS => ClashProxyOutput::Http(HttpProxy::from(proxy)),
395            ProxyType::Socks5 => ClashProxyOutput::Socks5(Socks5Proxy::from(proxy)),
396            ProxyType::Snell => {
397                // Skip Snell v4+ if exists - exactly matching C++ behavior
398                if proxy.snell_version >= 4 {
399                    // 为了处理这种特殊情况,我们返回一个默认的Snell代理
400                    // 调用方应该检查snell_version并据此跳过这个代理
401                    let common = CommonProxyOptions::builder(
402                        proxy.remark.clone(),
403                        proxy.hostname.clone(),
404                        proxy.port,
405                    )
406                    .build();
407                    ClashProxyOutput::Snell(SnellProxy::new(common))
408                } else {
409                    ClashProxyOutput::Snell(SnellProxy::from(proxy))
410                }
411            }
412            ProxyType::WireGuard => ClashProxyOutput::WireGuard(WireGuardProxy::from(proxy)),
413            ProxyType::Hysteria => ClashProxyOutput::Hysteria(HysteriaProxy::from(proxy)),
414            ProxyType::Hysteria2 => ClashProxyOutput::Hysteria2(Hysteria2Proxy::from(proxy)),
415            _ => {
416                // 遇到不支持的类型,返回一个默认的HTTP代理
417                // 实际使用时应该在转换前检查并筛选掉不支持的类型
418                let common = CommonProxyOptions::builder(
419                    proxy.remark.clone(),
420                    proxy.hostname.clone(),
421                    proxy.port,
422                )
423                .build();
424                ClashProxyOutput::Http(HttpProxy::new(common))
425            }
426        }
427    }
428}