sub_converter/formats/
clash.rs

1use crate::error::{Error, Result};
2use crate::ir::{Node, Protocol, Tls};
3use serde::{Deserialize, Serialize};
4use serde_yaml::Value;
5use std::collections::BTreeMap;
6
7/// Clash proxy configuration
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type")]
10#[serde(rename_all = "lowercase")]
11pub enum ClashProxy {
12    #[serde(rename = "ss")]
13    Ss {
14        name: String,
15        server: String,
16        port: u16,
17        cipher: String,
18        password: String,
19        #[serde(skip_serializing_if = "Option::is_none")]
20        udp: Option<bool>,
21    },
22    Trojan {
23        name: String,
24        server: String,
25        port: u16,
26        password: String,
27        #[serde(skip_serializing_if = "Option::is_none")]
28        sni: Option<String>,
29        #[serde(skip_serializing_if = "Option::is_none")]
30        alpn: Option<Vec<String>>,
31        #[serde(skip_serializing_if = "Option::is_none")]
32        #[serde(rename = "skip-cert-verify")]
33        skip_cert_verify: Option<bool>,
34    },
35}
36
37/// Clash proxy group configuration
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ProxyGroup {
40    pub name: String,
41    #[serde(rename = "type")]
42    pub group_type: String,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub proxies: Option<Vec<String>>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub url: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub interval: Option<u32>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub tolerance: Option<u32>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub lazy: Option<bool>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    #[serde(rename = "use")]
55    pub use_provider: Option<Vec<String>>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub filter: Option<String>,
58    #[serde(flatten)]
59    pub other: serde_yaml::Mapping,
60}
61
62/// Top-level Clash configuration that can be used for both parsing and templates
63#[derive(Debug, Clone, Default)]
64pub struct ClashConfig {
65    /// General configuration options (e.g., port, socks-port, allow-lan, mode, log-level, etc.)
66    pub general: Option<Value>,
67    /// List of proxies
68    pub proxies: Vec<ClashProxy>,
69    /// Proxy groups
70    pub proxy_groups: Option<Vec<ProxyGroup>>,
71    /// Rules
72    pub rules: Option<Value>,
73    /// DNS configuration
74    pub dns: Option<Value>,
75}
76
77impl<'de> Deserialize<'de> for ClashConfig {
78    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
79    where
80        D: serde::Deserializer<'de>,
81    {
82        let mut map = BTreeMap::<String, Value>::deserialize(deserializer)?;
83
84        let proxies = map
85            .remove("proxies")
86            .map(serde_yaml::from_value)
87            .transpose()
88            .map_err(serde::de::Error::custom)?
89            .unwrap_or_default();
90
91        let proxy_groups = map
92            .remove("proxy-groups")
93            .map(serde_yaml::from_value)
94            .transpose()
95            .map_err(serde::de::Error::custom)?;
96
97        let rules = map.remove("rules");
98        let dns = map.remove("dns");
99
100        // Everything else goes into general
101        let general = if map.is_empty() {
102            None
103        } else {
104            Some(Value::Mapping(
105                map.into_iter()
106                    .map(|(k, v)| (Value::String(k), v))
107                    .collect(),
108            ))
109        };
110
111        Ok(ClashConfig {
112            general,
113            proxies,
114            proxy_groups,
115            rules,
116            dns,
117        })
118    }
119}
120
121impl Serialize for ClashConfig {
122    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
123    where
124        S: serde::Serializer,
125    {
126        use serde::ser::SerializeMap;
127
128        let mut map = serializer.serialize_map(None)?;
129
130        if let Some(Value::Mapping(m)) = &self.general {
131            for (k, v) in m {
132                map.serialize_entry(k, v)?;
133            }
134        }
135
136        if !self.proxies.is_empty() {
137            map.serialize_entry("proxies", &self.proxies)?;
138        }
139
140        if let Some(pg) = &self.proxy_groups {
141            map.serialize_entry("proxy-groups", pg)?;
142        }
143
144        if let Some(r) = &self.rules {
145            map.serialize_entry("rules", r)?;
146        }
147
148        if let Some(d) = &self.dns {
149            map.serialize_entry("dns", d)?;
150        }
151
152        map.end()
153    }
154}
155
156// Conversion from ClashProxy to Node
157impl From<ClashProxy> for Node {
158    fn from(proxy: ClashProxy) -> Self {
159        match proxy {
160            ClashProxy::Ss {
161                name,
162                server,
163                port,
164                cipher,
165                password,
166                ..
167            } => Node {
168                name,
169                server,
170                port,
171                protocol: Protocol::Shadowsocks {
172                    method: cipher,
173                    password,
174                },
175                transport: None,
176                tls: None,
177                tags: Vec::new(),
178            },
179            ClashProxy::Trojan {
180                name,
181                server,
182                port,
183                password,
184                sni,
185                alpn,
186                skip_cert_verify,
187            } => Node {
188                name,
189                server,
190                port,
191                protocol: Protocol::Trojan { password },
192                transport: None,
193                tls: Some(Tls {
194                    enabled: true,
195                    server_name: sni,
196                    alpn,
197                    insecure: skip_cert_verify,
198                    utls_fingerprint: None,
199                }),
200                tags: Vec::new(),
201            },
202        }
203    }
204}
205
206// Conversion from Node to ClashProxy (fallible)
207impl TryFrom<Node> for ClashProxy {
208    type Error = Error;
209
210    fn try_from(node: Node) -> Result<Self> {
211        let Node {
212            name,
213            server,
214            port,
215            protocol,
216            tls,
217            ..
218        } = node;
219
220        match protocol {
221            Protocol::Shadowsocks { method, password } => {
222                if method.is_empty() || password.is_empty() {
223                    return Err(Error::ValidationError {
224                        reason: format!("Empty method or password for Shadowsocks node: {}", name),
225                    });
226                }
227
228                Ok(ClashProxy::Ss {
229                    name,
230                    server,
231                    port,
232                    cipher: method,
233                    password,
234                    udp: None,
235                })
236            }
237            Protocol::Trojan { password } => {
238                if password.is_empty() {
239                    return Err(Error::ValidationError {
240                        reason: format!("Empty password for Trojan node: {}", name),
241                    });
242                }
243
244                let (sni, alpn, skip_cert_verify) = tls
245                    .map(|tls| (tls.server_name, tls.alpn, tls.insecure))
246                    .unwrap_or((None, None, None));
247
248                Ok(ClashProxy::Trojan {
249                    name,
250                    server,
251                    port,
252                    password,
253                    sni,
254                    alpn,
255                    skip_cert_verify,
256                })
257            }
258            _ => Err(Error::Unsupported {
259                what: format!("protocol '{}' for clash", protocol.name()),
260            }),
261        }
262    }
263}