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 = ";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 LoadSnapshotAsync(string path, CancellationToken cancellationToken = default) { if (!File.Exists(path)) return null; await using var stream = File.OpenRead(path); return await JsonSerializer.DeserializeAsync(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); }