using System.Diagnostics; using System.IO.Compression; using System.Net; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Proxd_man.Config; using Proxd_man.Config.Outbound; namespace Proxd_man; public class XrayCoreManager( ILogger logger, IOptionsMonitor config) { private const string XrayLatestReleaseApiUrl = "https://api.github.com/repos/XTLS/Xray-core/releases/latest"; private const string XrayLatestReleaseAssetDownloadUrlPrefix = "https://github.com/XTLS/Xray-core/releases/latest/download/"; private string CoreProgramDir => config.CurrentValue.XrayCoreProgramDirectory; private string WorkingDirectory => config.CurrentValue.XrayCoreWorkingDirectory; // TODO: 看看注册订阅更新事件后, 需要进行哪些操作. public string GetXrayCoreProgramPath() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return CoreProgramDir + "/xray.exe"; } return CoreProgramDir + "/xray"; } public string GetXrayCoreProgramDownloadUrl() { string os; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var osVersion = Environment.OSVersion.Version; switch (osVersion.Major) { case < 6: case 6 when osVersion.Minor < 1: throw new Exception("Windows 7 or later is required."); case 6 when osVersion.Minor == 1: os = "win7"; break; default: os = "windows"; break; } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { os = "linux"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { os = "macos"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) { os = "freebsd"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("openbsd"))) { os = "openbsd"; } else { throw new Exception("Unsupported operating system."); } string arch; if (RuntimeInformation.ProcessArchitecture == Architecture.X64) { arch = "64"; } else if (RuntimeInformation.ProcessArchitecture == Architecture.X86) { arch = "32"; } else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm) { arch = "arm32-v7a"; } else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { arch = "arm64-v8a"; } else if (RuntimeInformation.ProcessArchitecture == Architecture.Armv6) { arch = "arm32-v6"; } else { throw new Exception("Unsupported architecture."); } return $"{XrayLatestReleaseAssetDownloadUrlPrefix}Xray-{os}-{arch}.zip"; } private Process CreateXrayCoreProcess(string args, bool redirectOutput = true) { var programPath = GetXrayCoreProgramPath(); if (!File.Exists(programPath)) { throw new Exception("Xray-Core program not found: " + programPath); } if (!Directory.Exists(WorkingDirectory)) { Directory.CreateDirectory(WorkingDirectory); } return new Process { StartInfo = new ProcessStartInfo { FileName = programPath, Arguments = args, RedirectStandardOutput = redirectOutput, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = WorkingDirectory } }; } /// /// 获取目前已安装的 Xray Core 版本. /// /// 如果已安装了 Xray Core 则返回对应的版本号, 否则返回 null. private async Task GetCurrentXrayCoreVersion() { // 检查平台, 如果是 Linux, 则检查 coreProgramDir + "/xray", 如果是 Windows, 则检查 coreProgramDir + "/xray.exe" var xrayCoreProgramPath = GetXrayCoreProgramPath(); if (!File.Exists(xrayCoreProgramPath)) { return null; } var process = CreateXrayCoreProcess("--version"); process.Start(); await process.WaitForExitAsync(); var output = (await process.StandardOutput.ReadToEndAsync()).Trim(); var versionMatch = Regex.Match(output, @"Xray (\d+\.\d+\.\d+)"); return versionMatch.Success ? versionMatch.Groups[1].Value : null; } public async Task GetLatestXrayCoreVersion() { using var client = new HttpClient(); client.DefaultRequestHeaders.Add("User-Agent", "XrayCoreManager"); var response = await client.GetAsync(XrayLatestReleaseApiUrl); if (response.StatusCode != HttpStatusCode.OK) { throw new Exception("Failed to get latest Xray Core version."); } using var content = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var root = content.RootElement; if (!root.TryGetProperty("tag_name", out var tagNameElement)) { throw new Exception("Failed to get latest Xray Core version."); } var tagName = tagNameElement.GetString() ?? throw new Exception("Failed to get latest Xray Core version."); return tagName.StartsWith('v') ? tagName[1..] : tagName; } public async Task FetchXrayCoreProgram() { if (!Directory.Exists(CoreProgramDir)) { Directory.CreateDirectory(CoreProgramDir); } var programArchiveUrl = GetXrayCoreProgramDownloadUrl(); var programArchiveDigestUrl = programArchiveUrl + ".dgst"; logger.LogDebug("Xray-Core 下载地址: {programArchiveUrl}", programArchiveUrl); logger.LogDebug("Xray-Core 下载地址 (Digest): {programArchiveDigestUrl}", programArchiveDigestUrl); var tempArchiveFile = Path.GetTempFileName(); using var httpClient = new HttpClient(); logger.LogDebug("正在下载 Xray-Core..."); await using var archiveStream = await httpClient.GetStreamAsync(programArchiveUrl); await using (var programArchiveFileStream = new FileStream(tempArchiveFile, FileMode.Create, FileAccess.Write)) { await archiveStream.CopyToAsync(programArchiveFileStream); } logger.LogDebug("正在获取 SHA-256 Digest..."); var digests = await httpClient.GetStringAsync(programArchiveDigestUrl); logger.LogDebug("可用的 Digests: \n{digests}", digests); // digests 每行都有一个不同算法的 digest HEX 值, 因此从 digests 读出开头为 SHA-256= 的行, 并提取出其右侧的 Sha256 值 var sha256Digest = digests.Split('\n').FirstOrDefault(line => line.StartsWith("SHA2-256="))?["SHA2-256=".Length..] ?.Trim(); if (sha256Digest == null) { throw new Exception("Failed to get SHA-256 digest."); } logger.LogDebug("SHA-256 Digest: {sha256Digest}", sha256Digest); // Verify digest using var sha256 = SHA256.Create(); await using (var localArchiveFileStream = new FileStream(tempArchiveFile, FileMode.Open, FileAccess.Read)) { var localDigest = await sha256.ComputeHashAsync(localArchiveFileStream); var localDigestHex = BitConverter.ToString(localDigest).Replace("-", "").ToLower(); logger.LogDebug("本地 SHA-256 Digest: {localDigest}", localDigestHex); if (localDigestHex != sha256Digest) { throw new Exception("SHA-256 digest mismatch."); } logger.LogDebug("SHA-256 Digest 验证通过."); } ZipFile.ExtractToDirectory(tempArchiveFile, CoreProgramDir, true); // Set permissions if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) { await Process.Start("chmod", "+x " + GetXrayCoreProgramPath()).WaitForExitAsync(); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { await Process.Start("chmod", "+x " + GetXrayCoreProgramPath()).WaitForExitAsync(); } File.Delete(tempArchiveFile); } public async Task InitialXrayCoreProgram() { logger.LogInformation("正在检查 Xray-Core 安装情况以及版本..."); var currentVersion = await GetCurrentXrayCoreVersion(); logger.LogDebug("当前 Xray-Core 版本: {currentVersion}", currentVersion); var requiredUpdate = false; if (currentVersion == null) { requiredUpdate = true; logger.LogInformation("未找到 Xray-Core, 开始下载最新版本..."); } else { var latestXrayCoreVersion = await GetLatestXrayCoreVersion(); if (currentVersion != latestXrayCoreVersion) { requiredUpdate = true; logger.LogInformation("发现新版本 Xray-Core (Version: {latestVersion}), 开始下载最新版本...", latestXrayCoreVersion); } } if (requiredUpdate) { await FetchXrayCoreProgram(); logger.LogInformation("Xray-Core 更新完成."); } else { logger.LogInformation("Xray-Core 已是最新版本, 无需更新."); } } public async Task VerifyXrayCoreConfigFile(string configFilePath) { var process = CreateXrayCoreProcess($"run -test -config {configFilePath}"); process.Start(); await process.WaitForExitAsync(); return process.ExitCode == 0; } /// /// 生成最终的 Xray-Core 配置 JSON 对象. /// /// 注意, 在 Xray-Core 中, Inbound 和 Outbound 的首位配置很重要. /// /// 配置使用的入站设置. /// 该配置使用的出站配置. /// /// public JsonObject GenerateXrayCoreConfigJson(List inboundConfigs, BaseOutboundSetting outboundSetting, bool onlyTest = false) { // 基本的设置. var proxyConfig = new JsonObject { { "log", new JsonObject { { "loglevel", "warning" } } }, { "policy", new JsonObject { { "levels", new JsonObject { // 给 DNS 使用的. { "1", new JsonObject { { "connIdle", 30 } } } } }, { "system", new JsonObject { { "statsOutboundDownlink", true }, { "statsOutboundUplink", true } } } } }, { "stats", new JsonObject() } }; // ------------- DNS 配置 ------------- var directResolveDomains = new JsonArray(); var localDnsServer = new JsonObject { { "address", "localhost" }, { "domains", directResolveDomains }, { "queryStrategy", "" } }; if (!onlyTest) { directResolveDomains.Add("geosite:cn"); } // 如果不是一个 IP 地址(v4 或 v6),则添加到 DNS 服务器列表中 if (!IPAddress.TryParse(outboundSetting.Address, out _)) { directResolveDomains.Add($"full:{outboundSetting.Address}"); } var dnsServers = new JsonArray(new JsonObject { { "address", config.CurrentValue.RemoteDnsServer }, { "domains", new JsonArray() }, { "queryStrategy", "" } }, localDnsServer); proxyConfig.Add("dns", new JsonObject { { "disableFallback", true }, { "servers", dnsServers } }); // ------------- DNS 配置 ↑ ------------- // ------------ Inbound 配置 ------------ var inbounds = new JsonArray(); proxyConfig.Add("inbounds", inbounds); foreach (var inboundConfig in inboundConfigs) { inbounds.Add(inboundConfig.ToJsonSetting()); } // ------------ Inbound 配置 ↑ ------------ var outbounds = new JsonArray(); proxyConfig.Add("outbounds", outbounds); var proxyOutboundSettings = outboundSetting.ToJsonSetting(); proxyOutboundSettings.Add("domainStrategy", "AsIs"); outbounds.Add(proxyOutboundSettings); outbounds.Add(new FreedomOutboundSetting("bypass") { Name = "bypass" }.ToJsonSetting()); outbounds.Add(new FreedomOutboundSetting { Name = "direct" }.ToJsonSetting()); outbounds.Add(new BlackholeOutboundSetting { Name = "block" }.ToJsonSetting()); // 添加 DNS 出站配置, 要求 DNS 走 Proxy 代理出站. outbounds.Add(new JsonObject { { "protocol", "dns" }, { "tag", "dns-out" }, { "settings", new JsonObject { { "address", "1.1.1.1" }, { "port", 53 }, { "network", "tcp" }, { "userLevel", 1 } } }, { "proxySettings", new JsonObject { { "tag", outboundSetting.GetTag() }, { "transportLayer", true } } } }); // ------------ Outbound 配置 ------------ // ------------ Routing 配置 ------------- var routingSettings = new JsonObject { { "domainStrategy", "AsIs" } }; proxyConfig.Add("routing", routingSettings); var inboundTags = new JsonArray(); foreach (var inboundConfig in inboundConfigs) { inboundTags.Add(inboundConfig.GetTag()); } var rules = new JsonArray(new JsonObject { { "inboundTag", inboundTags }, { "outboundTag", "dns-out" }, { "type", "field" }, { "port", "53" } }) { new JsonObject { { "ip", new JsonArray("geoip:cn", "geoip:private") }, { "outboundTag", "bypass" }, { "type", "field" }, }, new JsonObject { { "domain", new JsonArray("geosite:cn") }, { "outboundTag", "bypass" }, { "type", "field" }, }, new JsonObject { { "outboundTag", "proxy" }, { "port", "0-65535" }, { "type", "field" }, } }; routingSettings.Add("rules", rules); return proxyConfig; } public Process CreateOutboundTestProcess(ushort inboundHttpPort, BaseOutboundSetting outboundSetting) { var configJson = GenerateXrayCoreConfigJson([ new HttpInboundSetting { Port = inboundHttpPort } ], outboundSetting, true); var configFilePath = Path.Combine(Path.Combine(WorkingDirectory, "test-config"), Path.GetRandomFileName() + ".json"); using var configFileStream = File.CreateText(configFilePath); configFileStream.Write(JsonSerializer.Serialize(configJson)); return CreateXrayCoreProcess($"run -config {configFilePath}"); } }