Files
SbLauncher/Import.cs
T

735 lines
36 KiB
C#
Raw Normal View History

2026-09-26 14:41:04 +03:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Script.Serialization;
namespace SbLauncher
{
public class ImportResult
{
public readonly List<ServerCfg> Servers = new List<ServerCfg>();
public readonly List<string> Errors = new List<string>();
}
// Turns share links and pasted subscriptions into sing-box outbounds.
// Unknown transports are rejected rather than silently changing the protocol.
public static class KeyImporter
{
public static bool Same(ServerCfg a, ServerCfg b)
{
if (!string.Equals(a.Protocol, b.Protocol, StringComparison.OrdinalIgnoreCase) ||
!string.Equals(a.Server, b.Server, StringComparison.OrdinalIgnoreCase) || a.Port != b.Port)
return false;
if (a.Outbound != null && b.Outbound != null)
return Canonical(a.Outbound) == Canonical(b.Outbound);
if (a.Protocol == "vless") return string.Equals(a.Uuid, b.Uuid, StringComparison.OrdinalIgnoreCase);
return false;
}
static string Canonical(object value)
{
Dictionary<string, object> map = value as Dictionary<string, object>;
if (map != null)
{
List<string> keys = new List<string>(map.Keys);
keys.Sort(StringComparer.Ordinal);
StringBuilder output = new StringBuilder("{");
foreach (string key in keys)
output.Append(J.ToJson(key)).Append(':').Append(Canonical(map[key])).Append(',');
return output.Append('}').ToString();
}
object[] items = value as object[];
if (items != null)
{
StringBuilder output = new StringBuilder("[");
foreach (object item in items) output.Append(Canonical(item)).Append(',');
return output.Append(']').ToString();
}
return J.ToJson(value);
}
public static bool CheckWithSingBox(Paths paths, ServerCfg server, out string error)
{
error = "";
if (!File.Exists(paths.SbExe)) return true;
string file = Path.Combine(paths.RunDir, "import-check-" + Guid.NewGuid().ToString("N") + ".json");
try
{
Dictionary<string, object> outbound = J.Parse(J.ToJson(server.Outbound));
outbound["tag"] = "import-test";
J.Write(file, "{\"outbounds\":[" + J.ToJson(outbound) + "]}");
string output;
if (SingBoxRunner.Check(paths.SbExe, file, paths.Root, out output)) return true;
error = string.IsNullOrEmpty(output) ? "sing-box отклонил конфигурацию" : output;
Dictionary<string, object> tls = J.Obj(outbound.ContainsKey("tls") ? outbound["tls"] : null);
Dictionary<string, object> reality = tls == null ? null : J.Obj(tls.ContainsKey("reality") ? tls["reality"] : null);
Dictionary<string, object> obfs = J.Obj(outbound.ContainsKey("obfs") ? outbound["obfs"] : null);
foreach (string secret in new string[] { J.Str(outbound, "uuid", ""), J.Str(outbound, "password", ""),
J.Str(reality, "public_key", ""), J.Str(obfs, "password", "") })
if (!string.IsNullOrEmpty(secret)) error = error.Replace(secret, "[скрыто]");
if (error.Length > 300) error = error.Substring(0, 300) + "…";
return false;
}
catch (Exception ex) { error = ex.Message; return false; }
finally { try { if (File.Exists(file)) File.Delete(file); } catch { } }
}
const int MaxInput = 2000000;
static readonly Regex LinkPattern = new Regex(
@"(?i)(?:vless|vmess|trojan|ss|hysteria2|hy2|tuic|anytls)://\S+",
RegexOptions.Compiled);
public static ImportResult Parse(string input)
{
ImportResult result = new ImportResult();
if (string.IsNullOrWhiteSpace(input))
{
result.Errors.Add("поле ввода пусто");
return result;
}
if (input.Length > MaxInput)
{
result.Errors.Add("слишком большой текст подписки (максимум 2 МБ)");
return result;
}
ParseText(input.Trim().Trim('\uFEFF'), result, 0);
if (result.Servers.Count == 0 && result.Errors.Count == 0)
result.Errors.Add("поддерживаемых ключей не найдено");
return result;
}
static void ParseText(string text, ImportResult result, int depth)
{
if (depth > 2) { result.Errors.Add("слишком много слоёв base64"); return; }
string trimmed = text.Trim();
if (trimmed.StartsWith("{") || trimmed.StartsWith("["))
{
ParseJson(trimmed, result);
return;
}
if (!trimmed.Contains("://"))
{
string decoded;
if (TryDecodeBase64(trimmed, out decoded) && decoded != trimmed &&
(decoded.Contains("://") || decoded.TrimStart().StartsWith("{") || decoded.TrimStart().StartsWith("[")))
{
ParseText(decoded, result, depth + 1);
return;
}
}
string[] lines = text.Replace("\r", "").Split('\n');
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i].Trim();
if (line.Length == 0 || line.StartsWith("#") || line.StartsWith("//")) continue;
MatchCollection links = LinkPattern.Matches(line);
if (links.Count == 0)
{
if (Regex.IsMatch(line, @"(?i)^https?://\S+$"))
{
try { ParseText(FetchSubscription(line), result, depth + 1); }
catch (Exception ex) { result.Errors.Add("строка " + (i + 1) + ": " + ex.Message); }
}
else result.Errors.Add("строка " + (i + 1) + ": неизвестный формат ключа");
continue;
}
foreach (Match match in links)
{
if (result.Servers.Count >= 1000)
{
result.Errors.Add("лимит импорта — 1000 серверов");
return;
}
ServerCfg server;
string error;
if (TryParseOne(match.Value, out server, out error)) result.Servers.Add(server);
else result.Errors.Add("строка " + (i + 1) + ": " + error);
}
}
}
static string FetchSubscription(string url)
{
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
if (new Uri(url).IsLoopback) request.Proxy = null;
request.Timeout = 10000;
request.ReadWriteTimeout = 10000;
request.AllowAutoRedirect = true;
request.MaximumAutomaticRedirections = 3;
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
request.UserAgent = "SbLauncher/2";
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
using (Stream stream = response.GetResponseStream())
using (StreamReader reader = new StreamReader(stream, new UTF8Encoding(false, true), true))
{
if (response.ContentLength > MaxInput)
throw new FormatException("подписка больше 2 МБ");
StringBuilder body = new StringBuilder();
char[] buffer = new char[4096];
int count;
while ((count = reader.Read(buffer, 0, buffer.Length)) > 0)
{
body.Append(buffer, 0, count);
if (body.Length > MaxInput) throw new FormatException("подписка больше 2 МБ");
}
return body.ToString();
}
}
catch (FormatException) { throw; }
catch (WebException ex) { throw new FormatException("не удалось загрузить подписку (" + ex.Status + ")"); }
catch (Exception) { throw new FormatException("не удалось прочитать подписку"); }
}
static void ParseJson(string text, ImportResult result)
{
try
{
JavaScriptSerializer ser = new JavaScriptSerializer();
ser.MaxJsonLength = MaxInput;
object root = ser.DeserializeObject(J.StripComments(text));
Dictionary<string, object> document = root as Dictionary<string, object>;
object[] outbounds = root as object[];
if (document != null)
{
object listed;
if (document.TryGetValue("outbounds", out listed)) outbounds = listed as object[];
else if (document.ContainsKey("type")) outbounds = new object[] { document };
}
if (outbounds == null) { result.Errors.Add("JSON не содержит outbounds"); return; }
foreach (object item in outbounds)
{
Dictionary<string, object> outbound = J.Obj(item);
if (outbound == null) continue;
string type = J.Str(outbound, "type", "").ToLowerInvariant();
if (!Supported(type)) continue; // direct, selector, block etc. are not keys
try
{
if (result.Servers.Count >= 1000) throw new FormatException("лимит импорта — 1000 серверов");
result.Servers.Add(FromOutbound(outbound));
}
catch (Exception ex) { result.Errors.Add("JSON: " + ex.Message); }
}
if (result.Servers.Count == 0 && result.Errors.Count == 0)
result.Errors.Add("в JSON нет поддерживаемых прокси-серверов");
}
catch (Exception ex) { result.Errors.Add("не удалось прочитать JSON: " + ex.Message); }
}
public static bool TryParseOne(string text, out ServerCfg server, out string error)
{
server = null;
error = "";
try
{
if (string.IsNullOrWhiteSpace(text)) throw new FormatException("пустой ключ");
string raw = text.Trim();
int schemeEnd = raw.IndexOf("://", StringComparison.Ordinal);
if (schemeEnd < 0) throw new FormatException("нет схемы протокола");
string scheme = raw.Substring(0, schemeEnd).ToLowerInvariant();
string body = raw.Substring(schemeEnd + 3);
if (!Supported(scheme)) throw new FormatException("протокол " + scheme + " пока не поддерживается");
if (scheme == "vmess" && body.IndexOf('@') < 0)
server = ParseVmessJson(body);
else if (scheme == "ss") server = ParseShadowsocks(body);
else
{
Link link = ParseLink(body);
if (scheme == "vless") server = ParseVless(link);
else if (scheme == "vmess") server = ParseVmessUri(link);
else if (scheme == "trojan") server = ParseTrojan(link);
else if (scheme == "hysteria2" || scheme == "hy2") server = ParseHysteria2(link);
else if (scheme == "tuic") server = ParseTuic(link);
else if (scheme == "anytls") server = ParseAnyTls(link);
}
if (server == null) throw new FormatException("ключ не распознан");
return true;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
static bool Supported(string type)
{
return type == "vless" || type == "vmess" || type == "trojan" ||
type == "ss" || type == "shadowsocks" || type == "hysteria2" ||
type == "hy2" || type == "tuic" || type == "anytls";
}
sealed class Link
{
public string User, Host, Name;
public int Port;
public Dictionary<string, string> Query;
}
static Link ParseLink(string body)
{
string name = "";
int hash = body.IndexOf('#');
if (hash >= 0)
{
name = Decode(body.Substring(hash + 1));
int metadata = name.IndexOf("?serverDescription=", StringComparison.OrdinalIgnoreCase);
if (metadata >= 0) name = name.Substring(0, metadata);
body = body.Substring(0, hash);
}
string query = "";
int question = body.IndexOf('?');
if (question >= 0) { query = body.Substring(question + 1); body = body.Substring(0, question); }
body = body.TrimEnd('/');
int at = body.LastIndexOf('@');
if (at < 1) throw new FormatException("в ключе нет учётных данных или адреса сервера");
string user = Decode(body.Substring(0, at));
string address = body.Substring(at + 1);
string host;
int port;
ParseAddress(address, out host, out port);
return new Link { User = user, Host = host, Port = port,
Name = name.Length > 0 ? name : host, Query = Query(query) };
}
static void ParseAddress(string address, out string host, out int port)
{
string digits;
if (address.StartsWith("["))
{
int close = address.IndexOf(']');
if (close < 0 || close + 1 >= address.Length || address[close + 1] != ':')
throw new FormatException("неверный IPv6-адрес или порт");
host = address.Substring(1, close - 1);
digits = address.Substring(close + 2);
}
else
{
int colon = address.LastIndexOf(':');
if (colon < 1 || address.IndexOf(':') != colon)
throw new FormatException("адрес должен иметь вид host:port (IPv6 — в квадратных скобках)");
host = address.Substring(0, colon);
digits = address.Substring(colon + 1);
}
host = Decode(host).Trim();
if (host.Length == 0 || host.Contains(" ")) throw new FormatException("пустой или неверный адрес сервера");
if (!int.TryParse(digits, NumberStyles.None, CultureInfo.InvariantCulture, out port) || port < 1 || port > 65535)
throw new FormatException("порт должен быть числом от 1 до 65535");
}
static Dictionary<string, string> Query(string text)
{
Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (string pair in text.Split('&'))
{
if (pair.Length == 0) continue;
int eq = pair.IndexOf('=');
string key = Decode((eq < 0 ? pair : pair.Substring(0, eq)).Replace('+', ' '));
string value = eq < 0 ? "" : Decode(pair.Substring(eq + 1).Replace('+', ' '));
values[key] = value;
}
return values;
}
static string Get(Dictionary<string, string> q, string key, string fallback)
{
string value;
return q.TryGetValue(key, out value) ? value : fallback;
}
static string First(Dictionary<string, string> q, string a, string b, string fallback)
{
string value = Get(q, a, "");
return value.Length > 0 ? value : Get(q, b, fallback);
}
static string Decode(string value) { return Uri.UnescapeDataString(value); }
static bool TryDecodeBase64(string text, out string decoded)
{
decoded = null;
try
{
string clean = Regex.Replace(text, @"\s", "").Replace('-', '+').Replace('_', '/');
if (clean.Length == 0 || clean.Length > MaxInput * 2) return false;
clean = clean.PadRight((clean.Length + 3) / 4 * 4, '=');
decoded = new UTF8Encoding(false, true).GetString(Convert.FromBase64String(clean));
return true;
}
catch { return false; }
}
static string Base64(string value)
{
string decoded;
if (!TryDecodeBase64(value, out decoded)) throw new FormatException("неверная base64-строка");
return decoded;
}
static Dictionary<string, object> Base(string protocol, Link link)
{
Dictionary<string, object> outbound = new Dictionary<string, object>();
outbound["type"] = protocol;
outbound["server"] = link.Host;
outbound["server_port"] = link.Port;
return outbound;
}
static ServerCfg Finish(string protocol, Link link, Dictionary<string, object> outbound)
{
ServerCfg server = new ServerCfg();
server.Protocol = protocol;
server.Server = link.Host;
server.Port = link.Port;
server.Name = link.Name;
server.Code = MakeCode(server.Name);
server.Uuid = J.Str(outbound, "uuid", "");
server.Sni = J.Str(J.Obj(outbound.ContainsKey("tls") ? outbound["tls"] : null), "server_name", "");
server.Outbound = outbound;
return server;
}
static string MakeCode(string name)
{
StringBuilder code = new StringBuilder();
foreach (char c in name)
{
if (char.IsLetterOrDigit(c)) code.Append(char.ToUpperInvariant(c));
if (code.Length == 2) break;
}
return code.Length > 0 ? code.ToString() : "??";
}
static void AddTls(Dictionary<string, object> outbound, Link link, string mode, bool defaultEnabled)
{
string security = Get(link.Query, "security", mode).ToLowerInvariant();
if (security.Length == 0) security = defaultEnabled ? "tls" : "none";
if (security == "none" || security == "false") return;
if (security != "tls" && security != "reality")
throw new FormatException("режим TLS " + security + " не поддерживается");
Dictionary<string, object> tls = new Dictionary<string, object>();
tls["enabled"] = true;
string sni = First(link.Query, "sni", "peer", link.Host);
if (sni.Length > 0) tls["server_name"] = sni;
string alpn = Get(link.Query, "alpn", "");
if (alpn.Length > 0) tls["alpn"] = alpn.Split(',');
string insecure = First(link.Query, "allowInsecure", "insecure", "");
if (insecure == "1" || insecure.Equals("true", StringComparison.OrdinalIgnoreCase)) tls["insecure"] = true;
string fp = Get(link.Query, "fp", security == "reality" ? "chrome" : "");
if (fp.Length > 0 && fp != "none")
tls["utls"] = new Dictionary<string, object> { { "enabled", true }, { "fingerprint", fp } };
if (security == "reality")
{
string publicKey = First(link.Query, "pbk", "publicKey", "");
if (publicKey.Length == 0) throw new FormatException("в Reality отсутствует public key (pbk)");
tls["reality"] = new Dictionary<string, object> {
{ "enabled", true }, { "public_key", publicKey },
{ "short_id", First(link.Query, "sid", "shortId", "") }
};
}
outbound["tls"] = tls;
}
static void AddTransport(Dictionary<string, object> outbound, Dictionary<string, string> q)
{
string type = First(q, "type", "net", "tcp").ToLowerInvariant();
if (type == "" || type == "tcp" || type == "raw")
{
string header = Get(q, "headerType", "none");
if (header.Length > 0 && header != "none")
throw new FormatException("TCP headerType=" + header + " пока не поддерживается");
return;
}
Dictionary<string, object> transport = new Dictionary<string, object>();
if (type == "ws" || type == "websocket")
{
transport["type"] = "ws";
string path = Get(q, "path", "/");
Match early = Regex.Match(path, @"(?:\?|&)ed=(\d+)(?:&|$)");
if (early.Success)
{
int ed;
if (int.TryParse(early.Groups[1].Value, out ed) && ed > 0)
{
transport["max_early_data"] = ed;
transport["early_data_header_name"] = "Sec-WebSocket-Protocol";
}
path = path.Substring(0, early.Index).TrimEnd('?','&');
}
transport["path"] = path.Length > 0 ? path : "/";
string host = Get(q, "host", "");
if (host.Length > 0)
transport["headers"] = new Dictionary<string, object> { { "Host", host } };
}
else if (type == "grpc")
{
transport["type"] = "grpc";
transport["service_name"] = First(q, "serviceName", "path", "").TrimStart('/');
}
else if (type == "httpupgrade")
{
transport["type"] = "httpupgrade";
transport["path"] = Get(q, "path", "/");
string host = Get(q, "host", "");
if (host.Length > 0) transport["host"] = host;
}
else if (type == "http" || type == "h2")
{
transport["type"] = "http";
transport["path"] = Get(q, "path", "/");
string host = Get(q, "host", "");
if (host.Length > 0) transport["host"] = host.Split(',');
}
else if (type == "quic") transport["type"] = "quic";
else throw new FormatException("транспорт " + type + " пока не поддерживается");
outbound["transport"] = transport;
}
static ServerCfg ParseVless(Link link)
{
Guid id;
if (!Guid.TryParse(link.User, out id)) throw new FormatException("неверный UUID VLESS");
string encryption = Get(link.Query, "encryption", "none");
if (encryption.Length > 0 && encryption != "none")
throw new FormatException("шифрование VLESS " + encryption + " не поддерживается");
Dictionary<string, object> outbound = Base("vless", link);
outbound["uuid"] = link.User;
string flow = Get(link.Query, "flow", "");
if (flow.Length > 0)
{
if (flow != "xtls-rprx-vision") throw new FormatException("flow " + flow + " не поддерживается");
outbound["flow"] = flow;
}
string security = Get(link.Query, "security", Get(link.Query, "pbk", "").Length > 0 ? "reality" : "none");
AddTls(outbound, link, security, false);
AddTransport(outbound, link.Query);
outbound["packet_encoding"] = First(link.Query, "packetEncoding", "packet_encoding", "xudp");
ServerCfg result = Finish("vless", link, outbound);
result.Flow = flow;
result.PublicKey = Get(link.Query, "pbk", "");
result.ShortId = Get(link.Query, "sid", "");
result.Fingerprint = Get(link.Query, "fp", "chrome");
result.SpiderX = Get(link.Query, "spx", "");
return result;
}
static ServerCfg ParseVmessJson(string body)
{
string decoded = Base64(body.Split('#')[0]);
Dictionary<string, object> data = J.Parse(decoded);
if (data == null) throw new FormatException("VMess base64 не содержит JSON-объект");
string address = J.Str(data, "add", "");
int port = J.Int(data, "port", 0);
if (address.Length == 0 || port < 1 || port > 65535) throw new FormatException("VMess: неверный адрес или порт");
Link link = new Link();
link.User = J.Str(data, "id", "");
link.Host = address;
link.Port = port;
link.Name = J.Str(data, "ps", address);
link.Query = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
link.Query["type"] = J.Str(data, "net", "tcp");
link.Query["path"] = J.Str(data, "path", "");
link.Query["host"] = J.Str(data, "host", "");
link.Query["sni"] = J.Str(data, "sni", "");
link.Query["fp"] = J.Str(data, "fp", "");
link.Query["alpn"] = J.Str(data, "alpn", "");
string vmessTls = J.Str(data, "tls", "").ToLowerInvariant();
if (vmessTls != "" && vmessTls != "tls" && vmessTls != "none")
throw new FormatException("VMess: режим TLS " + vmessTls + " не поддерживается");
link.Query["security"] = vmessTls == "tls" ? "tls" : "none";
link.Query["headerType"] = J.Str(data, "type", "none");
link.Query["vmess_security"] = J.Str(data, "scy", "auto");
link.Query["alter_id"] = J.Str(data, "aid", "0");
return ParseVmessUri(link);
}
static ServerCfg ParseVmessUri(Link link)
{
Guid id;
if (!Guid.TryParse(link.User, out id)) throw new FormatException("неверный UUID VMess");
Dictionary<string, object> outbound = Base("vmess", link);
outbound["uuid"] = link.User;
outbound["security"] = First(link.Query, "vmess_security", "encryption", "auto");
int aid;
if (!int.TryParse(First(link.Query, "alter_id", "aid", "0"), out aid) || aid < 0)
throw new FormatException("неверный alter_id VMess");
outbound["alter_id"] = aid;
AddTls(outbound, link, Get(link.Query, "security", "none"), false);
AddTransport(outbound, link.Query);
return Finish("vmess", link, outbound);
}
static ServerCfg ParseTrojan(Link link)
{
if (link.User.Length == 0) throw new FormatException("пустой пароль Trojan");
Dictionary<string, object> outbound = Base("trojan", link);
outbound["password"] = link.User;
AddTls(outbound, link, Get(link.Query, "security", "tls"), true);
AddTransport(outbound, link.Query);
return Finish("trojan", link, outbound);
}
static ServerCfg ParseShadowsocks(string body)
{
string original = body;
int hash = body.IndexOf('#');
string name = "";
if (hash >= 0) { name = Decode(body.Substring(hash + 1)); body = body.Substring(0, hash); }
int queryAt = body.IndexOf('?');
string query = "";
if (queryAt >= 0) { query = body.Substring(queryAt + 1); body = body.Substring(0, queryAt); }
body = body.TrimEnd('/');
if (body.IndexOf('@') < 0) body = Base64(body);
int at = body.LastIndexOf('@');
if (at < 1) throw new FormatException("Shadowsocks: нет адреса или пароля");
string user = body.Substring(0, at);
if (user.IndexOf(':') < 0) user = Base64(user);
else user = Decode(user);
int colon = user.IndexOf(':');
if (colon < 1 || colon == user.Length - 1) throw new FormatException("Shadowsocks: ожидается method:password");
string method = user.Substring(0, colon);
string password = user.Substring(colon + 1);
string host; int port;
ParseAddress(body.Substring(at + 1), out host, out port);
Link link = new Link { User = password, Host = host, Port = port,
Name = name.Length > 0 ? name : host, Query = Query(query) };
Dictionary<string, object> outbound = Base("shadowsocks", link);
outbound["method"] = method;
outbound["password"] = password;
string plugin = Get(link.Query, "plugin", "");
if (plugin.Length > 0)
{
string[] parts = plugin.Split(new char[] {';'}, 2);
if (parts[0] != "obfs-local" && parts[0] != "v2ray-plugin")
throw new FormatException("плагин Shadowsocks " + parts[0] + " не поддерживается sing-box");
outbound["plugin"] = parts[0];
if (parts.Length > 1) outbound["plugin_opts"] = parts[1];
}
return Finish("shadowsocks", link, outbound);
}
static ServerCfg ParseHysteria2(Link link)
{
if (link.User.Length == 0) throw new FormatException("пустой пароль Hysteria2");
Dictionary<string, object> outbound = Base("hysteria2", link);
outbound["password"] = link.User;
Dictionary<string, object> tls = new Dictionary<string, object> { { "enabled", true } };
tls["server_name"] = Get(link.Query, "sni", link.Host);
string alpn = Get(link.Query, "alpn", "");
if (alpn.Length > 0) tls["alpn"] = alpn.Split(',');
string insecure = Get(link.Query, "insecure", "");
if (insecure == "1" || insecure.Equals("true", StringComparison.OrdinalIgnoreCase)) tls["insecure"] = true;
outbound["tls"] = tls;
string obfs = Get(link.Query, "obfs", "");
if (obfs.Length > 0)
{
if (obfs != "salamander") throw new FormatException("обфускация Hysteria2 " + obfs + " не поддерживается этой версией sing-box");
string secret = First(link.Query, "obfs-password", "obfsPassword", "");
if (secret.Length == 0) throw new FormatException("нет obfs-password Hysteria2");
outbound["obfs"] = new Dictionary<string, object> { { "type", "salamander" }, { "password", secret } };
}
string ports = Get(link.Query, "mport", "");
if (ports.Length > 0)
{
outbound.Remove("server_port");
outbound["server_ports"] = ports.Replace('-', ':').Split(',');
}
string up = Get(link.Query, "upmbps", ""), down = Get(link.Query, "downmbps", "");
int value;
if (int.TryParse(up, out value) && value > 0) outbound["up_mbps"] = value;
if (int.TryParse(down, out value) && value > 0) outbound["down_mbps"] = value;
return Finish("hysteria2", link, outbound);
}
static ServerCfg ParseTuic(Link link)
{
int colon = link.User.IndexOf(':');
if (colon < 1 || colon == link.User.Length - 1) throw new FormatException("TUIC: ожидается UUID:пароль");
string uuid = link.User.Substring(0, colon), password = link.User.Substring(colon + 1);
Guid id;
if (!Guid.TryParse(uuid, out id)) throw new FormatException("неверный UUID TUIC");
Dictionary<string, object> outbound = Base("tuic", link);
outbound["uuid"] = uuid;
outbound["password"] = password;
string cc = Get(link.Query, "congestion_control", "");
if (cc.Length > 0) outbound["congestion_control"] = cc;
string relay = Get(link.Query, "udp_relay_mode", "");
if (relay.Length > 0) outbound["udp_relay_mode"] = relay;
Dictionary<string, object> tls = new Dictionary<string, object> { { "enabled", true } };
tls["server_name"] = Get(link.Query, "sni", link.Host);
string alpn = Get(link.Query, "alpn", "");
if (alpn.Length > 0) tls["alpn"] = alpn.Split(',');
string insecure = Get(link.Query, "insecure", "");
if (insecure == "1" || insecure.Equals("true", StringComparison.OrdinalIgnoreCase)) tls["insecure"] = true;
outbound["tls"] = tls;
return Finish("tuic", link, outbound);
}
static ServerCfg ParseAnyTls(Link link)
{
if (link.User.Length == 0) throw new FormatException("пустой пароль AnyTLS");
Dictionary<string, object> outbound = Base("anytls", link);
outbound["password"] = link.User;
AddTls(outbound, link, "tls", true);
if (!outbound.ContainsKey("tls")) throw new FormatException("AnyTLS требует TLS");
return Finish("anytls", link, outbound);
}
static ServerCfg FromOutbound(Dictionary<string, object> input)
{
Dictionary<string, object> outbound = J.Parse(J.ToJson(input));
string type = J.Str(outbound, "type", "").ToLowerInvariant();
if (type == "hy2") type = "hysteria2";
if (type == "ss") type = "shadowsocks";
string host = J.Str(outbound, "server", "");
int port = J.Int(outbound, "server_port", 0);
if (host.Length == 0 || port < 1 || port > 65535)
throw new FormatException("outbound " + type + ": неверный server/server_port");
string auth = type == "vless" || type == "vmess" || type == "tuic" ?
J.Str(outbound, "uuid", "") : J.Str(outbound, "password", "");
if (auth.Length == 0) throw new FormatException("outbound " + type + ": нет UUID или пароля");
string name = J.Str(outbound, "tag", host);
outbound["type"] = type;
outbound.Remove("tag");
outbound.Remove("detour");
Link link = new Link { Host = host, Port = port, Name = name };
return Finish(type, link, outbound);
}
}
public static class ProxyOutbound
{
public static Dictionary<string, object> Build(ServerCfg server, string mode)
{
Dictionary<string, object> outbound;
if (server.Outbound != null) outbound = J.Parse(J.ToJson(server.Outbound));
else
{
outbound = new Dictionary<string, object>();
outbound["type"] = "vless";
outbound["server"] = server.Server;
outbound["server_port"] = server.Port;
outbound["uuid"] = server.Uuid;
if (!string.IsNullOrEmpty(server.Flow)) outbound["flow"] = server.Flow;
outbound["tls"] = new Dictionary<string, object> {
{ "enabled", true }, { "server_name", server.Sni },
{ "utls", new Dictionary<string, object> { { "enabled", true }, { "fingerprint", server.Fingerprint } } },
{ "reality", new Dictionary<string, object> { { "enabled", true },
{ "public_key", server.PublicKey }, { "short_id", server.ShortId } } }
};
outbound["packet_encoding"] = "xudp";
outbound["domain_strategy"] = "ipv4_only";
}
outbound["tag"] = "proxy-vless"; // shared route tag, independent of protocol
outbound["detour"] = mode == ConfigBuilder.ModeWg ? "proxy-wireguard" : "bound";
return outbound;
}
}
}