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#[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#[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#[derive(Debug, Clone, Default)]
69pub struct SingBoxConfig {
70 pub general: Option<Value>,
72 pub inbounds: Option<Value>,
74 pub outbounds: Vec<SingBoxOutbound>,
76 pub route: Option<Value>,
78 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 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
152impl 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
165impl 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
188impl 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 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}