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 Servers = new List(); public readonly List Errors = new List(); } // 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 map = value as Dictionary; if (map != null) { List keys = new List(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 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 tls = J.Obj(outbound.ContainsKey("tls") ? outbound["tls"] : null); Dictionary reality = tls == null ? null : J.Obj(tls.ContainsKey("reality") ? tls["reality"] : null); Dictionary 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 document = root as Dictionary; 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 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 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 Query(string text) { Dictionary values = new Dictionary(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 q, string key, string fallback) { string value; return q.TryGetValue(key, out value) ? value : fallback; } static string First(Dictionary 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 Base(string protocol, Link link) { Dictionary outbound = new Dictionary(); outbound["type"] = protocol; outbound["server"] = link.Host; outbound["server_port"] = link.Port; return outbound; } static ServerCfg Finish(string protocol, Link link, Dictionary 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 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 tls = new Dictionary(); 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 { { "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 { { "enabled", true }, { "public_key", publicKey }, { "short_id", First(link.Query, "sid", "shortId", "") } }; } outbound["tls"] = tls; } static void AddTransport(Dictionary outbound, Dictionary 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 transport = new Dictionary(); 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 { { "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 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 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(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 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 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 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 outbound = Base("hysteria2", link); outbound["password"] = link.User; Dictionary tls = new Dictionary { { "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 { { "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 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 tls = new Dictionary { { "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 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 input) { Dictionary 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 Build(ServerCfg server, string mode) { Dictionary outbound; if (server.Outbound != null) outbound = J.Parse(J.ToJson(server.Outbound)); else { outbound = new Dictionary(); 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 { { "enabled", true }, { "server_name", server.Sni }, { "utls", new Dictionary { { "enabled", true }, { "fingerprint", server.Fingerprint } } }, { "reality", new Dictionary { { "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; } } }