using System.Diagnostics; using System.Text.Json.Nodes; using QVPN.Core; public static class XrayConfigTests { public static async Task RunAsync(Func 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() == "socks" && inbound["listen"]!.GetValue() == "127.0.0.1"); Check(inbound["port"]!.GetValue() == Constants.XrayPort); Check(inbound["settings"]!["auth"]!.GetValue() == "noauth" && inbound["settings"]!["udp"]!.GetValue()); Check(inbound["settings"]!["ip"]!.GetValue() == "127.0.0.1" && !inbound["sniffing"]!["enabled"]!.GetValue()); var outbound = config["outbounds"]!.AsArray().Single()!; Check(outbound["protocol"]!.GetValue() == "vless" && !outbound["mux"]!["enabled"]!.GetValue()); Check(config["routing"]!["rules"]!.AsArray().Single()!["outboundTag"]!.GetValue() == outbound["tag"]!.GetValue()); Check(config["dns"] is null && config["log"]!["loglevel"]!.GetValue() == "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() == node.Host && endpoint["port"]!.GetValue() == node.Port); Check(user["id"]!.GetValue() == node.Uuid && user["flow"]!.GetValue() == node.Flow && user["encryption"]!.GetValue() == "none"); var stream = outbound["streamSettings"]!; Check(stream["network"]!.GetValue() == "raw" && stream["security"]!.GetValue() == "reality"); var reality = stream["realitySettings"]!; Check(reality["serverName"]!.GetValue() == node.ServerName && reality["fingerprint"]!.GetValue() == node.Fingerprint); Check(reality["password"]!.GetValue() == node.PublicKey && reality["shortId"]!.GetValue() == node.ShortId); Check(reality["show"]!.GetValue() == false && reality["spiderX"]!.GetValue() == "/"); } }); 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() == 17902); Check(bound["outbounds"]!.AsArray().Single()!["streamSettings"]!["sockopt"]!["interface"]!.GetValue() == "Беспроводная сеть"); Check(XrayConfigBuilder.Build(Fixture())["outbounds"]!.AsArray().Single()!["streamSettings"]!["sockopt"] is null); foreach (var port in new[] { 0, -1, 65536 }) Throws(() => XrayConfigBuilder.Build(Fixture(), port)); foreach (var name in new[] { "", " ", "adapter\nname", new string('a', 257) }) Throws(() => 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 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(Action action) where T : Exception { try { action(); } catch (T) { return; } throw new InvalidOperationException("Xray configuration validation regression."); } }