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