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, ILoggerFactory loggerFactory, 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; 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(List args, bool redirectOutput = true, bool redirectError = 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(programPath, args) { RedirectStandardOutput = redirectOutput, RedirectStandardError = redirectError, 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 if (!config.CurrentValue.SkipUpdateXrayCore) { try { var latestXrayCoreVersion = await GetLatestXrayCoreVersion(); if (currentVersion != latestXrayCoreVersion) { requiredUpdate = true; logger.LogInformation("发现新版本 Xray-Core (Version: {latestVersion}), 开始下载最新版本...", latestXrayCoreVersion); } } catch (HttpRequestException e) { logger.LogError(e, "检查 Xray-Core 更新时发生错误, 跳过更新."); return; } } if (requiredUpdate) { try { await FetchXrayCoreProgram(); logger.LogInformation("Xray-Core 更新完成."); } catch (Exception e) { logger.LogError(e, "更新 Xray-Core 时发生错误."); if (currentVersion == null) { throw; } } } else { logger.LogInformation("Xray-Core 已是最新版本, 无需更新."); } } public async Task VerifyXrayCoreConfigFile(string configFilePath) { var process = CreateXrayCoreProcess(["run", "-test", "-c", configFilePath]); process.Start(); RedirectXrayCoreProcessOutput(process); 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 async Task CreateOutboundTestProcess(ushort inboundHttpPort, BaseOutboundSetting outboundSetting) { var configJson = GenerateXrayCoreConfigJson([ new HttpInboundSetting { Port = inboundHttpPort } ], outboundSetting, true); var testingConfigDir = Path.Combine(WorkingDirectory, "test-config"); if (!Directory.Exists(testingConfigDir)) { Directory.CreateDirectory(testingConfigDir); } var configFilePath = Path.GetFullPath(Path.Combine(testingConfigDir, Path.GetRandomFileName() + ".json")); await using (var configFileStream = File.CreateText(configFilePath)) { await configFileStream.WriteAsync(JsonSerializer.Serialize(configJson)); } if (!await VerifyXrayCoreConfigFile(configFilePath)) { throw new Exception($"测试配置验证失败: {configFilePath}"); } logger.LogDebug("Xray-Core 测试配置文件: {configFilePath}", configFilePath); var xrayCoreProcess = CreateXrayCoreProcess(["run", "-c", configFilePath], true, true); xrayCoreProcess.Exited += (_, _) => File.Delete(configFilePath); return xrayCoreProcess; } public Process? MainXrayCoreProcess { get; private set; } public BaseOutboundSetting? CurrentOutboundSetting { get; private set; } public delegate void MainXrayCoreProcessExitedHandler(); public event MainXrayCoreProcessExitedHandler? MainXrayCoreProcessExited; public delegate void MainXrayCoreProcessStartedHandler(); public event MainXrayCoreProcessStartedHandler? MainXrayCoreProcessStarted; public async Task StartMainXrayCoreProcess(BaseOutboundSetting outboundSetting, bool noStart = false) { if (MainXrayCoreProcess != null) { await StopMainXrayCoreProcess(); } logger.LogInformation("正在启动主要 Xray-Core 进程..."); var configJson = GenerateXrayCoreConfigJson(config.CurrentValue.Inbounds.ToInboundSettings(), outboundSetting); var configFilePath = Path.GetFullPath(Path.Combine(WorkingDirectory, "main-config.json")); await File.WriteAllTextAsync(configFilePath, JsonSerializer.Serialize(configJson)); logger.LogDebug("配置文件已写入: {configPath}", configFilePath); var process = CreateXrayCoreProcess(["run", "-c", configFilePath]); process.Exited += async (_, _) => { MainXrayCoreProcessExited?.Invoke(); await StopMainXrayCoreProcess(); }; process.Start(); RedirectXrayCoreProcessOutput(process); logger.LogInformation("主要 Xray-Core 进程已启动."); MainXrayCoreProcessStarted?.Invoke(); MainXrayCoreProcess = process; CurrentOutboundSetting = outboundSetting; } public async Task StopMainXrayCoreProcess() { if (MainXrayCoreProcess == null) { return; } if (!MainXrayCoreProcess.HasExited) { MainXrayCoreProcess.Kill(); await MainXrayCoreProcess.WaitForExitAsync(); } MainXrayCoreProcess = null; CurrentOutboundSetting = null; } public void RedirectXrayCoreProcessOutput(Process process) { var processLogger = loggerFactory.CreateLogger($"Xray-Core.Process[{process.Id}]"); process.BeginOutputReadLine(); process.BeginErrorReadLine(); process.OutputDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) { processLogger.LogInformation("{content}", e.Data); } }; process.ErrorDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) { processLogger.LogError("{content}", e.Data); } }; } }