feat: 目前完成了订阅功能和 Xray-Core 的更新管理.

This commit is contained in:
2025-01-07 09:50:02 +08:00
parent 78c7db692a
commit 99cb0a03a5
22 changed files with 1153 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
bin/
obj/
.idea/
*.sln.DotSettings.user
run/
+16
View File
@@ -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
@@ -0,0 +1,79 @@
using System.Text.Json.Nodes;
using Proxd_man.Config.Stream;
using Proxd_man.Config.TransportLayerSecurity;
namespace Proxd_man.Config.Outbound;
/// <summary>
/// 基本的出站代理配置类.
///
/// 所有协议的出站配置都要继承这个类.
/// </summary>
public abstract class BaseOutboundSetting
{
/// <summary>
/// 出站配置名称.
///
/// 便于管理的名字, 可以直接从订阅中获取.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// 发送出站数据所使用的网卡地址.
/// </summary>
public string BindAddress { get; init; } = "0.0.0.0";
/// <summary>
/// 出站代理协议类型.
/// </summary>
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;
/// <summary>
/// 根据<a href="https://xtls.github.io/config/outbound.html">Xray-Core 的配置文件格式</a>,
/// 生成对应的 OutboundObject JSON 配置.
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// 实现依据配置内容, 生成对应的 Outbound Tag.
/// </summary>
/// <returns>返回该 Outbound 的 Tag, 最好唯一.</returns>
public abstract string GetTag();
/// <summary>
/// 实现类依据自身对应协议类型的设定, 生成对应的 V2Ray/Xray-Core Outbound Protocol Setting JSON 对象.
/// </summary>
/// <returns>返回协议配置的 JSON 对象.</returns>
protected abstract JsonObject? GetProtocolSetting();
}
@@ -0,0 +1,50 @@
using System.Text.Json.Nodes;
namespace Proxd_man.Config.Outbound;
/// <summary>
/// Xray-Core 的 ShadowSocks 出站配置.
///
/// 目前不支持设置多服务器.
/// </summary>
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;
}
}
+32
View File
@@ -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<SubscribeSource> Subscribes { get; } = [];
}
@@ -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();
}
@@ -0,0 +1,29 @@
using System.Text.Json.Nodes;
namespace Proxd_man.Config.Stream;
/// <summary>
/// 以 TCP 形式不加密直接传输.
///
/// 目前暂时不支持设置 Proxy Protocol.
/// </summary>
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;
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace Proxd_man.Config;
public class SubscribeSource
{
public required Uri Url { get; init; }
public string? Name { get; init; }
}
@@ -0,0 +1,16 @@
using System.Text.Json.Nodes;
namespace Proxd_man.Config.TransportLayerSecurity;
public abstract class BaseTransportLayerSecuritySetting
{
public TransportLayerSecurityType TransportLayerSecurityType { get; init; }
/// <summary>
/// 将配置写入到 StreamSetting JSON 对象中.
/// </summary>
/// <param name="streamSettingJson">现有的 StreamSetting JSON 对象.</param>
public abstract void ApplyJsonSetting(JsonObject streamSettingJson);
}
@@ -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");
}
}
@@ -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);
}
}
@@ -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<string> 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");
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace Proxd_man;
public enum OutboundProtocolType
{
Http,
Socks,
Shadowsocks,
Trojan,
Vless,
VMess,
WireGuard
}
+64
View File
@@ -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<ProxdConfig>(configuration.GetSection("Proxd"))
.AddSingleton<XrayCoreManager>()
.AddSingleton<SubscribeManager>()
.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
var xrayCoreManager = serviceProvider.GetRequiredService<XrayCoreManager>();
await xrayCoreManager.InitialXrayCoreProgram();
var subscribeManager = serviceProvider.GetRequiredService<SubscribeManager>();
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!");
}
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Proxd_man</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
</ItemGroup>
</Project>
+203
View File
@@ -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;
/// <summary>
/// Shadowsocks 分享链接解析器.
///
/// 遵循了 Shadowsocks 的 <a href="https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme"> SIP002 URI Scheme 规范</a>.
/// </summary>
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
};
}
}
/// <summary>
/// Shadowsocks 分享链接解析器.
///
/// 这个版本使用 Uri 进行解析, 而不是手搓解析过程.
///
/// 遵循了 Shadowsocks 的 <a href="https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme"> SIP002 URI Scheme 规范</a>.
/// </summary>
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();
}
}
+47
View File
@@ -0,0 +1,47 @@
using Proxd_man.Config.Outbound;
namespace Proxd_man.ShareLink;
public class ShareLinkParser
{
private static readonly Lazy<List<IShareLinkParser>> 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
{
/// <summary>
/// 检查该分享链接是否可以被该实现解析.
///
/// 该方法只需要检查是否可以解析, 并不需要检查是否正确.
/// </summary>
/// <param name="shareLink">要尝试解析的出站配置分享链接.</param>
/// <returns>如果可以被实现解析, 则返回 true.</returns>
bool CanParse(string shareLink);
/// <summary>
/// 将分享链接解析为出站配置.
/// </summary>
/// <param name="shareLink">要解析的分享链接.</param>
/// <returns>返回分享链接解析得出的出站配置.</returns>
/// <exception cref="InvalidShareLinkException">当分享链接错误导致无法解析时抛出异常.</exception>
BaseOutboundSetting Parse(string shareLink);
}
public class InvalidShareLinkException(string message, Exception? cause = null) : Exception(message, cause);
+14
View File
@@ -0,0 +1,14 @@
namespace Proxd_man;
public enum StreamType
{
/// <summary>
/// 直接通过 TCP 发送数据, 不进行处理.
/// </summary>
Raw,
XHttp,
Kcp,
GRpc,
WebSocket,
HttpUpgrade
}
+116
View File
@@ -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<SubscribeSource, List<BaseOutboundSetting>> _subscribeOutbounds = new();
private CancellationTokenSource? _cts;
private readonly ILogger<SubscribeManager> _logger;
private readonly IOptionsMonitor<ProxdConfig> _config;
public SubscribeManager(ILogger<SubscribeManager> logger, IOptionsMonitor<ProxdConfig> config)
{
_logger = logger;
_config = config;
_config.OnChange((_, _) => Task.Run(async () => await UpdateSubscribes()));
}
public delegate void SubscribeUpdatedEventHandler(IDictionary<SubscribeSource, List<BaseOutboundSetting>> 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<SubscribeSource, List<BaseOutboundSetting>>();
for (var i = 0; i < Config.Subscribe.Subscribes.Count; i++)
{
var subscribe = Config.Subscribe.Subscribes[i];
var outbounds = new List<BaseOutboundSetting>();
_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<SubscribeSource, List<BaseOutboundSetting>> 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();
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace Proxd_man;
public enum TransportLayerSecurityType
{
None,
Tls,
Reality,
}
+300
View File
@@ -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<XrayCoreManager> _logger;
private readonly string _coreProgramDir;
private readonly string _workingDirectory;
public XrayCoreManager(
ILogger<XrayCoreManager> 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
}
};
}
/// <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;
}
}
+30
View File
@@ -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": ""
}
]
}
}
}