/* * Copyright (c) 2025 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System.Collections.Specialized; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ProtonVPN.Client.Common.Attributes; using ProtonVPN.Client.Common.Collections; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Models; using ProtonVPN.Client.Core.Services.Activation; using ProtonVPN.Client.Core.Services.Navigation; using ProtonVPN.Client.Logic.Connection.Contracts; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Client.Settings.Contracts.Models; using ProtonVPN.Client.Settings.Contracts.RequiredReconnections; using ProtonVPN.Client.UI.Main.Settings.Bases; using ProtonVPN.Client.UI.Overlays.Selection.Contracts; using ProtonVPN.Common.Core.Networking; namespace ProtonVPN.Client.UI.Main.Settings.Pages.Advanced; public partial class CustomDnsServersViewModel : SettingsPageViewModelBase { private readonly IIpSelector _ipSelector; private bool _wasIpv6WarningDisplayed; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasIpv6DnsServersWhileIpv6Disabled))] private bool _isIpv6Enabled; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasIpv6DnsServersWhileIpv6Disabled))] private bool _isCustomDnsServersEnabled; public override string Title => Localizer.Get("Settings_Connection_Advanced_CustomDnsServers"); [property: SettingName(nameof(ISettings.CustomDnsServersList))] public SmartObservableCollection DnsServers { get; } = []; public IEnumerable SelectedDnsServers => DnsServers.Where(ip => ip.IsSelected).Select(ip => ip.Value); public bool HasSelectedDnsServers => SelectedDnsServers.Any(); public string DnsServersHeader => Localizer.GetFormat("Settings_Connection_Advanced_CustomDnsServers_FormattedHeader", SelectedDnsServers.Count()); public bool HasIpv6DnsServersWhileIpv6Disabled => !IsIpv6Enabled && IsCustomDnsServersEnabled && SelectedDnsServers.Any(ip => ip.IsIpV6); public CustomDnsServersViewModel( IRequiredReconnectionSettings requiredReconnectionSettings, IMainViewNavigator mainViewNavigator, ISettingsViewNavigator settingsViewNavigator, IMainWindowOverlayActivator mainWindowOverlayActivator, ISettings settings, ISettingsConflictResolver settingsConflictResolver, IConnectionManager connectionManager, IViewModelHelper viewModelHelper, IIpSelector ipSelector) : base(requiredReconnectionSettings, mainViewNavigator, settingsViewNavigator, mainWindowOverlayActivator, settings, settingsConflictResolver, connectionManager, viewModelHelper) { _ipSelector = ipSelector; DnsServers.CollectionChanged += OnDnsServersCollectionChanged; PageSettings = [ ChangedSettingArgs.Create(() => Settings.IsCustomDnsServersEnabled, () => IsCustomDnsServersEnabled), ChangedSettingArgs.Create(() => Settings.CustomDnsServersList, () => GetSettingsCustomDnsServers()), ChangedSettingArgs.Create(() => Settings.IsIpv6Enabled, () => IsIpv6Enabled) ]; } [RelayCommand] public async Task TriggerIpv6DisabledWarningAsync() { if (await ShowIpv6DisabledWarningAsync()) { IsIpv6Enabled = true; } // Show this warning only once per app launch _wasIpv6WarningDisplayed = true; } protected override void OnRetrieveSettings() { IsIpv6Enabled = Settings.IsIpv6Enabled; IsCustomDnsServersEnabled = Settings.IsCustomDnsServersEnabled; DnsServers.Reset(GetObservableCustomDnsServers()); } protected override bool IsReconnectionRequiredDueToChanges(IEnumerable changedSettings) { bool isReconnectionRequired = base.IsReconnectionRequiredDueToChanges(changedSettings); if (isReconnectionRequired) { // Check if there was any active DNS servers from the settings // then check if there is any active DNS servers now. // If there is none in both case, no need to reconnect. bool hadAnyActiveDnsServers = Settings.IsCustomDnsServersEnabled && Settings.CustomDnsServersList.Any(s => s.IsActive); bool hasAnyActiveDnsServers = IsCustomDnsServersEnabled && HasSelectedDnsServers; if (!hadAnyActiveDnsServers && !hasAnyActiveDnsServers) { return false; } } return isReconnectionRequired; } [RelayCommand] private async Task SelectCustomDnsServersAsync() { _ipSelector.Title = Localizer.Get("Settings_Connection_Advanced_CustomDnsServers_Header"); _ipSelector.Description = Localizer.Get("Settings_Connection_Advanced_CustomDnsServers_Footer"); _ipSelector.Caption = Localizer.Get("Settings_Connection_Advanced_CustomDnsServers_AddNew"); _ipSelector.CanReorder = true; _ipSelector.IsAddressRangeAuthorized = false; List? result = await _ipSelector.SelectAsync(DnsServers.Select(ip => ip.Clone()).ToList()); if (result != null) { DnsServers.Reset(result); if (HasIpv6DnsServersWhileIpv6Disabled && !_wasIpv6WarningDisplayed) { await TriggerIpv6DisabledWarningAsync(); } } } private List GetSettingsCustomDnsServers() { return DnsServers.Select(ip => new CustomDnsServer(ip.Value.ToString(), ip.IsSelected)).ToList(); } private List GetObservableCustomDnsServers() { List addresses = []; foreach (CustomDnsServer ip in Settings.CustomDnsServersList) { if (NetworkAddress.TryParse(ip.IpAddress, out NetworkAddress address)) { addresses.Add(new SelectableNetworkAddress(address, ip.IsActive)); } } return addresses; } private void OnDnsServersCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(DnsServersHeader)); OnPropertyChanged(nameof(SelectedDnsServers)); OnPropertyChanged(nameof(HasSelectedDnsServers)); OnPropertyChanged(nameof(HasIpv6DnsServersWhileIpv6Disabled)); } }