From f802695f39b0c1a7f5c7d2e713c22e1739bbbbe3 Mon Sep 17 00:00:00 2001 From: LamGC Date: Fri, 10 Jan 2025 10:00:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BE=9D=E6=8D=AE=20?= =?UTF-8?q?InboundSetting=20=E5=92=8C=20OutboundSetting=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=85=8D=E7=BD=AE=E7=9A=84=E5=8A=9F=E8=83=BD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Config/Outbound/BaseOutboundSetting.cs | 10 +- .../Outbound/ShadowSocksOutboundSetting.cs | 2 +- Proxd-man/Program.cs | 2 +- Proxd-man/Subscribe/SubscribeManager.cs | 6 +- Proxd-man/XrayCoreManager.cs | 276 +++++++++++++++--- 5 files changed, 247 insertions(+), 49 deletions(-) diff --git a/Proxd-man/Config/Outbound/BaseOutboundSetting.cs b/Proxd-man/Config/Outbound/BaseOutboundSetting.cs index 05427ec..3905f72 100644 --- a/Proxd-man/Config/Outbound/BaseOutboundSetting.cs +++ b/Proxd-man/Config/Outbound/BaseOutboundSetting.cs @@ -20,11 +20,13 @@ public abstract class BaseOutboundSetting /// /// 发送出站数据所使用的网卡地址. /// - public string BindAddress { get; init; } = "0.0.0.0"; + public string? BindAddress { get; init; } /// /// 出站代理协议类型. /// public OutboundProtocolType Protocol { get; init; } + + public string? Address { get; init; } public BaseStreamSetting StreamSetting { get; init; } = new RawStreamSetting(); @@ -41,12 +43,16 @@ public abstract class BaseOutboundSetting { var root = new JsonObject { - { "sendThrough", BindAddress }, { "protocol", Protocol.ToString().ToLower() }, { "tag", GetTag() }, { "settings", GetProtocolSetting() } }; + if (!string.IsNullOrEmpty(BindAddress)) + { + root.Add("sendThrough", BindAddress); + } + if (!string.IsNullOrEmpty(NextOutboundTag)) { root.Add("proxySettings", new JsonObject diff --git a/Proxd-man/Config/Outbound/ShadowSocksOutboundSetting.cs b/Proxd-man/Config/Outbound/ShadowSocksOutboundSetting.cs index cb33604..6e89f36 100644 --- a/Proxd-man/Config/Outbound/ShadowSocksOutboundSetting.cs +++ b/Proxd-man/Config/Outbound/ShadowSocksOutboundSetting.cs @@ -12,7 +12,7 @@ public class ShadowSocksOutboundSetting : BaseOutboundSetting public string? UserEmail { get; init; } = null; public required string Method { get; init; } - public required string Address { get; init; } + public new required string Address { get; init; } public int Port { get; init; } public required string Password { get; init; } public bool EnableUdpOverTcp { get; init; } = false; diff --git a/Proxd-man/Program.cs b/Proxd-man/Program.cs index ee6f0aa..c5f5869 100644 --- a/Proxd-man/Program.cs +++ b/Proxd-man/Program.cs @@ -41,7 +41,7 @@ internal class Program await xrayCoreManager.InitialXrayCoreProgram(); var subscribeManager = serviceProvider.GetRequiredService(); - subscribeManager.StartUpdateSubscribes(); + await subscribeManager.CheckAndUpdateSubscribes(); // 监听 SIGTERM 和 SIGINT 信号, 以优雅退出 var cts = new CancellationTokenSource(); diff --git a/Proxd-man/Subscribe/SubscribeManager.cs b/Proxd-man/Subscribe/SubscribeManager.cs index 106c22b..5b950df 100644 --- a/Proxd-man/Subscribe/SubscribeManager.cs +++ b/Proxd-man/Subscribe/SubscribeManager.cs @@ -22,7 +22,7 @@ public class SubscribeManager _logger = logger; _config = config; - _config.OnChange((_, _) => Task.Run(async () => await UpdateSubscribes())); + _config.OnChange((_, _) => Task.Run(async () => await CheckAndUpdateSubscribes())); } public delegate void SubscribeUpdatedEventHandler(IDictionary> subscribeOutbounds); @@ -30,7 +30,7 @@ public class SubscribeManager private ProxdConfig Config => _config.Get(null); - public async Task UpdateSubscribes(CancellationToken cancellationToken = default) + public async Task CheckAndUpdateSubscribes(CancellationToken cancellationToken = default) { var httpClient = new HttpClient(); @@ -100,7 +100,7 @@ public class SubscribeManager { while (!cts.IsCancellationRequested) { - await UpdateSubscribes(cts.Token); + await CheckAndUpdateSubscribes(cts.Token); await Task.Delay(TimeSpan.FromHours(12), cts.Token); } }, cts.Token); diff --git a/Proxd-man/XrayCoreManager.cs b/Proxd-man/XrayCoreManager.cs index a3313ab..769e840 100644 --- a/Proxd-man/XrayCoreManager.cs +++ b/Proxd-man/XrayCoreManager.cs @@ -4,44 +4,37 @@ 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 Proxd_man.Subscribe; +using Microsoft.Extensions.Options; +using Proxd_man.Config; +using Proxd_man.Config.Outbound; namespace Proxd_man; -public class XrayCoreManager +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 readonly ILogger _logger; - private readonly string _coreProgramDir; - private readonly string _workingDirectory; - public XrayCoreManager( - ILogger logger, - SubscribeManager subscribeManager, - string coreProgramDir = "./xray-core", - string workingDirectory = "./xray-core-run") - { - _logger = logger; - _coreProgramDir = coreProgramDir; - _workingDirectory = workingDirectory; + private string CoreProgramDir => config.CurrentValue.XrayCoreProgramDirectory; + private string WorkingDirectory => config.CurrentValue.XrayCoreWorkingDirectory; - // TODO: 看看注册订阅更新事件后, 需要进行哪些操作. - } + // TODO: 看看注册订阅更新事件后, 需要进行哪些操作. public string GetXrayCoreProgramPath() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return _coreProgramDir + "/xray.exe"; + return CoreProgramDir + "/xray.exe"; } - return _coreProgramDir + "/xray"; + return CoreProgramDir + "/xray"; } public string GetXrayCoreProgramDownloadUrl() @@ -113,7 +106,7 @@ public class XrayCoreManager return $"{XrayLatestReleaseAssetDownloadUrlPrefix}Xray-{os}-{arch}.zip"; } - + private Process CreateXrayCoreProcess(string args, bool redirectOutput = true) { var programPath = GetXrayCoreProgramPath(); @@ -122,11 +115,11 @@ public class XrayCoreManager throw new Exception("Xray-Core program not found: " + programPath); } - if (!Directory.Exists(_workingDirectory)) + if (!Directory.Exists(WorkingDirectory)) { - Directory.CreateDirectory(_workingDirectory); + Directory.CreateDirectory(WorkingDirectory); } - + return new Process { StartInfo = new ProcessStartInfo @@ -136,7 +129,7 @@ public class XrayCoreManager RedirectStandardOutput = redirectOutput, UseShellExecute = false, CreateNoWindow = true, - WorkingDirectory = _workingDirectory + WorkingDirectory = WorkingDirectory } }; } @@ -188,30 +181,30 @@ public class XrayCoreManager public async Task FetchXrayCoreProgram() { - if (!Directory.Exists(_coreProgramDir)) + if (!Directory.Exists(CoreProgramDir)) { - Directory.CreateDirectory(_coreProgramDir); + Directory.CreateDirectory(CoreProgramDir); } var programArchiveUrl = GetXrayCoreProgramDownloadUrl(); var programArchiveDigestUrl = programArchiveUrl + ".dgst"; - _logger.LogDebug("Xray-Core 下载地址: {programArchiveUrl}", programArchiveUrl); - _logger.LogDebug("Xray-Core 下载地址 (Digest): {programArchiveDigestUrl}", programArchiveDigestUrl); + 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..."); + 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..."); + logger.LogDebug("正在获取 SHA-256 Digest..."); var digests = await httpClient.GetStringAsync(programArchiveDigestUrl); - _logger.LogDebug("可用的 Digests: \n{digests}", digests); + logger.LogDebug("可用的 Digests: \n{digests}", digests); // digests 每行都有一个不同算法的 digest HEX 值, 因此从 digests 读出开头为 SHA-256= 的行, 并提取出其右侧的 Sha256 值 var sha256Digest = @@ -222,7 +215,7 @@ public class XrayCoreManager throw new Exception("Failed to get SHA-256 digest."); } - _logger.LogDebug("SHA-256 Digest: {sha256Digest}", sha256Digest); + logger.LogDebug("SHA-256 Digest: {sha256Digest}", sha256Digest); // Verify digest using var sha256 = SHA256.Create(); @@ -230,17 +223,17 @@ public class XrayCoreManager { var localDigest = await sha256.ComputeHashAsync(localArchiveFileStream); var localDigestHex = BitConverter.ToString(localDigest).Replace("-", "").ToLower(); - _logger.LogDebug("本地 SHA-256 Digest: {localDigest}", localDigestHex); + logger.LogDebug("本地 SHA-256 Digest: {localDigest}", localDigestHex); if (localDigestHex != sha256Digest) { throw new Exception("SHA-256 digest mismatch."); } - _logger.LogDebug("SHA-256 Digest 验证通过."); + logger.LogDebug("SHA-256 Digest 验证通过."); } - ZipFile.ExtractToDirectory(tempArchiveFile, _coreProgramDir, true); + ZipFile.ExtractToDirectory(tempArchiveFile, CoreProgramDir, true); // Set permissions if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || @@ -258,14 +251,14 @@ public class XrayCoreManager public async Task InitialXrayCoreProgram() { - _logger.LogInformation("正在检查 Xray-Core 安装情况以及版本..."); + logger.LogInformation("正在检查 Xray-Core 安装情况以及版本..."); var currentVersion = await GetCurrentXrayCoreVersion(); - _logger.LogDebug("当前 Xray-Core 版本: {currentVersion}", currentVersion); + logger.LogDebug("当前 Xray-Core 版本: {currentVersion}", currentVersion); var requiredUpdate = false; if (currentVersion == null) { requiredUpdate = true; - _logger.LogInformation("未找到 Xray-Core, 开始下载最新版本..."); + logger.LogInformation("未找到 Xray-Core, 开始下载最新版本..."); } else { @@ -273,7 +266,7 @@ public class XrayCoreManager if (currentVersion != latestXrayCoreVersion) { requiredUpdate = true; - _logger.LogInformation("发现新版本 Xray-Core (Version: {latestVersion}), 开始下载最新版本...", + logger.LogInformation("发现新版本 Xray-Core (Version: {latestVersion}), 开始下载最新版本...", latestXrayCoreVersion); } } @@ -281,11 +274,11 @@ public class XrayCoreManager if (requiredUpdate) { await FetchXrayCoreProgram(); - _logger.LogInformation("Xray-Core 更新完成."); + logger.LogInformation("Xray-Core 更新完成."); } else { - _logger.LogInformation("Xray-Core 已是最新版本, 无需更新."); + logger.LogInformation("Xray-Core 已是最新版本, 无需更新."); } } @@ -297,4 +290,203 @@ public class XrayCoreManager 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}"); + } + + } \ No newline at end of file