XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

LumaTunnel

【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。

公开
关注 0 Fork 0 Star 0
UTF-8
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);
}