Files
proxd-man/Proxd-man/XrayCoreManager.cs
T

492 lines
16 KiB
C#

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<XrayCoreManager> logger,
IOptionsMonitor<ProxdConfig> 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
}
};
}
/// <summary>
/// 获取目前已安装的 Xray Core 版本.
/// </summary>
/// <returns>如果已安装了 Xray Core 则返回对应的版本号, 否则返回 null.</returns>
private async Task<string?> 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<string> 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<bool> VerifyXrayCoreConfigFile(string configFilePath)
{
var process = CreateXrayCoreProcess($"run -test -config {configFilePath}");
process.Start();
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
/// <summary>
/// 生成最终的 Xray-Core 配置 JSON 对象.
///
/// 注意, 在 Xray-Core 中, Inbound 和 Outbound 的首位配置很重要.
/// </summary>
/// <param name="inboundConfigs">配置使用的入站设置.</param>
/// <param name="outboundSetting">该配置使用的出站配置.</param>
/// <param name="onlyTest"></param>
/// <returns></returns>
public JsonObject GenerateXrayCoreConfigJson(List<IInboundSetting> 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}");
}
}