935 lines
43 KiB
C#
935 lines
43 KiB
C#
// SbLauncher — GUI для sing-box.
|
||||
|
|
//
|
|||
|
|
// ПРИНЦИП (изменён по сравнению с прошлой версией):
|
|||
|
|
// Источник правды — profiles.json (серверы, общее) и local.json (ключ,
|
|||
|
|
// адрес, адаптер — своё на каждой машине). Конфиг sing-box НЕ хранится:
|
|||
|
|
// он собирается из шаблона templates\*.json при каждом запуске и кладётся
|
|||
|
|
// в run\config.json. Поэтому папок conf_* больше нет, и править один и
|
|||
|
|
// тот же private_key в пяти файлах не требуется.
|
|||
|
|
//
|
|||
|
|
// ПОРТАТИВНОСТЬ: всё ищется рядом с exe. Скопировали папку — работает.
|
|||
|
|
//
|
|||
|
|
// Компилятор — встроенный в Windows csc.exe (C# 5). Поэтому здесь нет
|
|||
|
|
// интерполяции строк ($""), ?. и expression-bodied членов. См. build.cmd.
|
|||
|
|
|
|||
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Collections.ObjectModel;
|
|||
|
|
using System.Collections.Concurrent;
|
|||
|
|
using System.ComponentModel;
|
|||
|
|
using System.Diagnostics;
|
|||
|
|
using System.Globalization;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Net;
|
|||
|
|
using System.Net.NetworkInformation;
|
|||
|
|
using System.Net.Sockets;
|
|||
|
|
using System.Reflection;
|
|||
|
|
using System.Runtime.CompilerServices;
|
|||
|
|
using System.Runtime.InteropServices;
|
|||
|
|
using System.Text;
|
|||
|
|
using System.Text.RegularExpressions;
|
|||
|
|
using System.Threading;
|
|||
|
|
using System.Threading.Tasks;
|
|||
|
|
using System.Web.Script.Serialization;
|
|||
|
|
using System.Windows;
|
|||
|
|
using System.Windows.Controls;
|
|||
|
|
using System.Windows.Input;
|
|||
|
|
using System.Windows.Markup;
|
|||
|
|
using System.Windows.Media;
|
|||
|
|
using System.Windows.Media.Imaging;
|
|||
|
|
using System.Windows.Threading;
|
|||
|
|
|
|||
|
|
namespace SbLauncher
|
|||
|
|
{
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
// JSON
|
|||
|
|
//
|
|||
|
|
// Свои файлы (profiles.json / local.json) содержат комментарии //,
|
|||
|
|
// поэтому перед разбором их снимаем. JavaScriptSerializer входит в
|
|||
|
|
// .NET Framework (System.Web.Extensions.dll) — внешних библиотек нет.
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
public static class J
|
|||
|
|
{
|
|||
|
|
public static Dictionary<string, object> Parse(string text)
|
|||
|
|
{
|
|||
|
|
JavaScriptSerializer ser = new JavaScriptSerializer();
|
|||
|
|
ser.MaxJsonLength = int.MaxValue;
|
|||
|
|
return ser.DeserializeObject(StripComments(text)) as Dictionary<string, object>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Dictionary<string, object> Obj(object o)
|
|||
|
|
{
|
|||
|
|
return o as Dictionary<string, object>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static List<Dictionary<string, object>> Arr(object o)
|
|||
|
|
{
|
|||
|
|
List<Dictionary<string, object>> list = new List<Dictionary<string, object>>();
|
|||
|
|
object[] a = o as object[];
|
|||
|
|
if (a == null) return list;
|
|||
|
|
foreach (object it in a)
|
|||
|
|
{
|
|||
|
|
Dictionary<string, object> d = it as Dictionary<string, object>;
|
|||
|
|
if (d != null) list.Add(d);
|
|||
|
|
}
|
|||
|
|
return list;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string Str(Dictionary<string, object> d, string key, string def)
|
|||
|
|
{
|
|||
|
|
object v;
|
|||
|
|
if (d == null || !d.TryGetValue(key, out v) || v == null) return def;
|
|||
|
|
return Convert.ToString(v, CultureInfo.InvariantCulture);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static int Int(Dictionary<string, object> d, string key, int def)
|
|||
|
|
{
|
|||
|
|
object v;
|
|||
|
|
if (d == null || !d.TryGetValue(key, out v) || v == null) return def;
|
|||
|
|
try { return Convert.ToInt32(v, CultureInfo.InvariantCulture); }
|
|||
|
|
catch { return def; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Экранирование значения, которое подставляется ВНУТРЬ строки JSON.
|
|||
|
|
// Обязательно: ключ или SNI с кавычкой иначе сломают весь файл.
|
|||
|
|
public static string Esc(string s)
|
|||
|
|
{
|
|||
|
|
if (s == null) return "";
|
|||
|
|
StringBuilder sb = new StringBuilder(s.Length + 8);
|
|||
|
|
foreach (char c in s)
|
|||
|
|
{
|
|||
|
|
switch (c)
|
|||
|
|
{
|
|||
|
|
case '"': sb.Append("\\\""); break;
|
|||
|
|
case '\\': sb.Append("\\\\"); break;
|
|||
|
|
case '\b': sb.Append("\\b"); break;
|
|||
|
|
case '\f': sb.Append("\\f"); break;
|
|||
|
|
case '\n': sb.Append("\\n"); break;
|
|||
|
|
case '\r': sb.Append("\\r"); break;
|
|||
|
|
case '\t': sb.Append("\\t"); break;
|
|||
|
|
default:
|
|||
|
|
if (c < ' ') sb.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
|
|||
|
|
else sb.Append(c);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Снимает // и /* */ с учётом строковых литералов: внутри кавычек
|
|||
|
|
// слэши не трогаем, иначе разъедется "https://..." и base64-ключ.
|
|||
|
|
public static string StripComments(string s)
|
|||
|
|
{
|
|||
|
|
StringBuilder sb = new StringBuilder(s.Length);
|
|||
|
|
bool inStr = false, esc = false;
|
|||
|
|
for (int i = 0; i < s.Length; i++)
|
|||
|
|
{
|
|||
|
|
char c = s[i];
|
|||
|
|
if (inStr)
|
|||
|
|
{
|
|||
|
|
sb.Append(c);
|
|||
|
|
if (esc) esc = false;
|
|||
|
|
else if (c == '\\') esc = true;
|
|||
|
|
else if (c == '"') inStr = false;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (c == '"') { inStr = true; sb.Append(c); continue; }
|
|||
|
|
if (c == '/' && i + 1 < s.Length && s[i + 1] == '/')
|
|||
|
|
{
|
|||
|
|
while (i < s.Length && s[i] != '\n') i++;
|
|||
|
|
if (i < s.Length) sb.Append('\n');
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (c == '/' && i + 1 < s.Length && s[i + 1] == '*')
|
|||
|
|
{
|
|||
|
|
i += 2;
|
|||
|
|
while (i + 1 < s.Length && !(s[i] == '*' && s[i + 1] == '/')) i++;
|
|||
|
|
i++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
sb.Append(c);
|
|||
|
|
}
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string ToJson(object value)
|
|||
|
|
{
|
|||
|
|
JavaScriptSerializer ser = new JavaScriptSerializer();
|
|||
|
|
ser.MaxJsonLength = int.MaxValue;
|
|||
|
|
return ser.Serialize(value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
|||
|
|
|
|||
|
|
public static void Write(string path, string text)
|
|||
|
|
{
|
|||
|
|
string dir = Path.GetDirectoryName(path);
|
|||
|
|
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
|||
|
|
// A failed write must never leave half of a settings/config file.
|
|||
|
|
string temp = path + ".tmp";
|
|||
|
|
File.WriteAllText(temp, text, Utf8NoBom);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (File.Exists(path)) File.Replace(temp, path, null);
|
|||
|
|
else File.Move(temp, path);
|
|||
|
|
}
|
|||
|
|
finally { if (File.Exists(temp)) File.Delete(temp); }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
// ПУТИ. Всё относительно папки с exe — это и есть портативность.
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
public class Paths
|
|||
|
|
{
|
|||
|
|
public string Root;
|
|||
|
|
|
|||
|
|
public Paths(string root) { Root = root; }
|
|||
|
|
|
|||
|
|
public string Profiles { get { return Path.Combine(Root, "profiles.json"); } }
|
|||
|
|
public string Local { get { return Path.Combine(Root, "local.json"); } }
|
|||
|
|
public string TemplatesDir { get { return Path.Combine(Root, "templates"); } }
|
|||
|
|
public string SbExe { get { return Path.Combine(Root, Path.Combine("bin", "sb.exe")); } }
|
|||
|
|
public string RuleSet { get { return Path.Combine(Root, Path.Combine("doc", "doc.srs")); } }
|
|||
|
|
public string RunDir { get { return Path.Combine(Root, "run"); } }
|
|||
|
|
public string RunConfig { get { return Path.Combine(RunDir, "config.json"); } }
|
|||
|
|
|
|||
|
|
// sing-box на Windows понимает прямые слэши, а обратные пришлось бы
|
|||
|
|
// экранировать в JSON. Поэтому путь к rule-set отдаём через "/".
|
|||
|
|
public string RuleSetJson { get { return RuleSet.Replace('\\', '/'); } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
// МОДЕЛЬ
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
public class ServerCfg
|
|||
|
|
{
|
|||
|
|
public string Code = "";
|
|||
|
|
public string Name = "";
|
|||
|
|
public string Server = "";
|
|||
|
|
public int Port = 443;
|
|||
|
|
public string Uuid = "";
|
|||
|
|
public string Sni = "";
|
|||
|
|
public string Fingerprint = "chrome";
|
|||
|
|
public string PublicKey = "";
|
|||
|
|
public string ShortId = "";
|
|||
|
|
public string Flow = ""; // xtls-rprx-vision или пусто (без flow)
|
|||
|
|
public string SpiderX = ""; // старые профили сохраняют это поле
|
|||
|
|
public string Protocol = "vless";
|
|||
|
|
public Dictionary<string, object> Outbound; // точная конфигурация импортированного ключа
|
|||
|
|
|
|||
|
|
public string Endpoint
|
|||
|
|
{
|
|||
|
|
get { return Server + ":" + Port.ToString(CultureInfo.InvariantCulture); }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public class WgCfg
|
|||
|
|
{
|
|||
|
|
public string Server = "";
|
|||
|
|
public int Port = 51820;
|
|||
|
|
public string PeerPublicKey = "";
|
|||
|
|
public int Mtu = 1280;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// profiles.json — локальная копия серверов; в тестовом репозитории не коммитится
|
|||
|
|
public class Profiles
|
|||
|
|
{
|
|||
|
|
public WgCfg Wg = new WgCfg();
|
|||
|
|
public List<ServerCfg> Servers = new List<ServerCfg>();
|
|||
|
|
|
|||
|
|
public static Profiles Load(string path)
|
|||
|
|
{
|
|||
|
|
Profiles p = new Profiles();
|
|||
|
|
if (!File.Exists(path)) return p;
|
|||
|
|
|
|||
|
|
Dictionary<string, object> root = J.Parse(File.ReadAllText(path, Encoding.UTF8));
|
|||
|
|
if (root == null) return p;
|
|||
|
|
|
|||
|
|
object wo;
|
|||
|
|
if (root.TryGetValue("wireguard", out wo))
|
|||
|
|
{
|
|||
|
|
Dictionary<string, object> w = J.Obj(wo);
|
|||
|
|
if (w != null)
|
|||
|
|
{
|
|||
|
|
p.Wg.Server = J.Str(w, "server", "");
|
|||
|
|
p.Wg.Port = J.Int(w, "port", 51820);
|
|||
|
|
p.Wg.PeerPublicKey = J.Str(w, "peer_public_key", "");
|
|||
|
|
p.Wg.Mtu = J.Int(w, "mtu", 1280);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
object so;
|
|||
|
|
if (root.TryGetValue("servers", out so))
|
|||
|
|
foreach (Dictionary<string, object> d in J.Arr(so))
|
|||
|
|
{
|
|||
|
|
ServerCfg s = new ServerCfg();
|
|||
|
|
s.Code = J.Str(d, "code", "");
|
|||
|
|
s.Name = J.Str(d, "name", s.Code);
|
|||
|
|
s.Server = J.Str(d, "server", "");
|
|||
|
|
s.Port = J.Int(d, "port", 443);
|
|||
|
|
s.Uuid = J.Str(d, "uuid", "");
|
|||
|
|
s.Sni = J.Str(d, "sni", "");
|
|||
|
|
s.Fingerprint = J.Str(d, "fingerprint", "chrome");
|
|||
|
|
s.PublicKey = J.Str(d, "public_key", "");
|
|||
|
|
s.ShortId = J.Str(d, "short_id", "");
|
|||
|
|
s.Flow = J.Str(d, "flow", "xtls-rprx-vision");
|
|||
|
|
s.SpiderX = J.Str(d, "spider_x", "");
|
|||
|
|
s.Protocol = J.Str(d, "protocol", "vless");
|
|||
|
|
object outbound;
|
|||
|
|
if (d.TryGetValue("outbound", out outbound)) s.Outbound = J.Obj(outbound);
|
|||
|
|
if (s.Outbound != null) s.Protocol = J.Str(s.Outbound, "type", s.Protocol);
|
|||
|
|
if (s.Server.Length > 0) p.Servers.Add(s);
|
|||
|
|
}
|
|||
|
|
return p;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Пишется только когда сервер добавили/удалили из GUI. Файл
|
|||
|
|
// пересобирается целиком, поэтому свои комментарии ВНУТРИ списка
|
|||
|
|
// серверов при этом теряются — шапка восстанавливается.
|
|||
|
|
public void Save(string path)
|
|||
|
|
{
|
|||
|
|
StringBuilder sb = new StringBuilder();
|
|||
|
|
sb.AppendLine("// Серверы и общие параметры этого тестового экземпляра.");
|
|||
|
|
sb.AppendLine("// Храните данные серверов локально, а ключ WireGuard — в local.json.");
|
|||
|
|
sb.AppendLine("{");
|
|||
|
|
sb.AppendLine(" \"wireguard\": {");
|
|||
|
|
sb.AppendLine(" \"server\": \"" + J.Esc(Wg.Server) + "\",");
|
|||
|
|
sb.AppendLine(" \"port\": " + Wg.Port.ToString(CultureInfo.InvariantCulture) + ",");
|
|||
|
|
sb.AppendLine(" \"peer_public_key\": \"" + J.Esc(Wg.PeerPublicKey) + "\",");
|
|||
|
|
sb.AppendLine(" \"mtu\": " + Wg.Mtu.ToString(CultureInfo.InvariantCulture));
|
|||
|
|
sb.AppendLine(" },");
|
|||
|
|
sb.AppendLine(" \"servers\": [");
|
|||
|
|
for (int i = 0; i < Servers.Count; i++)
|
|||
|
|
{
|
|||
|
|
ServerCfg s = Servers[i];
|
|||
|
|
sb.AppendLine(" {");
|
|||
|
|
sb.AppendLine(" \"code\": \"" + J.Esc(s.Code) + "\",");
|
|||
|
|
sb.AppendLine(" \"name\": \"" + J.Esc(s.Name) + "\",");
|
|||
|
|
sb.AppendLine(" \"server\": \"" + J.Esc(s.Server) + "\",");
|
|||
|
|
sb.AppendLine(" \"port\": " + s.Port.ToString(CultureInfo.InvariantCulture) + ",");
|
|||
|
|
sb.AppendLine(" \"uuid\": \"" + J.Esc(s.Uuid) + "\",");
|
|||
|
|
sb.AppendLine(" \"sni\": \"" + J.Esc(s.Sni) + "\",");
|
|||
|
|
sb.AppendLine(" \"fingerprint\": \"" + J.Esc(s.Fingerprint) + "\",");
|
|||
|
|
sb.AppendLine(" \"public_key\": \"" + J.Esc(s.PublicKey) + "\",");
|
|||
|
|
// flow пишем ВСЕГДА (даже пустой), чтобы отличать «flow не задан»
|
|||
|
|
// от «flow по умолчанию» при загрузке (обратная совместимость)
|
|||
|
|
sb.AppendLine(" \"flow\": \"" + J.Esc(s.Flow) + "\",");
|
|||
|
|
// spider_x пишем только когда задан
|
|||
|
|
if (!string.IsNullOrEmpty(s.SpiderX))
|
|||
|
|
sb.AppendLine(" \"spider_x\": \"" + J.Esc(s.SpiderX) + "\",");
|
|||
|
|
sb.AppendLine(" \"short_id\": \"" + J.Esc(s.ShortId) + "\",");
|
|||
|
|
sb.AppendLine(" \"protocol\": \"" + J.Esc(s.Protocol) + "\"" + (s.Outbound != null ? "," : ""));
|
|||
|
|
if (s.Outbound != null)
|
|||
|
|
sb.AppendLine(" \"outbound\": " + J.ToJson(s.Outbound));
|
|||
|
|
sb.AppendLine(i == Servers.Count - 1 ? " }" : " },");
|
|||
|
|
}
|
|||
|
|
sb.AppendLine(" ]");
|
|||
|
|
sb.AppendLine("}");
|
|||
|
|
J.Write(path, sb.ToString());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// local.json — параметры ЭТОЙ машины, в .gitignore
|
|||
|
|
public class LocalCfg
|
|||
|
|
{
|
|||
|
|
public string PrivateKey = "";
|
|||
|
|
public string LocalAddress = "";
|
|||
|
|
public string Adapter = "";
|
|||
|
|
public string Theme = ""; // пусто -> берём тему Windows
|
|||
|
|
public string Mode = "wg"; // wg | direct | wgonly
|
|||
|
|
public string LastServer = "";
|
|||
|
|
|
|||
|
|
public static LocalCfg Load(string path)
|
|||
|
|
{
|
|||
|
|
LocalCfg c = new LocalCfg();
|
|||
|
|
if (!File.Exists(path)) return c;
|
|||
|
|
Dictionary<string, object> d = J.Parse(File.ReadAllText(path, Encoding.UTF8));
|
|||
|
|
if (d == null) return c;
|
|||
|
|
c.PrivateKey = J.Str(d, "private_key", "");
|
|||
|
|
c.LocalAddress = J.Str(d, "local_address", "");
|
|||
|
|
c.Adapter = J.Str(d, "adapter", "");
|
|||
|
|
c.Theme = J.Str(d, "theme", "");
|
|||
|
|
c.Mode = J.Str(d, "mode", "wg");
|
|||
|
|
c.LastServer = J.Str(d, "last_server", "");
|
|||
|
|
return c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Save(string path)
|
|||
|
|
{
|
|||
|
|
StringBuilder sb = new StringBuilder();
|
|||
|
|
sb.AppendLine("// Параметры ЭТОЙ машины. В .gitignore — здесь приватный ключ.");
|
|||
|
|
sb.AppendLine("// Правится в GUI, руками лезть сюда не нужно.");
|
|||
|
|
sb.AppendLine("{");
|
|||
|
|
sb.AppendLine(" \"private_key\": \"" + J.Esc(PrivateKey) + "\",");
|
|||
|
|
sb.AppendLine(" \"local_address\": \"" + J.Esc(LocalAddress) + "\",");
|
|||
|
|
sb.AppendLine(" \"adapter\": \"" + J.Esc(Adapter) + "\",");
|
|||
|
|
sb.AppendLine(" \"theme\": \"" + J.Esc(Theme) + "\",");
|
|||
|
|
sb.AppendLine(" \"mode\": \"" + J.Esc(Mode) + "\",");
|
|||
|
|
sb.AppendLine(" \"last_server\": \"" + J.Esc(LastServer) + "\"");
|
|||
|
|
sb.AppendLine("}");
|
|||
|
|
J.Write(path, sb.ToString());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
// СЕТЕВЫЕ АДАПТЕРЫ
|
|||
|
|
//
|
|||
|
|
// bind_interface в sing-box хочет ИМЯ подключения из «Сетевых
|
|||
|
|
// подключений», а не описание устройства. У Windows по умолчанию это
|
|||
|
|
// «Wi-Fi» с дефисом, но адаптер часто переименован — из-за этого
|
|||
|
|
// конфиг с зашитым "WiFi" на другом ПК молча не работал.
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
public class AdapterInfo
|
|||
|
|
{
|
|||
|
|
public string Name = "";
|
|||
|
|
public string Description = "";
|
|||
|
|
public bool IsUp;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static class Adapters
|
|||
|
|
{
|
|||
|
|
public static List<AdapterInfo> List()
|
|||
|
|
{
|
|||
|
|
List<AdapterInfo> list = new List<AdapterInfo>();
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces())
|
|||
|
|
{
|
|||
|
|
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
|
|||
|
|
if (ni.NetworkInterfaceType == NetworkInterfaceType.Tunnel) continue;
|
|||
|
|
// свой же TUN: привязываться к нему бессмысленно
|
|||
|
|
if (ni.Name == "sing-box-tun") continue;
|
|||
|
|
|
|||
|
|
AdapterInfo a = new AdapterInfo();
|
|||
|
|
a.Name = ni.Name;
|
|||
|
|
a.Description = ni.Description;
|
|||
|
|
a.IsUp = ni.OperationalStatus == OperationalStatus.Up;
|
|||
|
|
list.Add(a);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
// сначала живые: обычно нужен именно тот, через который есть сеть
|
|||
|
|
list.Sort(delegate(AdapterInfo x, AdapterInfo y)
|
|||
|
|
{
|
|||
|
|
if (x.IsUp != y.IsUp) return x.IsUp ? -1 : 1;
|
|||
|
|
return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
});
|
|||
|
|
return list;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static AdapterInfo Find(string name)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(name)) return null;
|
|||
|
|
foreach (AdapterInfo a in List())
|
|||
|
|
if (string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)) return a;
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
// СБОРКА КОНФИГА ИЗ ШАБЛОНА
|
|||
|
|
//
|
|||
|
|
// Числовые токены записаны в шаблоне в кавычках ("@@WG_PORT@@"), чтобы
|
|||
|
|
// шаблон оставался валидным JSON и его можно было открыть в редакторе.
|
|||
|
|
// При подстановке кавычки снимаются вместе с токеном.
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
public class BuildResult
|
|||
|
|
{
|
|||
|
|
public bool Ok;
|
|||
|
|
public string Error;
|
|||
|
|
public string ConfigPath;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static class ConfigBuilder
|
|||
|
|
{
|
|||
|
|
public const string ModeWg = "wg";
|
|||
|
|
public const string ModeDirect = "direct";
|
|||
|
|
public const string ModeWgOnly = "wgonly";
|
|||
|
|
|
|||
|
|
public static bool UsesWireguard(string mode)
|
|||
|
|
{
|
|||
|
|
return mode == ModeWg || mode == ModeWgOnly;
|
|||
|
|
}
|
|||
|
|
public static bool UsesProxy(string mode)
|
|||
|
|
{
|
|||
|
|
return mode == ModeWg || mode == ModeDirect;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string ModeTitle(string mode)
|
|||
|
|
{
|
|||
|
|
if (mode == ModeDirect) return "напрямую через адаптер";
|
|||
|
|
if (mode == ModeWgOnly) return "только WireGuard";
|
|||
|
|
return "через WireGuard";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Файл templates\<mode>.json рядом с exe перекрывает встроенный —
|
|||
|
|
// тот же приём, что с Ui.xaml: правка шаблона без пересборки.
|
|||
|
|
public static string LoadTemplate(Paths paths, string mode)
|
|||
|
|
{
|
|||
|
|
string ext = Path.Combine(paths.TemplatesDir, mode + ".json");
|
|||
|
|
if (File.Exists(ext)) return File.ReadAllText(ext, Encoding.UTF8);
|
|||
|
|
|
|||
|
|
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream(mode + ".json"))
|
|||
|
|
{
|
|||
|
|
if (s == null) throw new FileNotFoundException("нет шаблона " + mode + ".json (ни рядом с exe, ни внутри)");
|
|||
|
|
using (StreamReader r = new StreamReader(s, Encoding.UTF8))
|
|||
|
|
return r.ReadToEnd();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// WireGuard-ключ — 32 байта в base64. Проверка ловит обрезанную
|
|||
|
|
// вставку из буфера, которую иначе увидел бы только sing-box.
|
|||
|
|
public static bool IsWgKey(string s)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(s)) return false;
|
|||
|
|
try { return Convert.FromBase64String(s.Trim()).Length == 32; }
|
|||
|
|
catch { return false; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static bool IsCidr(string s)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(s)) return false;
|
|||
|
|
string[] parts = s.Trim().Split('/');
|
|||
|
|
if (parts.Length != 2) return false;
|
|||
|
|
IPAddress ip;
|
|||
|
|
if (!IPAddress.TryParse(parts[0], out ip)) return false;
|
|||
|
|
int bits;
|
|||
|
|
if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out bits)) return false;
|
|||
|
|
int max = ip.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
|
|||
|
|
return bits >= 0 && bits <= max;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static List<string> Validate(Paths paths, Profiles pr, LocalCfg lc, ServerCfg srv, string mode)
|
|||
|
|
{
|
|||
|
|
List<string> bad = new List<string>();
|
|||
|
|
|
|||
|
|
if (UsesWireguard(mode))
|
|||
|
|
{
|
|||
|
|
if (!IsWgKey(lc.PrivateKey))
|
|||
|
|
bad.Add("приватный ключ WireGuard пуст или не 32 байта base64");
|
|||
|
|
if (!IsCidr(lc.LocalAddress))
|
|||
|
|
bad.Add("локальный IP должен быть вида 10.10.0.19/24");
|
|||
|
|
if (string.IsNullOrEmpty(pr.Wg.Server))
|
|||
|
|
bad.Add("не задан адрес WireGuard-сервера в profiles.json");
|
|||
|
|
if (!IsWgKey(pr.Wg.PeerPublicKey))
|
|||
|
|
bad.Add("peer_public_key в profiles.json пуст или не 32 байта base64");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (mode == ModeDirect)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(lc.Adapter))
|
|||
|
|
bad.Add("не выбран сетевой адаптер");
|
|||
|
|
else if (Adapters.Find(lc.Adapter) == null)
|
|||
|
|
bad.Add("адаптера \"" + lc.Adapter + "\" нет в системе");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (UsesProxy(mode))
|
|||
|
|
{
|
|||
|
|
if (srv == null) bad.Add("не выбран сервер");
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(srv.Server)) bad.Add("у сервера не задан адрес");
|
|||
|
|
if (srv.Port < 1 || srv.Port > 65535) bad.Add("у сервера некорректный порт");
|
|||
|
|
if (srv.Outbound == null && string.IsNullOrEmpty(srv.Uuid))
|
|||
|
|
bad.Add("у сервера не задан uuid");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!File.Exists(paths.RuleSet))
|
|||
|
|
bad.Add("не найден " + paths.RuleSet + " (geoip-ru)");
|
|||
|
|
if (!File.Exists(paths.SbExe))
|
|||
|
|
bad.Add("не найден " + paths.SbExe);
|
|||
|
|
|
|||
|
|
return bad;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string Render(string tpl,
|
|||
|
|
Dictionary<string, string> strings,
|
|||
|
|
Dictionary<string, int> numbers)
|
|||
|
|
{
|
|||
|
|
// числовые — первыми: они забирают кавычки вокруг токена
|
|||
|
|
foreach (KeyValuePair<string, int> kv in numbers)
|
|||
|
|
tpl = tpl.Replace("\"@@" + kv.Key + "@@\"",
|
|||
|
|
kv.Value.ToString(CultureInfo.InvariantCulture));
|
|||
|
|
|
|||
|
|
foreach (KeyValuePair<string, string> kv in strings)
|
|||
|
|
tpl = tpl.Replace("@@" + kv.Key + "@@", J.Esc(kv.Value));
|
|||
|
|
|
|||
|
|
// Незакрытый токен — это опечатка в шаблоне. Молча отдать такой
|
|||
|
|
// конфиг sing-box'у нельзя: ошибка будет невнятной.
|
|||
|
|
Match m = Regex.Match(tpl, "@@[A-Za-z0-9_]+@@");
|
|||
|
|
if (m.Success)
|
|||
|
|
throw new InvalidOperationException("в шаблоне остался неподставленный токен " + m.Value);
|
|||
|
|
|
|||
|
|
return tpl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static BuildResult Build(Paths paths, Profiles pr, LocalCfg lc, ServerCfg srv, string mode)
|
|||
|
|
{
|
|||
|
|
return Build(paths, pr, lc, srv, mode, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// outPath задаётся только в --selftest: там нужны все три режима
|
|||
|
|
// рядом, а не один перезаписанный run\config.json.
|
|||
|
|
public static BuildResult Build(Paths paths, Profiles pr, LocalCfg lc, ServerCfg srv,
|
|||
|
|
string mode, string outPath)
|
|||
|
|
{
|
|||
|
|
BuildResult r = new BuildResult();
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
List<string> bad = Validate(paths, pr, lc, srv, mode);
|
|||
|
|
if (bad.Count > 0)
|
|||
|
|
{
|
|||
|
|
r.Error = string.Join("; ", bad.ToArray());
|
|||
|
|
return r;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Dictionary<string, string> s = new Dictionary<string, string>();
|
|||
|
|
Dictionary<string, int> n = new Dictionary<string, int>();
|
|||
|
|
|
|||
|
|
s["RULESET_PATH"] = paths.RuleSetJson;
|
|||
|
|
|
|||
|
|
if (UsesWireguard(mode))
|
|||
|
|
{
|
|||
|
|
s["WG_SERVER"] = pr.Wg.Server;
|
|||
|
|
s["WG_LOCAL_ADDRESS"] = lc.LocalAddress.Trim();
|
|||
|
|
s["WG_PRIVATE_KEY"] = lc.PrivateKey.Trim();
|
|||
|
|
s["WG_PEER_PUBLIC_KEY"] = pr.Wg.PeerPublicKey.Trim();
|
|||
|
|
n["WG_PORT"] = pr.Wg.Port;
|
|||
|
|
n["WG_MTU"] = pr.Wg.Mtu;
|
|||
|
|
}
|
|||
|
|
if (mode == ModeDirect)
|
|||
|
|
s["ADAPTER"] = lc.Adapter;
|
|||
|
|
|
|||
|
|
string tpl = LoadTemplate(paths, mode);
|
|||
|
|
if (UsesProxy(mode))
|
|||
|
|
{
|
|||
|
|
const string marker = "\"@@PROXY_OUTBOUND@@\"";
|
|||
|
|
if (!tpl.Contains(marker))
|
|||
|
|
throw new InvalidOperationException("шаблон " + mode + ".json устарел: нет @@PROXY_OUTBOUND@@");
|
|||
|
|
tpl = tpl.Replace(marker, J.ToJson(ProxyOutbound.Build(srv, mode)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
string body = Render(tpl, s, n);
|
|||
|
|
|
|||
|
|
string head =
|
|||
|
|
"// СГЕНЕРИРОВАНО SbLauncher — правки здесь затрутся." + Environment.NewLine +
|
|||
|
|
"// Режим: " + mode + " (" + ModeTitle(mode) + ")" +
|
|||
|
|
(UsesProxy(mode) ? ", сервер: " + srv.Code + " " + srv.Endpoint : "") +
|
|||
|
|
Environment.NewLine +
|
|||
|
|
"// Правьте templates\\" + mode + ".json, profiles.json или local.json." +
|
|||
|
|
Environment.NewLine;
|
|||
|
|
|
|||
|
|
string dest = string.IsNullOrEmpty(outPath) ? paths.RunConfig : outPath;
|
|||
|
|
J.Write(dest, head + body);
|
|||
|
|
r.Ok = true;
|
|||
|
|
r.ConfigPath = dest;
|
|||
|
|
return r;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
r.Error = ex.Message;
|
|||
|
|
return r;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Старое имя остаётся для внешних сценариев и selftest.
|
|||
|
|
public static class VlessLink
|
|||
|
|
{
|
|||
|
|
public static ServerCfg Parse(string link)
|
|||
|
|
{
|
|||
|
|
ServerCfg server;
|
|||
|
|
string error;
|
|||
|
|
return KeyImporter.TryParseOne(link, out server, out error) &&
|
|||
|
|
server.Protocol == "vless" ? server : null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
// ЗАПУСК sing-box + Job Object
|
|||
|
|
//
|
|||
|
|
// ВАЖНО: при auto_route/strict_route осиротевший sing-box оставляет
|
|||
|
|
// систему со сломанным маршрутом по умолчанию. Job Object гарантирует,
|
|||
|
|
// что дочерний процесс умрёт вместе с лаунчером — даже если тот убит
|
|||
|
|
// через Диспетчер задач.
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
public class SingBoxRunner
|
|||
|
|
{
|
|||
|
|
public event Action<string> Log;
|
|||
|
|
public event Action<bool> StateChanged;
|
|||
|
|
|
|||
|
|
Process _proc;
|
|||
|
|
IntPtr _job = IntPtr.Zero;
|
|||
|
|
|
|||
|
|
public bool IsRunning
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
Process p = _proc;
|
|||
|
|
if (p == null) return false;
|
|||
|
|
try { return !p.HasExited; }
|
|||
|
|
catch (InvalidOperationException) { return false; }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void ApplyEnv(ProcessStartInfo psi)
|
|||
|
|
{
|
|||
|
|
// те же переменные, что были в startN.bat: конфиги пользуются
|
|||
|
|
// wireguard-outbound'ом, который в sing-box 1.12 объявлен
|
|||
|
|
// устаревшим и без этого флага не поднимается
|
|||
|
|
psi.EnvironmentVariables["ENABLE_DEPRECATED_WIREGUARD_OUTBOUND"] = "true";
|
|||
|
|
psi.EnvironmentVariables["ENABLE_DEPRECATED_TUN_ADDRESS_X"] = "true";
|
|||
|
|
psi.EnvironmentVariables["ENABLE_DEPRECATED_SPECIAL_OUTBOUNDS"] = "true";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// sb.exe check — синхронно, до запуска. Ошибку конфига лучше увидеть
|
|||
|
|
// в логе лаунчера, чем ловить её сломанными маршрутами.
|
|||
|
|
public static bool Check(string sbExe, string cfgPath, string workDir, out string output)
|
|||
|
|
{
|
|||
|
|
output = "";
|
|||
|
|
StringBuilder sb = new StringBuilder();
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
ProcessStartInfo psi = new ProcessStartInfo();
|
|||
|
|
psi.FileName = sbExe;
|
|||
|
|
psi.Arguments = "check -c \"" + cfgPath + "\"";
|
|||
|
|
psi.WorkingDirectory = workDir;
|
|||
|
|
psi.UseShellExecute = false;
|
|||
|
|
psi.CreateNoWindow = true;
|
|||
|
|
psi.RedirectStandardOutput = true;
|
|||
|
|
psi.RedirectStandardError = true;
|
|||
|
|
psi.StandardOutputEncoding = Encoding.UTF8;
|
|||
|
|
psi.StandardErrorEncoding = Encoding.UTF8;
|
|||
|
|
ApplyEnv(psi);
|
|||
|
|
|
|||
|
|
using (Process p = new Process())
|
|||
|
|
{
|
|||
|
|
p.StartInfo = psi;
|
|||
|
|
DataReceivedEventHandler grab = delegate(object s, DataReceivedEventArgs e)
|
|||
|
|
{
|
|||
|
|
if (e.Data != null) lock (sb) sb.AppendLine(e.Data);
|
|||
|
|
};
|
|||
|
|
p.OutputDataReceived += grab;
|
|||
|
|
p.ErrorDataReceived += grab;
|
|||
|
|
p.Start();
|
|||
|
|
p.BeginOutputReadLine();
|
|||
|
|
p.BeginErrorReadLine();
|
|||
|
|
if (!p.WaitForExit(10000))
|
|||
|
|
{
|
|||
|
|
try { p.Kill(); } catch { }
|
|||
|
|
output = "sb.exe check не ответил за 10 секунд";
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
p.WaitForExit(); // finish asynchronous stdout/stderr callbacks
|
|||
|
|
lock (sb) output = sb.ToString().Trim();
|
|||
|
|
return p.ExitCode == 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
output = ex.Message;
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Start(string sbExe, string cfgPath, string workDir)
|
|||
|
|
{
|
|||
|
|
if (IsRunning) return;
|
|||
|
|
EnsureJob();
|
|||
|
|
|
|||
|
|
ProcessStartInfo psi = new ProcessStartInfo();
|
|||
|
|
psi.FileName = sbExe;
|
|||
|
|
psi.Arguments = "run -c \"" + cfgPath + "\"";
|
|||
|
|
psi.WorkingDirectory = workDir;
|
|||
|
|
psi.UseShellExecute = false;
|
|||
|
|
psi.CreateNoWindow = true;
|
|||
|
|
psi.RedirectStandardOutput = true;
|
|||
|
|
psi.RedirectStandardError = true;
|
|||
|
|
psi.StandardOutputEncoding = Encoding.UTF8;
|
|||
|
|
psi.StandardErrorEncoding = Encoding.UTF8;
|
|||
|
|
ApplyEnv(psi);
|
|||
|
|
|
|||
|
|
Process p = new Process();
|
|||
|
|
p.StartInfo = psi;
|
|||
|
|
p.EnableRaisingEvents = true;
|
|||
|
|
p.OutputDataReceived += OnOut;
|
|||
|
|
p.ErrorDataReceived += OnOut;
|
|||
|
|
p.Exited += delegate
|
|||
|
|
{
|
|||
|
|
int code;
|
|||
|
|
string result = "?";
|
|||
|
|
try { code = p.ExitCode; result = code.ToString(CultureInfo.InvariantCulture); }
|
|||
|
|
catch { }
|
|||
|
|
Emit("— sing-box завершился (код " + result + ")");
|
|||
|
|
if (object.ReferenceEquals(_proc, p) && StateChanged != null) StateChanged(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
Emit("> " + psi.FileName + " " + psi.Arguments);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
p.Start();
|
|||
|
|
if (!AssignProcessToJobObject(_job, p.Handle))
|
|||
|
|
{
|
|||
|
|
int error = Marshal.GetLastWin32Error();
|
|||
|
|
try { p.Kill(); } catch { }
|
|||
|
|
throw new System.ComponentModel.Win32Exception(error, "Не удалось привязать sing-box к Job Object");
|
|||
|
|
}
|
|||
|
|
_proc = p;
|
|||
|
|
p.BeginOutputReadLine();
|
|||
|
|
p.BeginErrorReadLine();
|
|||
|
|
if (StateChanged != null) StateChanged(true);
|
|||
|
|
}
|
|||
|
|
catch
|
|||
|
|
{
|
|||
|
|
if (object.ReferenceEquals(_proc, p)) _proc = null;
|
|||
|
|
try { if (!p.HasExited) p.Kill(); } catch { }
|
|||
|
|
p.Dispose();
|
|||
|
|
throw;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Stop()
|
|||
|
|
{
|
|||
|
|
Process p = _proc;
|
|||
|
|
if (p == null) return;
|
|||
|
|
bool running;
|
|||
|
|
try { running = !p.HasExited; }
|
|||
|
|
catch { return; }
|
|||
|
|
if (!running) return;
|
|||
|
|
Emit("— останавливаю sing-box…");
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
p.CloseMainWindow();
|
|||
|
|
if (!p.WaitForExit(2500)) p.Kill();
|
|||
|
|
p.WaitForExit(3000);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { Emit("! " + ex.Message); }
|
|||
|
|
if (object.ReferenceEquals(_proc, p) && StateChanged != null) StateChanged(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void OnOut(object s, DataReceivedEventArgs e)
|
|||
|
|
{
|
|||
|
|
if (e.Data != null) Emit(e.Data);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Emit(string s)
|
|||
|
|
{
|
|||
|
|
if (Log != null) Log(s);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Job Object ───────────────────────────────────────────────────
|
|||
|
|
void EnsureJob()
|
|||
|
|
{
|
|||
|
|
if (_job != IntPtr.Zero) return;
|
|||
|
|
_job = CreateJobObject(IntPtr.Zero, null);
|
|||
|
|
if (_job == IntPtr.Zero)
|
|||
|
|
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Не удалось создать Job Object");
|
|||
|
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
|
|||
|
|
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|||
|
|
int len = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
|
|||
|
|
IntPtr ptr = Marshal.AllocHGlobal(len);
|
|||
|
|
Marshal.StructureToPtr(info, ptr, false);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (!SetInformationJobObject(_job, JobObjectExtendedLimitInformation, ptr, (uint)len))
|
|||
|
|
{
|
|||
|
|
int error = Marshal.GetLastWin32Error();
|
|||
|
|
CloseHandle(_job);
|
|||
|
|
_job = IntPtr.Zero;
|
|||
|
|
throw new System.ComponentModel.Win32Exception(error, "Не удалось настроить Job Object");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
finally { Marshal.FreeHGlobal(ptr); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const int JobObjectExtendedLimitInformation = 9;
|
|||
|
|
const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000;
|
|||
|
|
|
|||
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|||
|
|
static extern IntPtr CreateJobObject(IntPtr a, string lpName);
|
|||
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|||
|
|
static extern bool SetInformationJobObject(IntPtr hJob, int infoType, IntPtr info, uint cb);
|
|||
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|||
|
|
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|||
|
|
[DllImport("kernel32.dll")]
|
|||
|
|
static extern bool CloseHandle(IntPtr handle);
|
|||
|
|
|
|||
|
|
[StructLayout(LayoutKind.Sequential)]
|
|||
|
|
struct IO_COUNTERS
|
|||
|
|
{
|
|||
|
|
public ulong ReadOperationCount, WriteOperationCount, OtherOperationCount;
|
|||
|
|
public ulong ReadTransferCount, WriteTransferCount, OtherTransferCount;
|
|||
|
|
}
|
|||
|
|
[StructLayout(LayoutKind.Sequential)]
|
|||
|
|
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
|||
|
|
{
|
|||
|
|
public long PerProcessUserTimeLimit, PerJobUserTimeLimit;
|
|||
|
|
public uint LimitFlags;
|
|||
|
|
public UIntPtr MinimumWorkingSetSize, MaximumWorkingSetSize;
|
|||
|
|
public uint ActiveProcessLimit;
|
|||
|
|
public UIntPtr Affinity;
|
|||
|
|
public uint PriorityClass, SchedulingClass;
|
|||
|
|
}
|
|||
|
|
[StructLayout(LayoutKind.Sequential)]
|
|||
|
|
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|||
|
|
{
|
|||
|
|
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
|||
|
|
public IO_COUNTERS IoInfo;
|
|||
|
|
public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
// ПИНГ
|
|||
|
|
//
|
|||
|
|
// ICMP не годится: сервер 132.243.202.111 на echo-request не отвечает
|
|||
|
|
// (проверено). Меряем время TCP-коннекта до server:port — это и ближе
|
|||
|
|
// к реальности, и работает всегда, пока порт открыт.
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
public static class Pinger
|
|||
|
|
{
|
|||
|
|
public static async Task<int> TcpLatencyAsync(string host, int port, int timeoutMs)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(host)) return -1;
|
|||
|
|
Stopwatch sw = Stopwatch.StartNew();
|
|||
|
|
TcpClient c = new TcpClient();
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
Task connect = c.ConnectAsync(host, port);
|
|||
|
|
Task done = await Task.WhenAny(connect, Task.Delay(timeoutMs));
|
|||
|
|
if (done != connect || !c.Connected) return -1;
|
|||
|
|
sw.Stop();
|
|||
|
|
return (int)sw.ElapsedMilliseconds;
|
|||
|
|
}
|
|||
|
|
catch { return -1; }
|
|||
|
|
finally { try { c.Close(); } catch { } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Внешний IP — уходит через TUN, то есть показывает реальную точку выхода
|
|||
|
|
public static async Task<string> ExitIpAsync()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
using (WebClient w = new WebClient())
|
|||
|
|
{
|
|||
|
|
w.Headers.Add("User-Agent", "sb-launcher");
|
|||
|
|
Task<string> fetch = w.DownloadStringTaskAsync("https://api4.ipify.org");
|
|||
|
|
if (await Task.WhenAny(fetch, Task.Delay(5000)) != fetch)
|
|||
|
|
{
|
|||
|
|
w.CancelAsync();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return (await fetch).Trim();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch { return null; }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
// ИНФРАСТРУКТУРА ПРИВЯЗОК
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════
|
|||
|
|
}
|