use crate::generator::config::group::group_generate;
use crate::generator::config::remark::process_remark;
use crate::generator::ruleconvert::ruleset_to_sing_box::ruleset_to_sing_box;
use crate::models::{
ExtraSettings, Proxy, ProxyGroupConfigs, ProxyGroupType, ProxyType, RulesetContent,
};
use crate::utils::base64::base64_encode;
use crate::Settings;
use log::error;
use serde_json::{json, Map, Value as JsonValue};
fn format_singbox_interval(interval: u32) -> String {
let mut result = String::new();
let mut remaining_seconds = interval;
if remaining_seconds >= 3600 {
result.push_str(&format!("{}h", remaining_seconds / 3600));
remaining_seconds %= 3600;
}
if remaining_seconds >= 60 {
result.push_str(&format!("{}m", remaining_seconds / 60));
remaining_seconds %= 60;
}
if remaining_seconds > 0 {
result.push_str(&format!("{}s", remaining_seconds));
}
result
}
fn build_singbox_transport(proxy: &Proxy) -> JsonValue {
let mut transport = Map::new();
let transproto = proxy.transfer_protocol.as_deref().unwrap_or("");
match transproto {
"http" => {
if let Some(host) = &proxy.host {
if !host.is_empty() {
transport.insert("host".to_string(), JsonValue::String(host.clone()));
}
}
transport.insert("type".to_string(), JsonValue::String("http".to_string()));
}
"ws" => {
transport.insert("type".to_string(), JsonValue::String("ws".to_string()));
if let Some(path) = &proxy.path {
transport.insert("path".to_string(), JsonValue::String(path.clone()));
} else {
transport.insert("path".to_string(), JsonValue::String("/".to_string()));
}
let mut headers = Map::new();
if let Some(host) = &proxy.host {
if !host.is_empty() {
headers.insert("Host".to_string(), JsonValue::String(host.clone()));
}
}
if let Some(edge) = &proxy.edge {
if !edge.is_empty() {
headers.insert("Edge".to_string(), JsonValue::String(edge.clone()));
}
}
if !headers.is_empty() {
transport.insert("headers".to_string(), JsonValue::Object(headers));
}
}
"grpc" => {
transport.insert("type".to_string(), JsonValue::String("grpc".to_string()));
if let Some(path) = &proxy.path {
if !path.is_empty() {
transport.insert("service_name".to_string(), JsonValue::String(path.clone()));
}
}
}
_ => {} }
JsonValue::Object(transport)
}
fn add_singbox_common_members(
proxy_obj: &mut Map<String, JsonValue>,
proxy: &Proxy,
proxy_type: &str,
) {
proxy_obj.insert(
"type".to_string(),
JsonValue::String(proxy_type.to_string()),
);
proxy_obj.insert("tag".to_string(), JsonValue::String(proxy.remark.clone()));
proxy_obj.insert(
"server".to_string(),
JsonValue::String(proxy.hostname.clone()),
);
proxy_obj.insert(
"server_port".to_string(),
JsonValue::Number(proxy.port.into()),
);
}
fn string_array_to_json_array(array: &str, delimiter: &str) -> JsonValue {
let values: Vec<JsonValue> = array
.split(delimiter)
.map(|s| JsonValue::String(s.trim().to_string()))
.collect();
JsonValue::Array(values)
}
pub fn proxy_to_singbox(
nodes: &mut Vec<Proxy>,
base_conf: &str,
ruleset_content_array: &mut Vec<RulesetContent>,
extra_proxy_group: &ProxyGroupConfigs,
ext: &mut ExtraSettings,
) -> String {
let mut json: JsonValue = if ext.nodelist {
json!({})
} else {
match serde_json::from_str(base_conf) {
Ok(json) => json,
Err(e) => {
error!(
"SingBox base loader failed with error: {}, base_conf: {}",
e, base_conf
);
return String::new();
}
}
};
let mut outbounds = Vec::new();
let mut nodelist = Vec::new();
let mut remarks_list = Vec::new();
if !ext.nodelist {
outbounds.push(json!({
"type": "direct",
"tag": "DIRECT"
}));
outbounds.push(json!({
"type": "block",
"tag": "REJECT"
}));
outbounds.push(json!({
"type": "dns",
"tag": "dns-out"
}));
}
for node in nodes.iter_mut() {
if ext.append_proxy_type {
let proxy_type = node.proxy_type.to_string();
node.remark = format!("[{}] {}", proxy_type, node.remark);
}
let mut remark = node.remark.clone();
process_remark(&mut remark, &remarks_list, false);
node.remark = remark;
let mut udp = ext.udp;
let mut tfo = ext.tfo;
let mut scv = ext.skip_cert_verify;
udp = node.udp.as_ref().map_or(udp, |val| Some(*val));
tfo = node.tcp_fast_open.as_ref().map_or(tfo, |val| Some(*val));
scv = node.allow_insecure.as_ref().map_or(scv, |val| Some(*val));
let mut proxy_obj = match node.proxy_type {
ProxyType::Shadowsocks => {
let mut obj = Map::new();
add_singbox_common_members(&mut obj, node, "shadowsocks");
if let Some(method) = &node.encrypt_method {
obj.insert("method".to_string(), JsonValue::String(method.clone()));
}
if let Some(password) = &node.password {
obj.insert("password".to_string(), JsonValue::String(password.clone()));
}
if let (Some(plugin), Some(plugin_opts)) = (&node.plugin, &node.plugin_option) {
if !plugin.is_empty() && !plugin_opts.is_empty() {
let plugin_name = if plugin == "simple-obfs" {
"obfs-local"
} else {
plugin
};
obj.insert(
"plugin".to_string(),
JsonValue::String(plugin_name.to_string()),
);
obj.insert(
"plugin_opts".to_string(),
JsonValue::String(plugin_opts.clone()),
);
}
}
obj
}
ProxyType::ShadowsocksR => {
let mut obj = Map::new();
add_singbox_common_members(&mut obj, node, "shadowsocksr");
if let Some(method) = &node.encrypt_method {
obj.insert("method".to_string(), JsonValue::String(method.clone()));
}
if let Some(password) = &node.password {
obj.insert("password".to_string(), JsonValue::String(password.clone()));
}
if let Some(protocol) = &node.protocol {
obj.insert("protocol".to_string(), JsonValue::String(protocol.clone()));
}
if let Some(protocol_param) = &node.protocol_param {
obj.insert(
"protocol_param".to_string(),
JsonValue::String(protocol_param.clone()),
);
}
if let Some(obfs) = &node.obfs {
obj.insert("obfs".to_string(), JsonValue::String(obfs.clone()));
}
if let Some(obfs_param) = &node.obfs_param {
obj.insert(
"obfs_param".to_string(),
JsonValue::String(obfs_param.clone()),
);
}
obj
}
ProxyType::VMess => {
let mut obj = Map::new();
add_singbox_common_members(&mut obj, node, "vmess");
if let Some(user_id) = &node.user_id {
obj.insert("uuid".to_string(), JsonValue::String(user_id.clone()));
}
obj.insert(
"alter_id".to_string(),
JsonValue::Number(node.alter_id.into()),
);
if let Some(method) = &node.encrypt_method {
obj.insert("security".to_string(), JsonValue::String(method.clone()));
}
let transport = build_singbox_transport(node);
if !transport.as_object().unwrap().is_empty() {
obj.insert("transport".to_string(), transport);
}
obj
}
ProxyType::Trojan => {
let mut obj = Map::new();
add_singbox_common_members(&mut obj, node, "trojan");
if let Some(password) = &node.password {
obj.insert("password".to_string(), JsonValue::String(password.clone()));
}
let transport = build_singbox_transport(node);
if !transport.as_object().unwrap().is_empty() {
obj.insert("transport".to_string(), transport);
}
obj
}
ProxyType::WireGuard => {
let mut obj = Map::new();
obj.insert(
"type".to_string(),
JsonValue::String("wireguard".to_string()),
);
obj.insert("tag".to_string(), JsonValue::String(node.remark.clone()));
let mut addresses = Vec::new();
if let Some(self_ip) = &node.self_ip {
addresses.push(JsonValue::String(self_ip.clone()));
}
if let Some(self_ipv6) = &node.self_ipv6 {
if !self_ipv6.is_empty() {
addresses.push(JsonValue::String(self_ipv6.clone()));
}
}
obj.insert("local_address".to_string(), JsonValue::Array(addresses));
if let Some(private_key) = &node.private_key {
obj.insert(
"private_key".to_string(),
JsonValue::String(private_key.clone()),
);
}
let mut peer = Map::new();
peer.insert(
"server".to_string(),
JsonValue::String(node.hostname.clone()),
);
peer.insert(
"server_port".to_string(),
JsonValue::Number(node.port.into()),
);
if let Some(public_key) = &node.public_key {
peer.insert(
"public_key".to_string(),
JsonValue::String(public_key.clone()),
);
}
if let Some(pre_shared_key) = &node.pre_shared_key {
if !pre_shared_key.is_empty() {
peer.insert(
"pre_shared_key".to_string(),
JsonValue::String(pre_shared_key.clone()),
);
}
}
if !node.allowed_ips.is_empty() {
let allowed_ips = string_array_to_json_array(&node.allowed_ips, ",");
peer.insert("allowed_ips".to_string(), allowed_ips);
}
if let Some(client_id) = &node.client_id {
if !client_id.is_empty() {
let reserved = string_array_to_json_array(client_id, ",");
peer.insert("reserved".to_string(), reserved);
}
}
let peers = vec![JsonValue::Object(peer)];
obj.insert("peers".to_string(), JsonValue::Array(peers));
if node.mtu > 0 {
obj.insert("mtu".to_string(), JsonValue::Number(node.mtu.into()));
}
obj
}
ProxyType::Hysteria => {
let mut obj = Map::new();
add_singbox_common_members(&mut obj, node, "hysteria");
if node.up_speed > 0 {
obj.insert(
"up_mbps".to_string(),
JsonValue::Number(node.up_speed.into()),
);
}
if node.down_speed > 0 {
obj.insert(
"down_mbps".to_string(),
JsonValue::Number(node.down_speed.into()),
);
}
if let Some(obfs) = &node.obfs {
if !obfs.is_empty() {
obj.insert("obfs".to_string(), JsonValue::String(obfs.clone()));
}
}
if let Some(auth_str) = &node.auth_str {
if !auth_str.is_empty() {
obj.insert("auth_str".to_string(), JsonValue::String(auth_str.clone()));
let auth_str_value = auth_str.clone();
obj.insert(
"auth".to_string(),
JsonValue::String(base64_encode(&auth_str_value)),
);
}
}
if node.recv_window_conn > 0 {
obj.insert(
"recv_window_conn".to_string(),
JsonValue::Number(node.recv_window_conn.into()),
);
}
if node.recv_window > 0 {
obj.insert(
"recv_window".to_string(),
JsonValue::Number(node.recv_window.into()),
);
}
if let Some(disable_mtu_discovery) = node.disable_mtu_discovery {
obj.insert(
"disable_mtu_discovery".to_string(),
JsonValue::Bool(disable_mtu_discovery),
);
}
let mut tls = Map::new();
tls.insert("enabled".to_string(), JsonValue::Bool(true));
if let Some(allow_insecure) = scv {
tls.insert("insecure".to_string(), JsonValue::Bool(allow_insecure));
}
if !node.alpn.is_empty() && node.alpn.len() > 0 {
let alpn = vec![JsonValue::String(node.alpn.iter().next().unwrap().clone())];
tls.insert("alpn".to_string(), JsonValue::Array(alpn));
}
if let Some(ca) = &node.ca {
if !ca.is_empty() {
tls.insert("certificate".to_string(), JsonValue::String(ca.to_string()));
}
}
if let Some(ca_str) = &node.ca_str {
if !ca_str.is_empty() {
tls.insert("certificate".to_string(), JsonValue::String(ca_str.clone()));
}
}
obj.insert("tls".to_string(), JsonValue::Object(tls));
obj
}
ProxyType::Hysteria2 => {
let mut obj = Map::new();
add_singbox_common_members(&mut obj, node, "hysteria2");
if node.up_speed > 0 {
obj.insert(
"up_mbps".to_string(),
JsonValue::Number(node.up_speed.into()),
);
}
if node.down_speed > 0 {
obj.insert(
"down_mbps".to_string(),
JsonValue::Number(node.down_speed.into()),
);
}
if let Some(obfs) = &node.obfs {
if !obfs.is_empty() {
let mut obfs_obj = Map::new();
obfs_obj.insert("type".to_string(), JsonValue::String(obfs.clone()));
if let Some(obfs_param) = &node.obfs_param {
if !obfs_param.is_empty() {
obfs_obj.insert(
"password".to_string(),
JsonValue::String(obfs_param.clone()),
);
}
}
obj.insert("obfs".to_string(), JsonValue::Object(obfs_obj));
}
}
if let Some(password) = &node.password {
if !password.is_empty() {
obj.insert("password".to_string(), JsonValue::String(password.clone()));
}
}
let mut tls = Map::new();
tls.insert("enabled".to_string(), JsonValue::Bool(true));
if let Some(allow_insecure) = scv {
tls.insert("insecure".to_string(), JsonValue::Bool(allow_insecure));
}
if !node.alpn.is_empty() && node.alpn.len() > 0 {
let alpn = vec![JsonValue::String(node.alpn.iter().next().unwrap().clone())];
tls.insert("alpn".to_string(), JsonValue::Array(alpn));
}
if let Some(ca) = &node.ca {
if !ca.is_empty() {
tls.insert("certificate".to_string(), JsonValue::String(ca.to_string()));
}
}
if let Some(ca_str) = &node.ca_str {
if !ca_str.is_empty() {
tls.insert("certificate".to_string(), JsonValue::String(ca_str.clone()));
}
}
obj.insert("tls".to_string(), JsonValue::Object(tls));
obj
}
ProxyType::HTTP | ProxyType::HTTPS => {
let mut obj = Map::new();
add_singbox_common_members(&mut obj, node, "http");
if let Some(username) = &node.username {
obj.insert("username".to_string(), JsonValue::String(username.clone()));
}
if let Some(password) = &node.password {
obj.insert("password".to_string(), JsonValue::String(password.clone()));
}
obj
}
ProxyType::Socks5 => {
let mut obj = Map::new();
add_singbox_common_members(&mut obj, node, "socks");
obj.insert("version".to_string(), JsonValue::String("5".to_string()));
if let Some(username) = &node.username {
obj.insert("username".to_string(), JsonValue::String(username.clone()));
}
if let Some(password) = &node.password {
obj.insert("password".to_string(), JsonValue::String(password.clone()));
}
obj
}
_ => continue, };
if node.tls_secure {
let mut tls = Map::new();
tls.insert("enabled".to_string(), JsonValue::Bool(true));
if let Some(server_name) = &node.server_name {
if !server_name.is_empty() {
tls.insert(
"server_name".to_string(),
JsonValue::String(server_name.clone()),
);
}
} else if let Some(host) = &node.host {
if !host.is_empty() {
tls.insert("server_name".to_string(), JsonValue::String(host.clone()));
}
}
if let Some(allow_insecure) = scv {
tls.insert("insecure".to_string(), JsonValue::Bool(allow_insecure));
}
proxy_obj.insert("tls".to_string(), JsonValue::Object(tls));
}
if let Some(udp_enabled) = udp {
if !udp_enabled {
proxy_obj.insert("network".to_string(), JsonValue::String("tcp".to_string()));
}
}
if let Some(tfo_enabled) = tfo {
proxy_obj.insert("tcp_fast_open".to_string(), JsonValue::Bool(tfo_enabled));
}
nodelist.push(node.clone());
remarks_list.push(node.remark.clone());
outbounds.push(JsonValue::Object(proxy_obj));
}
if ext.nodelist {
if let JsonValue::Object(obj) = &mut json {
obj.insert("outbounds".to_string(), JsonValue::Array(outbounds));
}
return serde_json::to_string_pretty(&json).unwrap_or_default();
}
for group in extra_proxy_group {
let mut filtered_nodelist = Vec::new();
let group_type = match group.group_type {
ProxyGroupType::Select => "selector",
ProxyGroupType::URLTest | ProxyGroupType::Fallback | ProxyGroupType::LoadBalance => {
"urltest"
}
_ => continue, };
for proxy_name in &group.proxies {
group_generate(proxy_name, &nodelist, &mut filtered_nodelist, true, ext);
}
if filtered_nodelist.is_empty() {
filtered_nodelist.push("DIRECT".to_string());
}
let mut group_obj = Map::new();
group_obj.insert(
"type".to_string(),
JsonValue::String(group_type.to_string()),
);
group_obj.insert("tag".to_string(), JsonValue::String(group.name.clone()));
let group_outbounds: Vec<JsonValue> = filtered_nodelist
.iter()
.map(|name| JsonValue::String(name.clone()))
.collect();
group_obj.insert("outbounds".to_string(), JsonValue::Array(group_outbounds));
if group.group_type == ProxyGroupType::URLTest {
group_obj.insert("url".to_string(), JsonValue::String(group.url.clone()));
group_obj.insert(
"interval".to_string(),
JsonValue::String(format_singbox_interval(group.interval as u32)),
);
if group.tolerance > 0 {
group_obj.insert(
"tolerance".to_string(),
JsonValue::Number(group.tolerance.into()),
);
}
}
outbounds.push(JsonValue::Object(group_obj));
}
let global = Settings::current();
if global.singbox_add_clash_modes {
let mut global_group = Map::new();
global_group.insert(
"type".to_string(),
JsonValue::String("selector".to_string()),
);
global_group.insert("tag".to_string(), JsonValue::String("GLOBAL".to_string()));
let mut global_outbounds = vec![JsonValue::String("DIRECT".to_string())];
for remark in &remarks_list {
global_outbounds.push(JsonValue::String(remark.clone()));
}
global_group.insert("outbounds".to_string(), JsonValue::Array(global_outbounds));
outbounds.push(JsonValue::Object(global_group));
}
if let JsonValue::Object(obj) = &mut json {
obj.insert("outbounds".to_string(), JsonValue::Array(outbounds));
}
if !ext.enable_rule_generator {
return serde_json::to_string_pretty(&json).unwrap_or_default();
}
ruleset_to_sing_box(
&mut json,
ruleset_content_array,
ext.overwrite_original_rules,
);
serde_json::to_string_pretty(&json).unwrap_or_default()
}
pub fn generate_singbox(
_proxies: &[Proxy],
_group_config: &ProxyGroupConfigs,
_ruleset_content_array: &mut Vec<RulesetContent>,
_config: &str,
_extra_settings: &ExtraSettings,
) -> String {
String::new()
}