Integrate Xray 26.9.9 with the sing-box system tunnel and verify Windows routes during health checks. Improve endpoint selection, network recovery, subscription refresh deadlines, diagnostics, packaging, and regression coverage.
71 lines
3.7 KiB
C#
71 lines
3.7 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace QVPN.Core;
|
|
|
|
public static class Constants
|
|
{
|
|
public const string ServiceName = "QVPNService";
|
|
public const string PipeName = "QVPN.Control.v1";
|
|
public const int ProxyPort = 17891;
|
|
public const int XrayPort = 17892;
|
|
public static string DataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "QVPN");
|
|
public static JsonSerializerOptions Json { get; } = new() { PropertyNameCaseInsensitive = true, WriteIndented = false };
|
|
}
|
|
public record VpnNode(string Id, string Name, string Host, int Port, string Uuid, string ServerName, string PublicKey, string ShortId, string Fingerprint, string Flow);
|
|
public record NodeView(string Id, string Name, int? LatencyMs);
|
|
public sealed class Settings
|
|
{
|
|
public string SubscriptionUrl { get; set; } = "";
|
|
public List<VpnNode> Nodes { get; set; } = [];
|
|
public bool DesiredConnected { get; set; }
|
|
public bool RussianDirect { get; set; } = true;
|
|
public bool AutoFailover { get; set; } = true;
|
|
public string SelectedId { get; set; } = "";
|
|
public string LastWorkingId { get; set; } = "";
|
|
public DateTimeOffset? UpdatedAt { get; set; }
|
|
public string? SubscriptionInfo { get; set; }
|
|
}
|
|
public record Request(string Command, string? Value = null, bool? Enabled = null);
|
|
public record Status(string State, string Message, bool DesiredConnected, string? ActiveId, string? ActiveName, DateTimeOffset? ConnectedSince,
|
|
int Restarts, int? EnginePid, bool RussianDirect, bool AutoFailover, string SelectedId, DateTimeOffset? UpdatedAt, List<NodeView> Nodes, List<string> Events,
|
|
int? XrayPid = null, string? VpnCore = null, bool? SystemRoutesVerified = null);
|
|
public record Response(bool Ok, string? Error, Status? Status);
|
|
|
|
public sealed class SecureStore(string directory)
|
|
{
|
|
private readonly string path = Path.Combine(directory, "settings.dpapi");
|
|
public Settings Load()
|
|
{
|
|
if (!File.Exists(path)) return new();
|
|
return JsonSerializer.Deserialize<Settings>(ProtectedData.Unprotect(File.ReadAllBytes(path), null, DataProtectionScope.LocalMachine), Constants.Json)
|
|
?? throw new InvalidDataException("Повреждены настройки QVPN.");
|
|
}
|
|
public void Save(Settings settings)
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
byte[] plain = JsonSerializer.SerializeToUtf8Bytes(settings, Constants.Json);
|
|
try
|
|
{
|
|
byte[] encrypted = ProtectedData.Protect(plain, null, DataProtectionScope.LocalMachine);
|
|
string temp = path + ".new";
|
|
using (var file = new FileStream(temp, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { file.Write(encrypted); file.Flush(true); }
|
|
if (File.Exists(path)) File.Replace(temp, path, path + ".backup", true); else File.Move(temp, path);
|
|
}
|
|
finally { CryptographicOperations.ZeroMemory(plain); }
|
|
}
|
|
}
|
|
|
|
public static class SafeError
|
|
{
|
|
public static string Describe(Exception e) => e switch
|
|
{
|
|
HttpRequestException => "Сервер подписки недоступен. Сохранённые локации остаются доступны.",
|
|
OperationCanceledException => "Превышено время ожидания. Повторите действие.",
|
|
System.ComponentModel.Win32Exception => "Windows не разрешила запуск VPN. Проверьте установку службы.",
|
|
InvalidDataException or ArgumentException => e.Message,
|
|
_ => "Ошибка QVPN: " + e.GetType().Name + ". Подробности в диагностике."
|
|
};
|
|
}
|