sub_converter/formats/
sing_box.rs

1use crate::error::{Error, Result};
2use crate::ir::{Node, Protocol, Tls};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::BTreeMap;
6
7/// Sing-Box outbound configuration
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type")]
10#[serde(rename_all = "kebab-case")]
11pub enum SingBoxOutbound {
12    Shadowsocks {
13        #[serde(skip_serializing_if = "Option::is_none")]
14        tag: Option<String>,
15        server: String,
16        server_port: u16,
17        method: String,
18        password: String,
19    },
20    Trojan {
21        #[serde(skip_serializing_if = "Option::is_none")]
22        tag: Option<String>,
23        server: String,
24        server_port: u16,
25        password: String,
26        #[serde(skip_serializing_if = "Option::is_none")]
27        tls: Option<SingBoxTls>,
28    },
29    Selector {
30        #[serde(skip_serializing_if = "Option::is_none")]
31        tag: Option<String>,
32        outbounds: Vec<String>,
33    },
34    Urltest {
35        #[serde(skip_serializing_if = "Option::is_none")]
36        tag: Option<String>,
37        outbounds: Vec<String>,
38        url: String,
39        #[serde(skip_serializing_if = "Option::is_none")]
40        interval: Option<String>,
41        #[serde(skip_serializing_if = "Option::is_none")]
42        tolerance: Option<u32>,
43    },
44    Direct {
45        #[serde(skip_serializing_if = "Option::is_none")]
46        tag: Option<String>,
47    },
48    Block {
49        #[serde(skip_serializing_if = "Option::is_none")]
50        tag: Option<String>,
51    },
52}
53
54/// Sing-Box TLS configuration
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SingBoxTls {
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub enabled: Option<bool>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub server_name: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub alpn: Option<Vec<String>>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub insecure: Option<bool>,
65}
66
67/// Top-level Sing-Box configuration that can be used for both parsing and templates
68#[derive(Debug, Clone, Default)]
69pub struct SingBoxConfig {
70    /// General configuration options (e.g., log level, experimental features, etc.)
71    pub general: Option<Value>,
72    /// Inbound configurations
73    pub inbounds: Option<Value>,
74    /// Outbound configurations
75    pub outbounds: Vec<SingBoxOutbound>,
76    /// Route configuration
77    pub route: Option<Value>,
78    /// DNS configuration
79    pub dns: Option<Value>,
80}
81
82impl<'de> Deserialize<'de> for SingBoxConfig {
83    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
84    where
85        D: serde::Deserializer<'de>,
86    {
87        let mut map = BTreeMap::<String, Value>::deserialize(deserializer)?;
88
89        let outbounds = map
90            .remove("outbounds")
91            .map(serde_json::from_value)
92            .transpose()
93            .map_err(serde::de::Error::custom)?
94            .unwrap_or_default();
95
96        let inbounds = map.remove("inbounds");
97        let route = map.remove("route");
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::Object(map.into_iter().collect()))
105        };
106
107        Ok(SingBoxConfig {
108            general,
109            inbounds,
110            outbounds,
111            route,
112            dns,
113        })
114    }
115}
116
117impl Serialize for SingBoxConfig {
118    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
119    where
120        S: serde::Serializer,
121    {
122        use serde::ser::SerializeMap;
123
124        let mut map = serializer.serialize_map(None)?;
125
126        if let Some(Value::Object(m)) = &self.general {
127            for (k, v) in m {
128                map.serialize_entry(k, v)?;
129            }
130        }
131
132        if let Some(ib) = &self.inbounds {
133            map.serialize_entry("inbounds", ib)?;
134        }
135
136        if !self.outbounds.is_empty() {
137            map.serialize_entry("outbounds", &self.outbounds)?;
138        }
139
140        if let Some(r) = &self.route {
141            map.serialize_entry("route", r)?;
142        }
143
144        if let Some(d) = &self.dns {
145            map.serialize_entry("dns", d)?;
146        }
147
148        map.end()
149    }
150}
151
152// Conversion from SingBoxTls to Tls
153impl From<SingBoxTls> for Tls {
154    fn from(tls: SingBoxTls) -> Self {
155        Tls {
156            enabled: tls.enabled.unwrap_or(true),
157            server_name: tls.server_name,
158            alpn: tls.alpn,
159            insecure: tls.insecure,
160            utls_fingerprint: None,
161        }
162    }
163}
164
165// Conversion from Tls to SingBoxTls
166impl From<&Tls> for SingBoxTls {
167    fn from(tls: &Tls) -> Self {
168        SingBoxTls {
169            enabled: Some(tls.enabled),
170            server_name: tls.server_name.clone(),
171            alpn: tls.alpn.clone(),
172            insecure: tls.insecure,
173        }
174    }
175}
176
177impl From<Tls> for SingBoxTls {
178    fn from(tls: Tls) -> Self {
179        SingBoxTls {
180            enabled: Some(tls.enabled),
181            server_name: tls.server_name,
182            alpn: tls.alpn,
183            insecure: tls.insecure,
184        }
185    }
186}
187
188// Conversion from SingBoxOutbound to Node
189impl From<SingBoxOutbound> for Node {
190    fn from(outbound: SingBoxOutbound) -> Self {
191        match outbound {
192            SingBoxOutbound::Shadowsocks {
193                tag,
194                server,
195                server_port,
196                method,
197                password,
198            } => Node {
199                name: tag.unwrap_or_else(|| format!("ss:{}:{}", server, server_port)),
200                server,
201                port: server_port,
202                protocol: Protocol::Shadowsocks { method, password },
203                transport: None,
204                tls: None,
205                tags: Vec::new(),
206            },
207            SingBoxOutbound::Trojan {
208                tag,
209                server,
210                server_port,
211                password,
212                tls,
213            } => Node {
214                name: tag.unwrap_or_else(|| format!("trojan:{}:{}", server, server_port)),
215                server,
216                port: server_port,
217                protocol: Protocol::Trojan { password },
218                transport: None,
219                tls: tls.map(Into::into),
220                tags: Vec::new(),
221            },
222            // Proxy group types cannot be converted to individual nodes
223            SingBoxOutbound::Selector { tag, .. } => Node {
224                name: tag.unwrap_or_else(|| "selector".to_string()),
225                server: String::new(),
226                port: 0,
227                protocol: Protocol::Shadowsocks {
228                    method: "dummy".to_string(),
229                    password: "dummy".to_string(),
230                },
231                transport: None,
232                tls: None,
233                tags: Vec::new(),
234            },
235            SingBoxOutbound::Urltest { tag, .. } => Node {
236                name: tag.unwrap_or_else(|| "urltest".to_string()),
237                server: String::new(),
238                port: 0,
239                protocol: Protocol::Shadowsocks {
240                    method: "dummy".to_string(),
241                    password: "dummy".to_string(),
242                },
243                transport: None,
244                tls: None,
245                tags: Vec::new(),
246            },
247            SingBoxOutbound::Direct { tag } => Node {
248                name: tag.unwrap_or_else(|| "direct".to_string()),
249                server: String::new(),
250                port: 0,
251                protocol: Protocol::Shadowsocks {
252                    method: "dummy".to_string(),
253                    password: "dummy".to_string(),
254                },
255                transport: None,
256                tls: None,
257                tags: Vec::new(),
258            },
259            SingBoxOutbound::Block { tag } => Node {
260                name: tag.unwrap_or_else(|| "block".to_string()),
261                server: String::new(),
262                port: 0,
263                protocol: Protocol::Shadowsocks {
264                    method: "dummy".to_string(),
265                    password: "dummy".to_string(),
266                },
267                transport: None,
268                tls: None,
269                tags: Vec::new(),
270            },
271        }
272    }
273}
274
275impl TryFrom<Node> for SingBoxOutbound {
276    type Error = Error;
277
278    fn try_from(node: Node) -> Result<Self> {
279        match node.protocol {
280            Protocol::Shadowsocks { method, password } => {
281                if method.is_empty() || password.is_empty() {
282                    return Err(Error::ValidationError {
283                        reason: format!(
284                            "Empty method or password for Shadowsocks node: {}",
285                            node.name
286                        ),
287                    });
288                }
289
290                Ok(SingBoxOutbound::Shadowsocks {
291                    tag: Some(node.name),
292                    server: node.server,
293                    server_port: node.port,
294                    method,
295                    password,
296                })
297            }
298            Protocol::Trojan { password } => {
299                if password.is_empty() {
300                    return Err(Error::ValidationError {
301                        reason: format!("Empty password for Trojan node: {}", node.name),
302                    });
303                }
304
305                let tls = node.tls.map(Into::into);
306                Ok(SingBoxOutbound::Trojan {
307                    tag: Some(node.name),
308                    server: node.server,
309                    server_port: node.port,
310                    password,
311                    tls,
312                })
313            }
314            _ => Err(Error::Unsupported {
315                what: format!("protocol '{}' for sing-box", node.protocol.name()),
316            }),
317        }
318    }
319}