LumaTunnel
【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。
关注
0
Fork
0
Star
0
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);
}
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);
}