From 99cb0a03a5d632743e704a4d976d8e440a9c6cf4 Mon Sep 17 00:00:00 2001 From: LamGC Date: Tue, 7 Jan 2025 09:50:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=AE=E5=89=8D=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=BA=86=E8=AE=A2=E9=98=85=E5=8A=9F=E8=83=BD=E5=92=8C=20Xray-C?= =?UTF-8?q?ore=20=E7=9A=84=E6=9B=B4=E6=96=B0=E7=AE=A1=E7=90=86.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + Proxd-man.sln | 16 + .../Config/Outbound/BaseOutboundSetting.cs | 79 +++++ .../Outbound/ShadowSocksOutboundSetting.cs | 50 +++ Proxd-man/Config/ProxdConfigs.cs | 32 ++ Proxd-man/Config/Stream/BaseStreamSetting.cs | 30 ++ Proxd-man/Config/Stream/RawStreamSetting.cs | 29 ++ Proxd-man/Config/SubscribeSource.cs | 7 + .../BaseTransportLayerSecuritySetting.cs | 16 + .../NoneSecuritySetting.cs | 17 + .../RealitySecuritySetting.cs | 30 ++ .../TlsSecuritySetting.cs | 26 ++ Proxd-man/OutboundProtocolType.cs | 12 + Proxd-man/Program.cs | 64 ++++ Proxd-man/Proxd-man.csproj | 21 ++ Proxd-man/ShareLink/ShareLinkParserImpls.cs | 203 ++++++++++++ Proxd-man/ShareLink/ShareLinkParsers.cs | 47 +++ Proxd-man/StreamType.cs | 14 + Proxd-man/Subscribe/SubscribeManager.cs | 116 +++++++ Proxd-man/TransportLayerSecurityType.cs | 8 + Proxd-man/XrayCoreManager.cs | 300 ++++++++++++++++++ Proxd-man/appsettings.json | 30 ++ 22 files changed, 1153 insertions(+) create mode 100644 .gitignore create mode 100644 Proxd-man.sln create mode 100644 Proxd-man/Config/Outbound/BaseOutboundSetting.cs create mode 100644 Proxd-man/Config/Outbound/ShadowSocksOutboundSetting.cs create mode 100644 Proxd-man/Config/ProxdConfigs.cs create mode 100644 Proxd-man/Config/Stream/BaseStreamSetting.cs create mode 100644 Proxd-man/Config/Stream/RawStreamSetting.cs create mode 100644 Proxd-man/Config/SubscribeSource.cs create mode 100644 Proxd-man/Config/TransportLayerSecurity/BaseTransportLayerSecuritySetting.cs create mode 100644 Proxd-man/Config/TransportLayerSecurity/NoneSecuritySetting.cs create mode 100644 Proxd-man/Config/TransportLayerSecurity/RealitySecuritySetting.cs create mode 100644 Proxd-man/Config/TransportLayerSecurity/TlsSecuritySetting.cs create mode 100644 Proxd-man/OutboundProtocolType.cs create mode 100644 Proxd-man/Program.cs create mode 100644 Proxd-man/Proxd-man.csproj create mode 100644 Proxd-man/ShareLink/ShareLinkParserImpls.cs create mode 100644 Proxd-man/ShareLink/ShareLinkParsers.cs create mode 100644 Proxd-man/StreamType.cs create mode 100644 Proxd-man/Subscribe/SubscribeManager.cs create mode 100644 Proxd-man/TransportLayerSecurityType.cs create mode 100644 Proxd-man/XrayCoreManager.cs create mode 100644 Proxd-man/appsettings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ce93a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +obj/ +.idea/ +*.sln.DotSettings.user + +run/ diff --git a/Proxd-man.sln b/Proxd-man.sln new file mode 100644 index 0000000..4680344 --- /dev/null +++ b/Proxd-man.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proxd-man", "Proxd-man\Proxd-man.csproj", "{AFA74924-C8B9-4DB2-8507-DC3480063C23}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AFA74924-C8B9-4DB2-8507-DC3480063C23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFA74924-C8B9-4DB2-8507-DC3480063C23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFA74924-C8B9-4DB2-8507-DC3480063C23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFA74924-C8B9-4DB2-8507-DC3480063C23}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Proxd-man/Config/Outbound/BaseOutboundSetting.cs b/Proxd-man/Config/Outbound/BaseOutboundSetting.cs new file mode 100644 index 0000000..c495ea0 --- /dev/null +++ b/Proxd-man/Config/Outbound/BaseOutboundSetting.cs @@ -0,0 +1,79 @@ +using System.Text.Json.Nodes; +using Proxd_man.Config.Stream; +using Proxd_man.Config.TransportLayerSecurity; + +namespace Proxd_man.Config.Outbound; + +/// +/// 基本的出站代理配置类. +/// +/// 所有协议的出站配置都要继承这个类. +/// +public abstract class BaseOutboundSetting +{ + /// + /// 出站配置名称. + /// + /// 便于管理的名字, 可以直接从订阅中获取. + /// + public required string Name { get; init; } + /// + /// 发送出站数据所使用的网卡地址. + /// + public string BindAddress { get; init; } = "0.0.0.0"; + /// + /// 出站代理协议类型. + /// + public required OutboundProtocolType Protocol { get; init; } + + public BaseStreamSetting StreamSetting { get; init; } = new RawStreamSetting(); + + public BaseTransportLayerSecuritySetting TransportLayerSecuritySetting { get; init; } = new NoneSecuritySetting(); + + public string? NextOutboundTag { get; init; } = null; + + /// + /// 根据Xray-Core 的配置文件格式, + /// 生成对应的 OutboundObject JSON 配置. + /// + /// + public JsonObject ToJsonSetting() + { + var root = new JsonObject + { + { "sendThrough", BindAddress }, + { "protocol", Protocol.ToString().ToLower() }, + { "tag", GetTag() }, + { "settings", GetProtocolSetting() } + }; + + if (!string.IsNullOrEmpty(NextOutboundTag)) + { + root.Add("proxySettings", new JsonObject + { + { "tag", NextOutboundTag } + }); + } + else + { + var streamSetting = StreamSetting.ToJsonSetting(); + TransportLayerSecuritySetting.ApplyJsonSetting(streamSetting); + root.Add("streamSettings", streamSetting); + } + + return root; + } + + /// + /// 实现依据配置内容, 生成对应的 Outbound Tag. + /// + /// 返回该 Outbound 的 Tag, 最好唯一. + public abstract string GetTag(); + + /// + /// 实现类依据自身对应协议类型的设定, 生成对应的 V2Ray/Xray-Core Outbound Protocol Setting JSON 对象. + /// + /// 返回协议配置的 JSON 对象. + protected abstract JsonObject? GetProtocolSetting(); + +} \ No newline at end of file diff --git a/Proxd-man/Config/Outbound/ShadowSocksOutboundSetting.cs b/Proxd-man/Config/Outbound/ShadowSocksOutboundSetting.cs new file mode 100644 index 0000000..b92d67a --- /dev/null +++ b/Proxd-man/Config/Outbound/ShadowSocksOutboundSetting.cs @@ -0,0 +1,50 @@ +using System.Text.Json.Nodes; + +namespace Proxd_man.Config.Outbound; + +/// +/// Xray-Core 的 ShadowSocks 出站配置. +/// +/// 目前不支持设置多服务器. +/// +public class ShadowSocksOutboundSetting : BaseOutboundSetting +{ + + public string? UserEmail { get; init; } = null; + public required string Method { get; init; } + public required string Address { get; init; } + public int Port { get; init; } + public required string Password { get; init; } + public bool EnableUdpOverTcp { get; init; } = false; + public uint? UdpOverTcpVersion { get; init; } = null; + + public override string GetTag() + { + return $"ShadowSocks-{GetHashCode()}"; + } + + protected override JsonObject GetProtocolSetting() + { + var root = new JsonObject(); + + var setting = new JsonObject(); + setting.Add("address", Address); + setting.Add("port", Port); + setting.Add("method", Method); + setting.Add("password", Password); + + setting.Add("uot", EnableUdpOverTcp); + if (EnableUdpOverTcp) + { + setting.Add("UoTVersion", UdpOverTcpVersion); + } + + if (!string.IsNullOrEmpty(UserEmail)) + { + setting.Add("email", UserEmail); + } + + root.Add("servers", new JsonArray { setting }); + return root; + } +} \ No newline at end of file diff --git a/Proxd-man/Config/ProxdConfigs.cs b/Proxd-man/Config/ProxdConfigs.cs new file mode 100644 index 0000000..966b50b --- /dev/null +++ b/Proxd-man/Config/ProxdConfigs.cs @@ -0,0 +1,32 @@ +namespace Proxd_man.Config; + +public class ProxdConfig +{ + public InboundConfig Inbound { get; } = new(new HttpInboundSetting()); + public SubscribeSetting Subscribe { get; } = new(); +} + +public class InboundConfig(HttpInboundSetting? http = null) +{ + public HttpInboundSetting? Http { get; } = http; +} + +public class HttpInboundSetting(string? bindAddress = null, ushort port = 1080, BasicInboundAuthSetting? authentication = null) +{ + public string? BindAddress { get; } = bindAddress; + public ushort Port = port; + + public BasicInboundAuthSetting? Authentication { get; } = authentication; +} + +public class BasicInboundAuthSetting(string username, string password) +{ + public string Username { get; } = username; + public string Password { get; } = password; +} + +public class SubscribeSetting +{ + public List Subscribes { get; } = []; + +} \ No newline at end of file diff --git a/Proxd-man/Config/Stream/BaseStreamSetting.cs b/Proxd-man/Config/Stream/BaseStreamSetting.cs new file mode 100644 index 0000000..633b0cc --- /dev/null +++ b/Proxd-man/Config/Stream/BaseStreamSetting.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Nodes; + +namespace Proxd_man.Config.Stream; + +public abstract class BaseStreamSetting +{ + + public StreamType StreamType { get; init; } + + public JsonObject ToJsonSetting() + { + var setting = new JsonObject + { + { "network", StreamType.ToString().ToLower() } + }; + + var specificSettingFieldName = GetSpecificSettingFieldName(); + if (!string.IsNullOrEmpty(specificSettingFieldName)) + { + setting.Add(specificSettingFieldName, ToJsonSpecificSetting()); + } + + return setting; + } + + protected abstract string? GetSpecificSettingFieldName(); + + protected abstract JsonObject? ToJsonSpecificSetting(); + +} \ No newline at end of file diff --git a/Proxd-man/Config/Stream/RawStreamSetting.cs b/Proxd-man/Config/Stream/RawStreamSetting.cs new file mode 100644 index 0000000..e02099e --- /dev/null +++ b/Proxd-man/Config/Stream/RawStreamSetting.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Nodes; + +namespace Proxd_man.Config.Stream; + +/// +/// 以 TCP 形式不加密直接传输. +/// +/// 目前暂时不支持设置 Proxy Protocol. +/// +public class RawStreamSetting : BaseStreamSetting +{ + + public RawStreamSetting() + { + StreamType = StreamType.Raw; + } + + protected override string GetSpecificSettingFieldName() + { + return "rawSettings"; + } + + protected override JsonObject ToJsonSpecificSetting() + { + var setting = new JsonObject(); + + return setting; + } +} \ No newline at end of file diff --git a/Proxd-man/Config/SubscribeSource.cs b/Proxd-man/Config/SubscribeSource.cs new file mode 100644 index 0000000..0329dd0 --- /dev/null +++ b/Proxd-man/Config/SubscribeSource.cs @@ -0,0 +1,7 @@ +namespace Proxd_man.Config; + +public class SubscribeSource +{ + public required Uri Url { get; init; } + public string? Name { get; init; } +} \ No newline at end of file diff --git a/Proxd-man/Config/TransportLayerSecurity/BaseTransportLayerSecuritySetting.cs b/Proxd-man/Config/TransportLayerSecurity/BaseTransportLayerSecuritySetting.cs new file mode 100644 index 0000000..8706a84 --- /dev/null +++ b/Proxd-man/Config/TransportLayerSecurity/BaseTransportLayerSecuritySetting.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Nodes; + +namespace Proxd_man.Config.TransportLayerSecurity; + +public abstract class BaseTransportLayerSecuritySetting +{ + + public TransportLayerSecurityType TransportLayerSecurityType { get; init; } + + /// + /// 将配置写入到 StreamSetting JSON 对象中. + /// + /// 现有的 StreamSetting JSON 对象. + public abstract void ApplyJsonSetting(JsonObject streamSettingJson); + +} \ No newline at end of file diff --git a/Proxd-man/Config/TransportLayerSecurity/NoneSecuritySetting.cs b/Proxd-man/Config/TransportLayerSecurity/NoneSecuritySetting.cs new file mode 100644 index 0000000..675cbf7 --- /dev/null +++ b/Proxd-man/Config/TransportLayerSecurity/NoneSecuritySetting.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Nodes; + +namespace Proxd_man.Config.TransportLayerSecurity; + +public class NoneSecuritySetting : BaseTransportLayerSecuritySetting +{ + + public NoneSecuritySetting() + { + TransportLayerSecurityType = TransportLayerSecurityType.None; + } + + public override void ApplyJsonSetting(JsonObject streamSettingJson) + { + streamSettingJson.Add("security", "none"); + } +} \ No newline at end of file diff --git a/Proxd-man/Config/TransportLayerSecurity/RealitySecuritySetting.cs b/Proxd-man/Config/TransportLayerSecurity/RealitySecuritySetting.cs new file mode 100644 index 0000000..f67212f --- /dev/null +++ b/Proxd-man/Config/TransportLayerSecurity/RealitySecuritySetting.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Nodes; + +namespace Proxd_man.Config.TransportLayerSecurity; + +public class RealitySecuritySetting : BaseTransportLayerSecuritySetting +{ + + public string ServerName { get; init; } = "0.0.0.0"; + public string FingerPrint { get; init; } = "chrome"; + public required string ShortId { get; init; } + public required string PublicKey { get; init; } + + public RealitySecuritySetting() + { + TransportLayerSecurityType = TransportLayerSecurityType.Reality; + } + + public override void ApplyJsonSetting(JsonObject streamSettingJson) + { + streamSettingJson.Add("security", "reality"); + + var realitySetting = new JsonObject(); + realitySetting.Add("serverName", ServerName); + realitySetting.Add("fingerprint", FingerPrint); + realitySetting.Add("shortId", ShortId); + realitySetting.Add("publicKey", PublicKey); + + streamSettingJson.Add("realitySettings", realitySetting); + } +} \ No newline at end of file diff --git a/Proxd-man/Config/TransportLayerSecurity/TlsSecuritySetting.cs b/Proxd-man/Config/TransportLayerSecurity/TlsSecuritySetting.cs new file mode 100644 index 0000000..eb2edf2 --- /dev/null +++ b/Proxd-man/Config/TransportLayerSecurity/TlsSecuritySetting.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Nodes; + +namespace Proxd_man.Config.TransportLayerSecurity; + +public class TlsSecuritySetting : BaseTransportLayerSecuritySetting +{ + + public string? ServerName { get; init; } = null; + public bool IsRejectUnknownSni { get; init; } = false; + public List Alpn { get; init; } = ["h2", "http/1.1"]; + public string MinVersion { get; init; } = "1.2"; + public string MaxVersion { get; init; } = "1.3"; + public string? CipherSuites { get; init; } = null; + public bool IsAllowInsecure { get; init; } = false; + public string FingerPrint { get; init; } = "chrome"; + + public TlsSecuritySetting() + { + TransportLayerSecurityType = TransportLayerSecurityType.Tls; + } + + public override void ApplyJsonSetting(JsonObject streamSettingJson) + { + streamSettingJson.Add("security", "tls"); + } +} \ No newline at end of file diff --git a/Proxd-man/OutboundProtocolType.cs b/Proxd-man/OutboundProtocolType.cs new file mode 100644 index 0000000..bc0528a --- /dev/null +++ b/Proxd-man/OutboundProtocolType.cs @@ -0,0 +1,12 @@ +namespace Proxd_man; + +public enum OutboundProtocolType +{ + Http, + Socks, + Shadowsocks, + Trojan, + Vless, + VMess, + WireGuard +} \ No newline at end of file diff --git a/Proxd-man/Program.cs b/Proxd-man/Program.cs new file mode 100644 index 0000000..ee6f0aa --- /dev/null +++ b/Proxd-man/Program.cs @@ -0,0 +1,64 @@ +using System.Runtime.Loader; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Proxd_man.Config; +using Proxd_man.Subscribe; + +namespace Proxd_man; + +internal class Program +{ + + public static async Task Main(string[] args) + { + Console.WriteLine($"Base Directory: {AppContext.BaseDirectory}"); + + var configurationBuilder = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddCommandLine(args); + + var configuration = configurationBuilder.Build(); + + var serviceProvider = new ServiceCollection() + .AddLogging(configure => + { + configure.AddConfiguration(configuration); + configure.AddConsole(); + }) + .Configure(configuration.GetSection("Proxd")) + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); + + var logger = serviceProvider.GetRequiredService>(); + + var xrayCoreManager = serviceProvider.GetRequiredService(); + await xrayCoreManager.InitialXrayCoreProgram(); + + var subscribeManager = serviceProvider.GetRequiredService(); + subscribeManager.StartUpdateSubscribes(); + + // 监听 SIGTERM 和 SIGINT 信号, 以优雅退出 + var cts = new CancellationTokenSource(); + AppDomain.CurrentDomain.ProcessExit += (_, _) => cts.Cancel(); + Console.CancelKeyPress += (_, _) => cts.Cancel(); + AssemblyLoadContext.Default.Unloading += _ => cts.Cancel(); + + try + { + await Task.Delay(-1, cts.Token); + } + catch (TaskCanceledException) + { + logger.LogInformation("正在退出..."); + } + + logger.LogInformation("Good Bye!"); + } + +} diff --git a/Proxd-man/Proxd-man.csproj b/Proxd-man/Proxd-man.csproj new file mode 100644 index 0000000..bcfb473 --- /dev/null +++ b/Proxd-man/Proxd-man.csproj @@ -0,0 +1,21 @@ + + + + Exe + net9.0 + Proxd_man + enable + enable + + + + + + + + + + + + + diff --git a/Proxd-man/ShareLink/ShareLinkParserImpls.cs b/Proxd-man/ShareLink/ShareLinkParserImpls.cs new file mode 100644 index 0000000..1af4218 --- /dev/null +++ b/Proxd-man/ShareLink/ShareLinkParserImpls.cs @@ -0,0 +1,203 @@ +using System.Buffers.Text; +using System.Net; +using System.Text; +using Proxd_man.Config.Outbound; +using Proxd_man.Config.Stream; + +namespace Proxd_man.ShareLink; + +/// +/// Shadowsocks 分享链接解析器. +/// +/// 遵循了 Shadowsocks 的 SIP002 URI Scheme 规范. +/// +public class ShadowSocksLinkParser : IShareLinkParser +{ + public bool CanParse(string shareLink) + { + return shareLink.StartsWith("ss://", StringComparison.CurrentCultureIgnoreCase); + } + + public BaseOutboundSetting Parse(string shareLink) + { + var content = shareLink[5..]; + if (content.Length == 0) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接内容为空."); + } + + // 按照顺序, 先解析 userInfo. + var userInfoEndAt = content.IndexOf('@'); + if (userInfoEndAt == -1) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接中未包含用户信息."); + } + + var userInfo = content[..userInfoEndAt]; + string userInfoContent; + + if (userInfo.Contains(':')) + { + userInfoContent = userInfo.Trim(); + } + else + { + if (!Base64.IsValid(userInfo)) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接中的用户信息不是有效的 Base64 字符串."); + } + + var userInfoBytes = Convert.FromBase64String(userInfo); + userInfoContent = Encoding.UTF8.GetString(userInfoBytes); + } + + var userInfoParts = userInfoContent.Split(':', 2); + var method = userInfoParts[0]; + var password = userInfoParts[1]; + + // 开始解析 userInfo 后面的内容. + content = content[(userInfoEndAt + 1)..]; + + var pathSeparatorAt = content.IndexOf('/'); + var fragmentSeparatorAt = content.IndexOf('#'); + int hostEndAt; + + // 首先检查一下合法性, 遵循这些规则: + // 如果有 pathSeparator, 则检查是否有 "?", 且是否位于 pathSeparator 之后, 否则抛出异常. + // 其次, fragmentSeparator 只能出现在 pathSeparator 之后. + + if (pathSeparatorAt != -1) + { + if (content.Contains('?') && content.IndexOf('?') < pathSeparatorAt) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接中的 path 部分不合法."); + } + + if (fragmentSeparatorAt != -1 && fragmentSeparatorAt < pathSeparatorAt) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接中的 tag 部分不合法."); + } + + hostEndAt = pathSeparatorAt; + } else if (fragmentSeparatorAt != -1) + { + hostEndAt = fragmentSeparatorAt; + } + else + { + hostEndAt = content.Length; + } + + if (content[pathSeparatorAt..].Contains("?plugin=")) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接暂时不支持 plugin 参数."); + } + + var name = fragmentSeparatorAt != -1 ? null : + // 需要 Url 解码 + WebUtility.UrlDecode(content[(fragmentSeparatorAt + 1)..].Trim()); + + var hostContent = content[..hostEndAt]; + var hostPortParts = hostContent.Split(':'); + switch (hostPortParts.Length) + { + case > 2: + throw new InvalidShareLinkException("Shadowsocks 分享链接中的 host 部分不合法."); + case < 2: + throw new InvalidShareLinkException("Shadowsocks 分享链接中的 host 部分不包含端口."); + } + + var host = hostPortParts[0]; + if (!int.TryParse(hostPortParts[1], out var port)) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接中的端口不是一个数字."); + } + + if (port is < 0 or > 65535) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接中的端口超出了端口范围."); + } + + return new ShadowSocksOutboundSetting + { + Protocol = OutboundProtocolType.Shadowsocks, + Name = name ?? $"{host}:{port}", + StreamSetting = new RawStreamSetting(), + + Address = host, + Port = port, + Method = method, + Password = password + }; + } +} + +/// +/// Shadowsocks 分享链接解析器. +/// +/// 这个版本使用 Uri 进行解析, 而不是手搓解析过程. +/// +/// 遵循了 Shadowsocks 的 SIP002 URI Scheme 规范. +/// +public class ShadowSocksLinkParserV2 : IShareLinkParser +{ + public bool CanParse(string shareLink) + { + return shareLink.StartsWith("ss://", StringComparison.CurrentCultureIgnoreCase); + } + + public BaseOutboundSetting Parse(string shareLink) + { + var shareLinkUri = new Uri(shareLink); + + var userInfoContent = shareLinkUri.UserInfo; + + if (!userInfoContent.Contains(':')) + { + if (!Base64.IsValid(userInfoContent)) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接中的用户信息不是有效的 Base64 字符串."); + } + + var userInfoBytes = Convert.FromBase64String(userInfoContent); + userInfoContent = Encoding.UTF8.GetString(userInfoBytes); + } + + var userInfoParts = userInfoContent.Split(':'); + if (userInfoParts.Length != 2) + { + throw new InvalidShareLinkException("Shadowsocks 分享链接中的 userInfo 不合法."); + } + + var method = WebUtility.UrlDecode(userInfoParts[0]); + var password = WebUtility.UrlDecode(userInfoParts[1]); + + var host = shareLinkUri.Host; + var port = (ushort) shareLinkUri.Port; + + return new ShadowSocksOutboundSetting + { + Protocol = OutboundProtocolType.Shadowsocks, + Name = string.IsNullOrEmpty(shareLinkUri.Fragment) ? $"{host}:{port}" : WebUtility.UrlDecode(shareLinkUri.Fragment.Trim()), + StreamSetting = new RawStreamSetting(), + + Address = host, + Port = port, + Method = method, + Password = password + }; + } +} + +public class VMessLinkParser : IShareLinkParser +{ + public bool CanParse(string shareLink) + { + return shareLink.StartsWith("vmess://", StringComparison.CurrentCultureIgnoreCase); + } + + public BaseOutboundSetting Parse(string shareLink) + { + throw new NotImplementedException(); + } +} diff --git a/Proxd-man/ShareLink/ShareLinkParsers.cs b/Proxd-man/ShareLink/ShareLinkParsers.cs new file mode 100644 index 0000000..3d6cbda --- /dev/null +++ b/Proxd-man/ShareLink/ShareLinkParsers.cs @@ -0,0 +1,47 @@ +using Proxd_man.Config.Outbound; + +namespace Proxd_man.ShareLink; + +public class ShareLinkParser +{ + + private static readonly Lazy> parser = new(() => [ + new ShadowSocksLinkParserV2() + ]); + + public static BaseOutboundSetting Parse(string shareLink) + { + foreach (var p in parser.Value.Where(p => p.CanParse(shareLink))) + { + return p.Parse(shareLink); + } + + throw new InvalidShareLinkException("不支持的分享链接: " + shareLink); + } + +} + +public interface IShareLinkParser +{ + + /// + /// 检查该分享链接是否可以被该实现解析. + /// + /// 该方法只需要检查是否可以解析, 并不需要检查是否正确. + /// + /// 要尝试解析的出站配置分享链接. + /// 如果可以被实现解析, 则返回 true. + bool CanParse(string shareLink); + + + /// + /// 将分享链接解析为出站配置. + /// + /// 要解析的分享链接. + /// 返回分享链接解析得出的出站配置. + /// 当分享链接错误导致无法解析时抛出异常. + BaseOutboundSetting Parse(string shareLink); + +} + +public class InvalidShareLinkException(string message, Exception? cause = null) : Exception(message, cause); diff --git a/Proxd-man/StreamType.cs b/Proxd-man/StreamType.cs new file mode 100644 index 0000000..4f451d8 --- /dev/null +++ b/Proxd-man/StreamType.cs @@ -0,0 +1,14 @@ +namespace Proxd_man; + +public enum StreamType +{ + /// + /// 直接通过 TCP 发送数据, 不进行处理. + /// + Raw, + XHttp, + Kcp, + GRpc, + WebSocket, + HttpUpgrade +} \ No newline at end of file diff --git a/Proxd-man/Subscribe/SubscribeManager.cs b/Proxd-man/Subscribe/SubscribeManager.cs new file mode 100644 index 0000000..106c22b --- /dev/null +++ b/Proxd-man/Subscribe/SubscribeManager.cs @@ -0,0 +1,116 @@ +using System.Buffers.Text; +using System.Collections.Immutable; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Proxd_man.Config; +using Proxd_man.Config.Outbound; +using Proxd_man.ShareLink; + +namespace Proxd_man.Subscribe; + +public class SubscribeManager +{ + + private readonly Dictionary> _subscribeOutbounds = new(); + private CancellationTokenSource? _cts; + private readonly ILogger _logger; + private readonly IOptionsMonitor _config; + + public SubscribeManager(ILogger logger, IOptionsMonitor config) + { + _logger = logger; + _config = config; + + _config.OnChange((_, _) => Task.Run(async () => await UpdateSubscribes())); + } + + public delegate void SubscribeUpdatedEventHandler(IDictionary> subscribeOutbounds); + public event SubscribeUpdatedEventHandler? SubscribeUpdated; + + private ProxdConfig Config => _config.Get(null); + + public async Task UpdateSubscribes(CancellationToken cancellationToken = default) + { + var httpClient = new HttpClient(); + + _logger.LogInformation("开始更新订阅..."); + + var newSubscribeOutbounds = new Dictionary>(); + for (var i = 0; i < Config.Subscribe.Subscribes.Count; i++) + { + var subscribe = Config.Subscribe.Subscribes[i]; + var outbounds = new List(); + + _logger.LogInformation("正在更新订阅 {subscribeName} ({current}/{total})...", subscribe.Name, i + 1, Config.Subscribe.Subscribes.Count); + try + { + var response = await httpClient.GetAsync(subscribe.Url, cancellationToken); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); + + if (Base64.IsValid(content)) + { + content = Encoding.UTF8.GetString(Convert.FromBase64String(content)); + } + + var shareLinks = content.Split('\n').Select(l => l.Trim()).Where(l => !string.IsNullOrEmpty(l)); + foreach (var shareLink in shareLinks) + { + try + { + var outbound = ShareLinkParser.Parse(shareLink); + outbounds.Add(outbound); + } + catch (InvalidShareLinkException e) + { + _logger.LogWarning(e, "解析分享链接失败: {shareLink}\n", shareLink); + } + } + } + catch (Exception e) + { + _logger.LogWarning(e, "更新订阅失败: {subscribeName}", subscribe.Name); + } + + newSubscribeOutbounds[subscribe] = outbounds; + _logger.LogInformation("订阅 {subscribeName} 更新完成. (共 {outboundCount} 个节点)", subscribe.Name, outbounds.Count); + } + + _subscribeOutbounds.Clear(); + foreach (var (source, outbounds) in newSubscribeOutbounds) + { + _subscribeOutbounds[source] = outbounds; + } + + _logger.LogInformation("订阅更新完成."); + SubscribeUpdated?.Invoke(_subscribeOutbounds.ToImmutableDictionary()); + } + + public IDictionary> GetSubscribeOutbounds() + { + return _subscribeOutbounds.ToImmutableDictionary(); + } + + public void StartUpdateSubscribes() + { + var cts = new CancellationTokenSource(); + + Task.Run(async () => + { + while (!cts.IsCancellationRequested) + { + await UpdateSubscribes(cts.Token); + await Task.Delay(TimeSpan.FromHours(12), cts.Token); + } + }, cts.Token); + _cts = cts; + _logger.LogInformation("订阅更新任务已启动."); + } + + public void StopUpdateSubscribes() + { + _cts?.Cancel(); + } + +} \ No newline at end of file diff --git a/Proxd-man/TransportLayerSecurityType.cs b/Proxd-man/TransportLayerSecurityType.cs new file mode 100644 index 0000000..8cf0e93 --- /dev/null +++ b/Proxd-man/TransportLayerSecurityType.cs @@ -0,0 +1,8 @@ +namespace Proxd_man; + +public enum TransportLayerSecurityType +{ + None, + Tls, + Reality, +} \ No newline at end of file diff --git a/Proxd-man/XrayCoreManager.cs b/Proxd-man/XrayCoreManager.cs new file mode 100644 index 0000000..a3313ab --- /dev/null +++ b/Proxd-man/XrayCoreManager.cs @@ -0,0 +1,300 @@ +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.RegularExpressions; +using Microsoft.Extensions.Logging; +using Proxd_man.Subscribe; + +namespace Proxd_man; + +public class XrayCoreManager +{ + + 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; + + // 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; + } +} \ No newline at end of file diff --git a/Proxd-man/appsettings.json b/Proxd-man/appsettings.json new file mode 100644 index 0000000..5f5bc86 --- /dev/null +++ b/Proxd-man/appsettings.json @@ -0,0 +1,30 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug" + }, + "Console": { + "LogLevel": { + "Default": "Debug" + } + } + }, + + "Proxd": { + "Inbound": { + "Http": { + "BindAddress": "0.0.0.0", + "Port": "1080", + "Authentication": null + } + }, + "Subscribe": { + "Subscribes": [ + { + "Name": "", + "Url": "" + } + ] + } + } +} \ No newline at end of file