using Microsoft.Win32; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; namespace FoxBrowser.Core; public enum VpnConnectionState { Disconnected, Connecting, Connected, Error } public sealed class VpnProfile { public VpnProfile(string path) { FilePath = path; Name = Path.GetFileNameWithoutExtension(path); } public string Name { get; } public string FilePath { get; } } public sealed class VpnManager { private readonly string _profileDirectory = Path.Combine(AppContext.BaseDirectory, "VPN"); private string? _activeTunnel; public VpnManager() { Directory.CreateDirectory(_profileDirectory); Profiles = new ObservableCollection(); RefreshProfiles(); } public ObservableCollection Profiles { get; } public VpnConnectionState State { get; private set; } = VpnConnectionState.Disconnected; public string? LastError { get; private set; } public VpnProfile? ActiveProfile { get; private set; } public bool IsConnected => State == VpnConnectionState.Connected; public string? WireGuardPath => FindWireGuard(); public event EventHandler? StateChanged; public void RefreshProfiles() { Profiles.Clear(); foreach (var path in Directory.EnumerateFiles(_profileDirectory, "*.conf")) Profiles.Add(new VpnProfile(path)); } public VpnProfile ImportTunnelConfig(string confFilePath) { if (!File.Exists(confFilePath)) throw new FileNotFoundException("Конфігурацію WireGuard не знайдено.", confFilePath); if (!string.Equals(Path.GetExtension(confFilePath), ".conf", StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Потрібен файл конфігурації з розширенням .conf."); var safeName = Path.GetFileNameWithoutExtension(confFilePath); foreach (var invalid in Path.GetInvalidFileNameChars()) safeName = safeName.Replace(invalid, '_'); var destination = Path.Combine(_profileDirectory, safeName + ".conf"); File.Copy(confFilePath, destination, true); RefreshProfiles(); return Profiles.First(x => string.Equals(x.FilePath, destination, StringComparison.OrdinalIgnoreCase)); } public async Task ConnectAsync(VpnProfile profile, CancellationToken cancellationToken = default) { if (State == VpnConnectionState.Connecting) return; var wireGuard = FindWireGuard(); if (wireGuard is null) { State = VpnConnectionState.Error; LastError = "WireGuard for Windows не встановлено. Завантажте його з https://www.wireguard.com/install/"; StateChanged?.Invoke(this, EventArgs.Empty); throw new InvalidOperationException(LastError); } State = VpnConnectionState.Connecting; LastError = null; ActiveProfile = profile; _activeTunnel = profile.Name; StateChanged?.Invoke(this, EventArgs.Empty); try { var result = await RunAsync(wireGuard, cancellationToken, "/installtunnelservice", profile.FilePath); if (result.ExitCode != 0) throw new InvalidOperationException(string.IsNullOrWhiteSpace(result.Error) ? result.Output : result.Error); State = VpnConnectionState.Connected; } catch (Exception ex) { State = VpnConnectionState.Error; LastError = ex.Message; throw; } finally { StateChanged?.Invoke(this, EventArgs.Empty); } } public async Task DisconnectAsync(CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(_activeTunnel)) return; var wireGuard = FindWireGuard(); if (wireGuard is null) throw new InvalidOperationException("WireGuard for Windows не встановлено."); var result = await RunAsync(wireGuard, cancellationToken, "/uninstalltunnelservice", _activeTunnel); if (result.ExitCode != 0) { State = VpnConnectionState.Error; LastError = string.IsNullOrWhiteSpace(result.Error) ? result.Output : result.Error; StateChanged?.Invoke(this, EventArgs.Empty); throw new InvalidOperationException(LastError); } _activeTunnel = null; ActiveProfile = null; State = VpnConnectionState.Disconnected; LastError = null; StateChanged?.Invoke(this, EventArgs.Empty); } private static async Task<(int ExitCode, string Output, string Error)> RunAsync(string executable, CancellationToken cancellationToken, params object[] arguments) { using var process = new Process { StartInfo = new ProcessStartInfo { FileName = executable, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, } }; foreach (var argument in arguments) process.StartInfo.ArgumentList.Add(Convert.ToString(argument) ?? string.Empty); process.Start(); var outputTask = process.StandardOutput.ReadToEndAsync(); var errorTask = process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(cancellationToken); return (process.ExitCode, await outputTask, await errorTask); } private static string? FindWireGuard() { var candidates = new[] { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WireGuard", "wireguard.exe"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "WireGuard", "wireguard.exe"), }; foreach (var candidate in candidates) if (File.Exists(candidate)) return candidate; using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\wireguard.exe"); var registered = key?.GetValue(null) as string; if (!string.IsNullOrWhiteSpace(registered) && File.Exists(registered)) return registered; foreach (var segment in (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(Path.PathSeparator)) { var candidate = Path.Combine(segment.Trim(), "wireguard.exe"); try { if (File.Exists(candidate)) return candidate; } catch { /* Ignore unreadable PATH entries. */ } } return null; } }