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.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.Json;
using LumaTunnel.Shared.Protocol;
using Microsoft.Win32;

namespace LumaTunnel.Shared.Windows;

[SupportedOSPlatform("windows")]
public static class SystemProxyRegistry
{
    private const string InternetSettingsKey = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
    private const string ConnectionsKey = InternetSettingsKey + @"\Connections";
    private const string OwnedProxyOverride = "<local>;localhost;127.*;[::1]";

    public static SystemProxySnapshot Capture(string ownerId)
    {
        EnsureWindows();
        using var key = Registry.CurrentUser.OpenSubKey(InternetSettingsKey, false)
                        ?? throw new InvalidOperationException("Unable to open the current user's Internet Settings registry key.");
        using var connections = Registry.CurrentUser.OpenSubKey(ConnectionsKey, false);
        return new SystemProxySnapshot
        {
            ProxyEnable = Convert.ToInt32(key.GetValue("ProxyEnable", 0), System.Globalization.CultureInfo.InvariantCulture),
            ProxyServer = key.GetValue("ProxyServer") as string,
            ProxyOverride = key.GetValue("ProxyOverride") as string,
            AutoConfigUrl = key.GetValue("AutoConfigURL") as string,
            AutoDetect = key.GetValue("AutoDetect") is int autoDetect ? autoDetect : null,
            DefaultConnectionSettings = connections?.GetValue("DefaultConnectionSettings") as byte[],
            SavedLegacySettings = connections?.GetValue("SavedLegacySettings") as byte[],
            OwnerId = ownerId
        };
    }

    public static void ApplyLoopbackProxy(int httpPort)
    {
        EnsureWindows();
        if (httpPort is < 1 or > 65535)
            throw new ArgumentOutOfRangeException(nameof(httpPort));
        using var key = Registry.CurrentUser.OpenSubKey(InternetSettingsKey, true)
                        ?? throw new InvalidOperationException("Unable to update the current user's Internet Settings registry key.");
        key.SetValue("ProxyEnable", 1, RegistryValueKind.DWord);
        key.SetValue("ProxyServer", ExpectedProxy(httpPort), RegistryValueKind.String);
        key.SetValue("ProxyOverride", OwnedProxyOverride, RegistryValueKind.String);
        key.DeleteValue("AutoConfigURL", false);
        key.SetValue("AutoDetect", 0, RegistryValueKind.DWord);
        NotifyChanged();
    }

    public static bool RestoreIfOwned(SystemProxySnapshot snapshot, int expectedHttpPort)
    {
        ArgumentNullException.ThrowIfNull(snapshot);
        EnsureWindows();
        using var key = Registry.CurrentUser.OpenSubKey(InternetSettingsKey, true)
                        ?? throw new InvalidOperationException("Unable to update the current user's Internet Settings registry key.");
        var enabled = Convert.ToInt32(key.GetValue("ProxyEnable", 0), System.Globalization.CultureInfo.InvariantCulture);
        var current = key.GetValue("ProxyServer") as string;
        var bypass = key.GetValue("ProxyOverride") as string;
        var autoConfig = key.GetValue("AutoConfigURL") as string;
        var autoDetect = Convert.ToInt32(key.GetValue("AutoDetect", 0), System.Globalization.CultureInfo.InvariantCulture);
        if (enabled != 1
            || !string.Equals(current, ExpectedProxy(expectedHttpPort), StringComparison.OrdinalIgnoreCase)
            || !string.Equals(bypass, OwnedProxyOverride, StringComparison.Ordinal)
            || !string.IsNullOrEmpty(autoConfig)
            || autoDetect != 0)
            return false;

        key.SetValue("ProxyEnable", snapshot.ProxyEnable, RegistryValueKind.DWord);
        RestoreString(key, "ProxyServer", snapshot.ProxyServer);
        RestoreString(key, "ProxyOverride", snapshot.ProxyOverride);
        RestoreString(key, "AutoConfigURL", snapshot.AutoConfigUrl);
        RestoreInt(key, "AutoDetect", snapshot.AutoDetect);
        using (var connections = Registry.CurrentUser.OpenSubKey(ConnectionsKey, true))
        {
            if (connections is not null)
            {
                RestoreBinary(connections, "DefaultConnectionSettings", snapshot.DefaultConnectionSettings);
                RestoreBinary(connections, "SavedLegacySettings", snapshot.SavedLegacySettings);
            }
        }
        NotifyChanged();
        return true;
    }

    public static async Task SaveSnapshotAsync(string path, SystemProxySnapshot snapshot, CancellationToken cancellationToken = default)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Snapshot path has no parent directory."));
        var temp = path + ".tmp";
        try
        {
            await using (var stream = new FileStream(temp, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough | FileOptions.Asynchronous))
                await JsonSerializer.SerializeAsync(stream, snapshot, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false);
            File.Move(temp, path, true);
        }
        finally
        {
            if (File.Exists(temp))
                File.Delete(temp);
        }
    }

    public static async Task<SystemProxySnapshot?> LoadSnapshotAsync(string path, CancellationToken cancellationToken = default)
    {
        if (!File.Exists(path))
            return null;
        await using var stream = File.OpenRead(path);
        return await JsonSerializer.DeserializeAsync<SystemProxySnapshot>(stream, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false);
    }

    public static string ExpectedProxy(int httpPort) => $"http=127.0.0.1:{httpPort};https=127.0.0.1:{httpPort}";

    private static void RestoreString(RegistryKey key, string name, string? value)
    {
        if (value is null)
            key.DeleteValue(name, false);
        else
            key.SetValue(name, value, RegistryValueKind.String);
    }

    private static void RestoreInt(RegistryKey key, string name, int? value)
    {
        if (value is null) key.DeleteValue(name, false);
        else key.SetValue(name, value.Value, RegistryValueKind.DWord);
    }

    private static void RestoreBinary(RegistryKey key, string name, byte[]? value)
    {
        if (value is null) key.DeleteValue(name, false);
        else key.SetValue(name, value, RegistryValueKind.Binary);
    }

    private static void NotifyChanged()
    {
        _ = InternetSetOption(IntPtr.Zero, 39, IntPtr.Zero, 0);
        _ = InternetSetOption(IntPtr.Zero, 37, IntPtr.Zero, 0);
    }

    private static void EnsureWindows()
    {
        if (!OperatingSystem.IsWindows())
            throw new PlatformNotSupportedException("Windows system proxy management is only available on Windows.");
    }

    [DllImport("wininet.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool InternetSetOption(IntPtr internet, int option, IntPtr buffer, int bufferLength);
}