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.
110 lines
7.1 KiB
C#
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.");
|
|
}
|
|
}
|