Files
Курнат Андрей aae6dd28dd Release QVPN 1.0.5 with Xray and resilient tunnel recovery
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.
2026-10-30 06:15:08 +03:00

110 lines
7.1 KiB
C#

using System.Diagnostics;
using System.Text.Json.Nodes;
using QVPN.Core;
public static class XrayConfigTests
{
public static async Task RunAsync(Func<string, Action, Task> test)
{
await test("Xray accepts only local SOCKS traffic and has no direct fallback", () =>
{
var config = XrayConfigBuilder.Build(Fixture());
var inbound = config["inbounds"]!.AsArray().Single()!;
Check(inbound["protocol"]!.GetValue<string>() == "socks" && inbound["listen"]!.GetValue<string>() == "127.0.0.1");
Check(inbound["port"]!.GetValue<int>() == Constants.XrayPort);
Check(inbound["settings"]!["auth"]!.GetValue<string>() == "noauth" && inbound["settings"]!["udp"]!.GetValue<bool>());
Check(inbound["settings"]!["ip"]!.GetValue<string>() == "127.0.0.1" && !inbound["sniffing"]!["enabled"]!.GetValue<bool>());
var outbound = config["outbounds"]!.AsArray().Single()!;
Check(outbound["protocol"]!.GetValue<string>() == "vless" && !outbound["mux"]!["enabled"]!.GetValue<bool>());
Check(config["routing"]!["rules"]!.AsArray().Single()!["outboundTag"]!.GetValue<string>() == outbound["tag"]!.GetValue<string>());
Check(config["dns"] is null && config["log"]!["loglevel"]!.GetValue<string>() == "error");
});
await test("Xray retains every VLESS and Reality connection field", () =>
{
foreach (var flow in new[] { "xtls-rprx-vision", "" })
{
var node = Fixture() with { Flow = flow };
var outbound = XrayConfigBuilder.Build(node)["outbounds"]!.AsArray().Single()!;
var endpoint = outbound["settings"]!["vnext"]!.AsArray().Single()!;
var user = endpoint["users"]!.AsArray().Single()!;
Check(endpoint["address"]!.GetValue<string>() == node.Host && endpoint["port"]!.GetValue<int>() == node.Port);
Check(user["id"]!.GetValue<string>() == node.Uuid && user["flow"]!.GetValue<string>() == node.Flow && user["encryption"]!.GetValue<string>() == "none");
var stream = outbound["streamSettings"]!;
Check(stream["network"]!.GetValue<string>() == "raw" && stream["security"]!.GetValue<string>() == "reality");
var reality = stream["realitySettings"]!;
Check(reality["serverName"]!.GetValue<string>() == node.ServerName && reality["fingerprint"]!.GetValue<string>() == node.Fingerprint);
Check(reality["password"]!.GetValue<string>() == node.PublicKey && reality["shortId"]!.GetValue<string>() == node.ShortId);
Check(reality["show"]!.GetValue<bool>() == false && reality["spiderX"]!.GetValue<string>() == "/");
}
});
await test("Xray optional Windows binding uses the requested adapter and port", () =>
{
var bound = XrayConfigBuilder.Build(Fixture(), 17902, "Беспроводная сеть");
Check(bound["inbounds"]!.AsArray().Single()!["port"]!.GetValue<int>() == 17902);
Check(bound["outbounds"]!.AsArray().Single()!["streamSettings"]!["sockopt"]!["interface"]!.GetValue<string>() == "Беспроводная сеть");
Check(XrayConfigBuilder.Build(Fixture())["outbounds"]!.AsArray().Single()!["streamSettings"]!["sockopt"] is null);
foreach (var port in new[] { 0, -1, 65536 }) Throws<ArgumentOutOfRangeException>(() => XrayConfigBuilder.Build(Fixture(), port));
foreach (var name in new[] { "", " ", "adapter\nname", new string('a', 257) }) Throws<ArgumentException>(() => XrayConfigBuilder.Build(Fixture(), bindInterface: name));
});
}
// Checks the exact packaged engine using synthetic credentials only. Raw
// process output is drained, since future checks may include private data.
public static async Task<int> CheckEngineAsync(string executable, CancellationToken token = default)
{
string directory = Path.Combine(Path.GetTempPath(), "QVPN.XrayConfigTests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
try
{
foreach (var flow in new[] { "xtls-rprx-vision", "" })
foreach (var bound in new[] { false, true })
{
var config = XrayConfigBuilder.Build(Fixture() with { Flow = flow }, 17902, bound ? "Ethernet" : null);
string path = Path.Combine(directory, "synthetic-config.json");
await File.WriteAllTextAsync(path, config.ToJsonString(), token);
var start = new ProcessStartInfo(Path.GetFullPath(executable))
{
UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true,
WorkingDirectory = Path.GetDirectoryName(Path.GetFullPath(executable))!
};
start.ArgumentList.Add("run"); start.ArgumentList.Add("-test"); start.ArgumentList.Add("-config"); start.ArgumentList.Add(path);
using var process = Process.Start(start) ?? throw new InvalidOperationException("Xray test process did not start.");
var stdout = process.StandardOutput.ReadToEndAsync(); var stderr = process.StandardError.ReadToEndAsync();
using var limit = CancellationTokenSource.CreateLinkedTokenSource(token); limit.CancelAfter(TimeSpan.FromSeconds(10));
try { await process.WaitForExitAsync(limit.Token); }
catch { try { if (!process.HasExited) process.Kill(true); } catch { } throw; }
await Task.WhenAll(stdout, stderr);
Console.WriteLine($"XRAY_CONFIG_CHECK flowVision={flow.Length > 0} binding={bound} exit={process.ExitCode}");
if (process.ExitCode != 0) return process.ExitCode;
}
return 0;
}
finally
{
string absolute = Path.GetFullPath(directory);
string root = Path.GetFullPath(Path.GetTempPath()).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (!absolute.StartsWith(root, StringComparison.OrdinalIgnoreCase) || !Path.GetFileName(absolute).StartsWith("QVPN.XrayConfigTests-", StringComparison.Ordinal))
throw new InvalidOperationException("Unsafe Xray test cleanup target.");
Directory.Delete(absolute, true);
}
}
private static VpnNode Fixture()
{
string uuid = new Guid(Enumerable.Range(1, 16).Select(i => (byte)i).ToArray()).ToString();
string key = Convert.ToBase64String(Enumerable.Range(1, 32).Select(i => (byte)i).ToArray()).TrimEnd('=').Replace('+', '-').Replace('/', '_');
return new("synthetic", "Synthetic location", "8.8.8.8", 443, uuid, "example.com", key, "abcd", "chrome", "xtls-rprx-vision");
}
private static void Check(bool condition)
{
if (!condition) throw new InvalidOperationException("Xray configuration regression.");
}
private static void Throws<T>(Action action) where T : Exception
{
try { action(); } catch (T) { return; }
throw new InvalidOperationException("Xray configuration validation regression.");
}
}