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 System.Diagnostics;
using System.IO.Pipes;
using LumaTunnel.Client.Interface;
using LumaTunnel.Client.Profiles;
using LumaTunnel.Shared.Windows;
using XFEExtension.NetCore.WinUIHelper.Implements.Services;

namespace LumaTunnel.Client.Implements;

public sealed class SystemProxyService(ClientProfileStore profileStore) : GlobalServiceBase, ISystemProxyService, IDisposable
{
    private readonly string _snapshotPath = Path.Combine(profileStore.StatePath, "system-proxy-snapshot.json");
    private NamedPipeServerStream? _watchdogPipe;
    private Process? _watchdog;
    private SystemProxySnapshot? _snapshot;
    private string? _ownerId;
    private int _httpPort;

    public bool IsEnabled { get; private set; }

    public async Task EnableAsync(int httpPort, CancellationToken cancellationToken = default)
    {
        if (IsEnabled)
            return;
        _ownerId = Guid.NewGuid().ToString("N");
        _httpPort = httpPort;
        _snapshot = SystemProxyRegistry.Capture(_ownerId);
        await SystemProxyRegistry.SaveSnapshotAsync(_snapshotPath, _snapshot, cancellationToken).ConfigureAwait(false);
        SystemProxyRegistry.ApplyLoopbackProxy(httpPort);
        IsEnabled = true;
        StartWatchdog();
    }

    public async Task<bool> DisableAsync(CancellationToken cancellationToken = default)
    {
        if (!IsEnabled && _snapshot is null)
            return false;
        var snapshot = _snapshot ?? await SystemProxyRegistry.LoadSnapshotAsync(_snapshotPath, cancellationToken).ConfigureAwait(false);
        var restored = snapshot is not null && SystemProxyRegistry.RestoreIfOwned(snapshot, _httpPort);
        IsEnabled = false;
        await SignalNormalExitAsync(cancellationToken).ConfigureAwait(false);
        _snapshot = null;
        if (File.Exists(_snapshotPath)) File.Delete(_snapshotPath);
        return restored;
    }

    public bool EmergencyRestore()
    {
        try
        {
            var snapshot = _snapshot ?? SystemProxyRegistry.LoadSnapshotAsync(_snapshotPath).GetAwaiter().GetResult();
            return snapshot is not null && SystemProxyRegistry.RestoreIfOwned(snapshot, _httpPort == 0 ? AppServices.Profile.HttpPort : _httpPort);
        }
        catch
        {
            return false;
        }
    }

    public async Task<bool> RecoverStaleSnapshotAsync(int expectedHttpPort, CancellationToken cancellationToken = default)
    {
        var snapshot = await SystemProxyRegistry.LoadSnapshotAsync(_snapshotPath, cancellationToken).ConfigureAwait(false);
        if (snapshot is null) return false;
        var restored = SystemProxyRegistry.RestoreIfOwned(snapshot, expectedHttpPort);
        if (restored && File.Exists(_snapshotPath)) File.Delete(_snapshotPath);
        return restored;
    }

    private void StartWatchdog()
    {
        var executable = Path.Combine(AppContext.BaseDirectory, "LumaTunnel.Watchdog.exe");
        if (!File.Exists(executable) || _ownerId is null)
            return;
        var pipeName = "LumaTunnel.Watchdog." + _ownerId;
        _watchdogPipe = new NamedPipeServerStream(pipeName, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
        _ = _watchdogPipe.WaitForConnectionAsync();
        var startInfo = new ProcessStartInfo(executable) { UseShellExecute = false, CreateNoWindow = true };
        foreach (var argument in new[] { "--parent", Environment.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture), "--pipe", pipeName, "--snapshot", _snapshotPath, "--owner", _ownerId, "--port", _httpPort.ToString(System.Globalization.CultureInfo.InvariantCulture) })
            startInfo.ArgumentList.Add(argument);
        _watchdog = Process.Start(startInfo);
    }

    private async Task SignalNormalExitAsync(CancellationToken cancellationToken)
    {
        try
        {
            if (_watchdogPipe is { IsConnected: true } pipe)
            {
                await pipe.WriteAsync("normal"u8.ToArray(), cancellationToken).ConfigureAwait(false);
                await pipe.FlushAsync(cancellationToken).ConfigureAwait(false);
            }
        }
        catch (IOException)
        {
        }
        finally
        {
            _watchdogPipe?.Dispose();
            _watchdogPipe = null;
            _watchdog?.Dispose();
            _watchdog = null;
        }
    }

    public void Dispose()
    {
        _watchdogPipe?.Dispose();
        _watchdog?.Dispose();
        GC.SuppressFinalize(this);
    }
}