/*
* 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 Microsoft.Win32;
namespace ProtonVPN.OperatingSystems.NRPT;
public static class StaticNrptInvoker
{
private const string NRPT_COMMENT_KEY_NAME = "Comment";
private const string NRPT_DISPLAY_NAME_KEY_NAME = "DisplayName";
private const string NRPT_COMMENT_VALUE = "Force all DNS requests via Proton VPN";
private const string NRPT_DISPLAY_NAME_VALUE = "Proton VPN";
private const string NRPT_RULES_PATH = @"SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig";
private static readonly string[] _allDomains = ["."];
private static readonly object _lock = new();
/// If the NRPT rule was added successfully
public static bool CreateRule(string nameServers, Action onException, Action onError, Action onSuccess)
{
try
{
lock (_lock)
{
string ruleGuid = Guid.NewGuid().ToString().ToUpper();
string rulePath = $"{NRPT_RULES_PATH}\\{{{ruleGuid}}}";
using (RegistryKey ruleKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).CreateSubKey(rulePath, writable: true))
{
if (ruleKey == null)
{
onError($"Failed to open or create NRPT registry path {NRPT_RULES_PATH}.");
return false;
}
ruleKey.SetValue(NRPT_COMMENT_KEY_NAME, NRPT_COMMENT_VALUE, RegistryValueKind.String);
ruleKey.SetValue("ConfigOptions", 8, RegistryValueKind.DWord);
ruleKey.SetValue(NRPT_DISPLAY_NAME_KEY_NAME, NRPT_DISPLAY_NAME_VALUE, RegistryValueKind.String);
ruleKey.SetValue("GenericDNSServers", nameServers, RegistryValueKind.String);
ruleKey.SetValue("IPSECCARestriction", string.Empty, RegistryValueKind.String);
ruleKey.SetValue("Name", _allDomains, RegistryValueKind.MultiString);
ruleKey.SetValue("Version", 2, RegistryValueKind.DWord);
return true;
}
}
}
catch (Exception ex)
{
if (onException is not null)
{
onException("Exception thrown when adding the NRPT rule", ex);
}
return false;
}
}
/// If the NRPT rule was removed successfully
public static bool DeleteRule(Action onException, Action onSuccess)
{
try
{
lock (_lock)
{
bool result = false;
using (RegistryKey pathKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(NRPT_RULES_PATH, writable: true))
{
if (pathKey == null)
{
return false; // NRPT path doesn't exist, nothing to do here
}
string[] nrptRulesKeyNames = pathKey.GetSubKeyNames();
foreach (string nrptRuleKeyName in nrptRulesKeyNames)
{
result = CheckAndDeleteRule(nrptRuleKeyName, pathKey, onException, onSuccess: onSuccess) || result;
}
}
return result;
}
}
catch (Exception ex)
{
if (onException is not null)
{
onException("Exception thrown when removing the NRPT rule", ex);
}
return false;
}
}
/// Was the NRPT rule removed successfully (Failure doesn't mean anything wrong, might not be our rule)
private static bool CheckAndDeleteRule(string nrptRuleKeyName, RegistryKey pathKey,
Action onException, Action onSuccess)
{
try
{
using (RegistryKey nrptRuleKey = pathKey.OpenSubKey(nrptRuleKeyName))
{
if (nrptRuleKey == null)
{
return false; // NRPT rule key name doesn't exist
}
object displayNameObj = nrptRuleKey.GetValue(NRPT_DISPLAY_NAME_KEY_NAME);
string displayName = displayNameObj?.ToString();
if (displayName is not null && displayName.Equals(NRPT_DISPLAY_NAME_VALUE, StringComparison.InvariantCultureIgnoreCase))
{
DeleteKey(pathKey, nrptRuleKeyName, onSuccess);
return true;
}
object commentObj = nrptRuleKey.GetValue(NRPT_COMMENT_KEY_NAME);
string comment = commentObj?.ToString();
if (comment is not null && comment.Equals(NRPT_COMMENT_VALUE, StringComparison.InvariantCultureIgnoreCase))
{
DeleteKey(pathKey, nrptRuleKeyName, onSuccess);
return true;
}
}
return false; // This NRPT rule exists but is not ours, leave it as is
}
catch (Exception ex)
{
if (onException is not null)
{
onException("Exception thrown when removing the NRPT rule", ex);
}
return false;
}
}
private static void DeleteKey(RegistryKey pathKey, string nrptRuleKeyName, Action onSuccess)
{
pathKey.DeleteSubKey(nrptRuleKeyName);
onSuccess($"Successfully deleted the NRPT rule '{nrptRuleKeyName}'.");
}
}