LumaTunnel
【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。
关注
0
Fork
0
Star
0
using LumaTunnel.Client.Core.Abstractions;
using LumaTunnel.Client.Core.Networking;
using LumaTunnel.Client.Core.Nodes;
using LumaTunnel.Client.Core.Proxy;
using LumaTunnel.Client.Core.Routing;
using LumaTunnel.Client.Core.Tunnel;
using LumaTunnel.Client.Implements;
using LumaTunnel.Client.Profiles;
using LumaTunnel.Shared.Models;
using LumaTunnel.Shared.Protocol;
using XFEExtension.NetCore.WinUIHelper.Utilities;
namespace LumaTunnel.Client;
public static class AppServices
{
private static readonly SemaphoreSlim s_profileGate = new(1, 1);
public static ClientProfileStore ProfileStore { get; } = new();
public static SecretStore SecretStore { get; } = new();
public static SystemProxyService SystemProxy { get; } = new(ProfileStore);
public static StartupService Startup { get; } = new();
public static TrayService Tray { get; } = new();
public static RootNavigationService RootNavigation { get; } = new();
public static TrafficMeterService TrafficMeter { get; } = new();
public static RoutingService Routing { get; } = new();
public static TunnelService Tunnel { get; } = new();
public static DirectConnector Direct { get; } = new();
public static ProxyEngineService ProxyEngine { get; } = new(Routing, Direct, Tunnel);
public static NodeApiClient NodeApi { get; } = new();
public static NodeManagerService Nodes { get; } = new(NodeApi, Tunnel, GetTokenAsync, "0.1.0-alpha");
public static ClientProfile Profile { get; private set; } = new();
public static bool IsEnabled => ProxyEngine.IsRunning;
public static event EventHandler? StateChanged;
public static async Task InitializeAsync()
{
Profile = await ProfileStore.LoadAsync().ConfigureAwait(false);
await SystemProxy.RecoverStaleSnapshotAsync(Profile.HttpPort).ConfigureAwait(false);
Utilities.AppLog.Initialize(ProfileStore.LogPath);
Routing.Mode = Profile.Mode;
Routing.SetRules(Profile.Rules);
Nodes.SetNodes(Profile.Nodes);
ServiceManager.RegisterGlobalService(SecretStore);
ServiceManager.RegisterGlobalService(SystemProxy);
ServiceManager.RegisterGlobalService(Startup);
ServiceManager.RegisterGlobalService(Tray);
ServiceManager.RegisterGlobalService(RootNavigation);
ServiceManager.RegisterGlobalService(TrafficMeter);
}
public static async Task EnableAsync(CancellationToken cancellationToken = default)
{
if (IsEnabled) return;
var options = new ProxyEngineOptions { HttpPort = Profile.HttpPort, SocksPort = Profile.SocksPort, Mode = Profile.Mode };
await ProxyEngine.StartAsync(options, cancellationToken).ConfigureAwait(false);
try
{
if (Profile.Mode != ProxyMode.Direct)
await Nodes.ConnectBestAsync(cancellationToken).ConfigureAwait(false);
await SystemProxy.EnableAsync(Profile.HttpPort, cancellationToken).ConfigureAwait(false);
await Nodes.StartMonitoringAsync(cancellationToken).ConfigureAwait(false);
StateChanged?.Invoke(null, EventArgs.Empty);
}
catch
{
await ProxyEngine.StopAsync(cancellationToken).ConfigureAwait(false);
throw;
}
}
public static async Task DisableAsync(CancellationToken cancellationToken = default)
{
await SystemProxy.DisableAsync(cancellationToken).ConfigureAwait(false);
await Nodes.StopMonitoringAsync(cancellationToken).ConfigureAwait(false);
await ProxyEngine.StopAsync(cancellationToken).ConfigureAwait(false);
await Tunnel.DisconnectAsync(cancellationToken).ConfigureAwait(false);
StateChanged?.Invoke(null, EventArgs.Empty);
}
public static async Task<NodeProfile> EnrollNodeAsync(string baseUrl, string pairingCode, string name, CancellationToken cancellationToken = default)
{
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps)
throw new ArgumentException("节点地址必须是有效的 https:// URL。", nameof(baseUrl));
var response = await NodeApi.EnrollAsync(uri, new DeviceEnrollmentRequest(pairingCode, Profile.DeviceId, Environment.MachineName, "0.1.0-alpha"), cancellationToken).ConfigureAwait(false);
var secretReference = "node-token:" + response.NodeId;
await SecretStore.SetAsync(secretReference, response.DeviceToken, cancellationToken).ConfigureAwait(false);
var profile = new NodeProfile
{
Name = string.IsNullOrWhiteSpace(name) ? response.NodeName : name.Trim(),
ApiUri = uri,
TunnelUri = response.TunnelUri,
DeviceId = response.DeviceId,
SecretReference = secretReference,
Priority = Profile.Nodes.Count
};
await UpdateProfileAsync(value => value with { Nodes = [.. value.Nodes, profile] }, cancellationToken).ConfigureAwait(false);
return profile;
}
public static Task SetModeAsync(ProxyMode mode, CancellationToken cancellationToken = default) =>
UpdateProfileAsync(profile => profile with { Mode = mode }, cancellationToken);
public static async Task RemoveNodeAsync(Guid id, CancellationToken cancellationToken = default)
{
var node = Profile.Nodes.FirstOrDefault(item => item.Id == id);
if (node is null) return;
if (node.SecretReference is not null)
await SecretStore.RemoveAsync(node.SecretReference, cancellationToken).ConfigureAwait(false);
await UpdateProfileAsync(profile => profile with { Nodes = profile.Nodes.Where(item => item.Id != id).ToList() }, cancellationToken).ConfigureAwait(false);
}
public static Task RenameNodeAsync(Guid id, string name, CancellationToken cancellationToken = default) =>
UpdateProfileAsync(profile => profile with
{
Nodes = profile.Nodes.Select(node => node.Id == id ? node with { Name = name.Trim() } : node).ToList()
}, cancellationToken);
public static async Task<double> TestNodeAsync(Guid id, CancellationToken cancellationToken = default)
{
var node = Profile.Nodes.FirstOrDefault(item => item.Id == id) ?? throw new KeyNotFoundException("Node not found.");
var (_, latency) = await NodeApi.GetHealthAsync(node.ApiUri, cancellationToken).ConfigureAwait(false);
await UpdateProfileAsync(profile => profile with
{
Nodes = profile.Nodes.Select(item => item.Id == id ? item with { LastLatencyMilliseconds = latency.TotalMilliseconds, LastHealthCheckUtc = DateTimeOffset.UtcNow } : item).ToList()
}, cancellationToken).ConfigureAwait(false);
return latency.TotalMilliseconds;
}
public static Task SetRulesAsync(IEnumerable<RoutingRule> rules, CancellationToken cancellationToken = default)
{
var list = rules.OrderBy(static rule => rule.Priority).ToList();
Routing.SetRules(list);
return UpdateProfileAsync(profile => profile with { Rules = list }, cancellationToken);
}
public static Task UpdateSettingsAsync(int httpPort, int socksPort, bool closeToTray, bool startAtLogin, string theme, CancellationToken cancellationToken = default)
{
Startup.SetEnabled(startAtLogin);
return UpdateProfileAsync(profile => profile with
{
HttpPort = httpPort,
SocksPort = socksPort,
CloseToTray = closeToTray,
StartAtLogin = startAtLogin,
Theme = theme
}, cancellationToken);
}
public static async Task ShutdownAsync()
{
try { await DisableAsync().ConfigureAwait(false); } catch { SystemProxy.EmergencyRestore(); }
Tray.Dispose();
await Nodes.DisposeAsync().ConfigureAwait(false);
await ProxyEngine.DisposeAsync().ConfigureAwait(false);
await Tunnel.DisposeAsync().ConfigureAwait(false);
SystemProxy.Dispose();
ProfileStore.Dispose();
}
private static async Task UpdateProfileAsync(Func<ClientProfile, ClientProfile> update, CancellationToken cancellationToken)
{
await s_profileGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
Profile = update(Profile);
Routing.Mode = Profile.Mode;
Nodes.SetNodes(Profile.Nodes);
await ProfileStore.SaveAsync(Profile, cancellationToken).ConfigureAwait(false);
}
finally
{
s_profileGate.Release();
}
StateChanged?.Invoke(null, EventArgs.Empty);
}
private static Task<string?> GetTokenAsync(NodeProfile profile, CancellationToken cancellationToken) =>
profile.SecretReference is null ? Task.FromResult<string?>(null) : SecretStore.GetAsync(profile.SecretReference, cancellationToken);
}
using LumaTunnel.Client.Core.Abstractions;
using LumaTunnel.Client.Core.Networking;
using LumaTunnel.Client.Core.Nodes;
using LumaTunnel.Client.Core.Proxy;
using LumaTunnel.Client.Core.Routing;
using LumaTunnel.Client.Core.Tunnel;
using LumaTunnel.Client.Implements;
using LumaTunnel.Client.Profiles;
using LumaTunnel.Shared.Models;
using LumaTunnel.Shared.Protocol;
using XFEExtension.NetCore.WinUIHelper.Utilities;
namespace LumaTunnel.Client;
public static class AppServices
{
private static readonly SemaphoreSlim s_profileGate = new(1, 1);
public static ClientProfileStore ProfileStore { get; } = new();
public static SecretStore SecretStore { get; } = new();
public static SystemProxyService SystemProxy { get; } = new(ProfileStore);
public static StartupService Startup { get; } = new();
public static TrayService Tray { get; } = new();
public static RootNavigationService RootNavigation { get; } = new();
public static TrafficMeterService TrafficMeter { get; } = new();
public static RoutingService Routing { get; } = new();
public static TunnelService Tunnel { get; } = new();
public static DirectConnector Direct { get; } = new();
public static ProxyEngineService ProxyEngine { get; } = new(Routing, Direct, Tunnel);
public static NodeApiClient NodeApi { get; } = new();
public static NodeManagerService Nodes { get; } = new(NodeApi, Tunnel, GetTokenAsync, "0.1.0-alpha");
public static ClientProfile Profile { get; private set; } = new();
public static bool IsEnabled => ProxyEngine.IsRunning;
public static event EventHandler? StateChanged;
public static async Task InitializeAsync()
{
Profile = await ProfileStore.LoadAsync().ConfigureAwait(false);
await SystemProxy.RecoverStaleSnapshotAsync(Profile.HttpPort).ConfigureAwait(false);
Utilities.AppLog.Initialize(ProfileStore.LogPath);
Routing.Mode = Profile.Mode;
Routing.SetRules(Profile.Rules);
Nodes.SetNodes(Profile.Nodes);
ServiceManager.RegisterGlobalService(SecretStore);
ServiceManager.RegisterGlobalService(SystemProxy);
ServiceManager.RegisterGlobalService(Startup);
ServiceManager.RegisterGlobalService(Tray);
ServiceManager.RegisterGlobalService(RootNavigation);
ServiceManager.RegisterGlobalService(TrafficMeter);
}
public static async Task EnableAsync(CancellationToken cancellationToken = default)
{
if (IsEnabled) return;
var options = new ProxyEngineOptions { HttpPort = Profile.HttpPort, SocksPort = Profile.SocksPort, Mode = Profile.Mode };
await ProxyEngine.StartAsync(options, cancellationToken).ConfigureAwait(false);
try
{
if (Profile.Mode != ProxyMode.Direct)
await Nodes.ConnectBestAsync(cancellationToken).ConfigureAwait(false);
await SystemProxy.EnableAsync(Profile.HttpPort, cancellationToken).ConfigureAwait(false);
await Nodes.StartMonitoringAsync(cancellationToken).ConfigureAwait(false);
StateChanged?.Invoke(null, EventArgs.Empty);
}
catch
{
await ProxyEngine.StopAsync(cancellationToken).ConfigureAwait(false);
throw;
}
}
public static async Task DisableAsync(CancellationToken cancellationToken = default)
{
await SystemProxy.DisableAsync(cancellationToken).ConfigureAwait(false);
await Nodes.StopMonitoringAsync(cancellationToken).ConfigureAwait(false);
await ProxyEngine.StopAsync(cancellationToken).ConfigureAwait(false);
await Tunnel.DisconnectAsync(cancellationToken).ConfigureAwait(false);
StateChanged?.Invoke(null, EventArgs.Empty);
}
public static async Task<NodeProfile> EnrollNodeAsync(string baseUrl, string pairingCode, string name, CancellationToken cancellationToken = default)
{
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps)
throw new ArgumentException("节点地址必须是有效的 https:// URL。", nameof(baseUrl));
var response = await NodeApi.EnrollAsync(uri, new DeviceEnrollmentRequest(pairingCode, Profile.DeviceId, Environment.MachineName, "0.1.0-alpha"), cancellationToken).ConfigureAwait(false);
var secretReference = "node-token:" + response.NodeId;
await SecretStore.SetAsync(secretReference, response.DeviceToken, cancellationToken).ConfigureAwait(false);
var profile = new NodeProfile
{
Name = string.IsNullOrWhiteSpace(name) ? response.NodeName : name.Trim(),
ApiUri = uri,
TunnelUri = response.TunnelUri,
DeviceId = response.DeviceId,
SecretReference = secretReference,
Priority = Profile.Nodes.Count
};
await UpdateProfileAsync(value => value with { Nodes = [.. value.Nodes, profile] }, cancellationToken).ConfigureAwait(false);
return profile;
}
public static Task SetModeAsync(ProxyMode mode, CancellationToken cancellationToken = default) =>
UpdateProfileAsync(profile => profile with { Mode = mode }, cancellationToken);
public static async Task RemoveNodeAsync(Guid id, CancellationToken cancellationToken = default)
{
var node = Profile.Nodes.FirstOrDefault(item => item.Id == id);
if (node is null) return;
if (node.SecretReference is not null)
await SecretStore.RemoveAsync(node.SecretReference, cancellationToken).ConfigureAwait(false);
await UpdateProfileAsync(profile => profile with { Nodes = profile.Nodes.Where(item => item.Id != id).ToList() }, cancellationToken).ConfigureAwait(false);
}
public static Task RenameNodeAsync(Guid id, string name, CancellationToken cancellationToken = default) =>
UpdateProfileAsync(profile => profile with
{
Nodes = profile.Nodes.Select(node => node.Id == id ? node with { Name = name.Trim() } : node).ToList()
}, cancellationToken);
public static async Task<double> TestNodeAsync(Guid id, CancellationToken cancellationToken = default)
{
var node = Profile.Nodes.FirstOrDefault(item => item.Id == id) ?? throw new KeyNotFoundException("Node not found.");
var (_, latency) = await NodeApi.GetHealthAsync(node.ApiUri, cancellationToken).ConfigureAwait(false);
await UpdateProfileAsync(profile => profile with
{
Nodes = profile.Nodes.Select(item => item.Id == id ? item with { LastLatencyMilliseconds = latency.TotalMilliseconds, LastHealthCheckUtc = DateTimeOffset.UtcNow } : item).ToList()
}, cancellationToken).ConfigureAwait(false);
return latency.TotalMilliseconds;
}
public static Task SetRulesAsync(IEnumerable<RoutingRule> rules, CancellationToken cancellationToken = default)
{
var list = rules.OrderBy(static rule => rule.Priority).ToList();
Routing.SetRules(list);
return UpdateProfileAsync(profile => profile with { Rules = list }, cancellationToken);
}
public static Task UpdateSettingsAsync(int httpPort, int socksPort, bool closeToTray, bool startAtLogin, string theme, CancellationToken cancellationToken = default)
{
Startup.SetEnabled(startAtLogin);
return UpdateProfileAsync(profile => profile with
{
HttpPort = httpPort,
SocksPort = socksPort,
CloseToTray = closeToTray,
StartAtLogin = startAtLogin,
Theme = theme
}, cancellationToken);
}
public static async Task ShutdownAsync()
{
try { await DisableAsync().ConfigureAwait(false); } catch { SystemProxy.EmergencyRestore(); }
Tray.Dispose();
await Nodes.DisposeAsync().ConfigureAwait(false);
await ProxyEngine.DisposeAsync().ConfigureAwait(false);
await Tunnel.DisposeAsync().ConfigureAwait(false);
SystemProxy.Dispose();
ProfileStore.Dispose();
}
private static async Task UpdateProfileAsync(Func<ClientProfile, ClientProfile> update, CancellationToken cancellationToken)
{
await s_profileGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
Profile = update(Profile);
Routing.Mode = Profile.Mode;
Nodes.SetNodes(Profile.Nodes);
await ProfileStore.SaveAsync(Profile, cancellationToken).ConfigureAwait(false);
}
finally
{
s_profileGate.Release();
}
StateChanged?.Invoke(null, EventArgs.Empty);
}
private static Task<string?> GetTokenAsync(NodeProfile profile, CancellationToken cancellationToken) =>
profile.SecretReference is null ? Task.FromResult<string?>(null) : SecretStore.GetAsync(profile.SecretReference, cancellationToken);
}