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