feat: 目前完成了订阅功能和 Xray-Core 的更新管理.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
bin/
|
||||
obj/
|
||||
.idea/
|
||||
*.sln.DotSettings.user
|
||||
|
||||
run/
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Proxd_man;
|
||||
|
||||
public enum OutboundProtocolType
|
||||
{
|
||||
Http,
|
||||
Socks,
|
||||
Shadowsocks,
|
||||
Trojan,
|
||||
Vless,
|
||||
VMess,
|
||||
WireGuard
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Proxd_man;
|
||||
|
||||
public enum StreamType
|
||||
{
|
||||
/// <summary>
|
||||
/// 直接通过 TCP 发送数据, 不进行处理.
|
||||
/// </summary>
|
||||
Raw,
|
||||
XHttp,
|
||||
Kcp,
|
||||
GRpc,
|
||||
WebSocket,
|
||||
HttpUpgrade
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Proxd_man;
|
||||
|
||||
public enum TransportLayerSecurityType
|
||||
{
|
||||
None,
|
||||
Tls,
|
||||
Reality,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user