Skip to content

Commit 589f21d

Browse files
committed
Switch NRPT logic to use Registry instead of Powershell [VPNWIN-3001]
1 parent ed46537 commit 589f21d

File tree

12 files changed

+222
-152
lines changed

12 files changed

+222
-152
lines changed

src/Client/Logic/Feedback/ProtonVPN.Client.Logic.Feedback/Diagnostics/Logs/InstalledAppsLog.cs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,27 @@ private string GenerateContent()
5757
private List<string> GetAppNames()
5858
{
5959
List<string> appNames = new();
60-
RegistryKey? registryFolder;
6160

62-
registryFolder = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(UNINSTALL_REGISTRY_PATH);
63-
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
64-
65-
registryFolder = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(UNINSTALL_REGISTRY_PATH);
66-
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
67-
68-
registryFolder = Registry.LocalMachine.OpenSubKey(WOW_UNINSTALL_REGISTRY_PATH);
69-
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
70-
71-
registryFolder = Registry.CurrentUser.OpenSubKey(UNINSTALL_REGISTRY_PATH);
72-
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
73-
74-
registryFolder = Registry.CurrentUser.OpenSubKey(WINDOWS_APPS_REGISTRY_PATH);
75-
appNames.AddRange(GetInstalledNames(registryFolder, ParseWindowsAppNameIfExists));
61+
using (RegistryKey? registryFolder = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(UNINSTALL_REGISTRY_PATH))
62+
{
63+
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
64+
}
65+
using (RegistryKey? registryFolder = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(UNINSTALL_REGISTRY_PATH))
66+
{
67+
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
68+
}
69+
using (RegistryKey? registryFolder = Registry.LocalMachine.OpenSubKey(WOW_UNINSTALL_REGISTRY_PATH))
70+
{
71+
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
72+
}
73+
using (RegistryKey? registryFolder = Registry.CurrentUser.OpenSubKey(UNINSTALL_REGISTRY_PATH))
74+
{
75+
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
76+
}
77+
using (RegistryKey? registryFolder = Registry.CurrentUser.OpenSubKey(WINDOWS_APPS_REGISTRY_PATH))
78+
{
79+
appNames.AddRange(GetInstalledNames(registryFolder, ParseWindowsAppNameIfExists));
80+
}
7681

7782
return appNames;
7883
}
@@ -88,13 +93,15 @@ private IList<string> GetInstalledNames(RegistryKey? registryFolder, Func<Regist
8893

8994
foreach (string registryFolderChild in registryFolder.GetSubKeyNames())
9095
{
91-
RegistryKey? subKey = registryFolder.OpenSubKey(registryFolderChild);
92-
if (subKey != null)
96+
using (RegistryKey? subKey = registryFolder.OpenSubKey(registryFolderChild))
9397
{
94-
string name = installedNameParser(subKey);
95-
if (name != null)
98+
if (subKey != null)
9699
{
97-
names.Add(name);
100+
string name = installedNameParser(subKey);
101+
if (name != null)
102+
{
103+
names.Add(name);
104+
}
98105
}
99106
}
100107
}

src/Client/Settings/ProtonVPN.Client.Settings.Contracts/Helpers/DefaultAppsHelper.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,19 @@ private static IEnumerable<SplitTunnelingApp> GetClientApps(RegistryKey? parentK
8787
{
8888
return FailSafeRegistryAccess(() =>
8989
{
90-
RegistryKey? subKey = parentKey?.OpenSubKey(subKeyName);
91-
return GetClientApps(subKey);
90+
using (RegistryKey? subKey = parentKey?.OpenSubKey(subKeyName))
91+
{
92+
return GetClientApps(subKey);
93+
}
9294
}, Enumerable.Empty<SplitTunnelingApp>());
9395
}
9496

95-
private static IEnumerable<SplitTunnelingApp> GetClientApps(RegistryKey? parentKey)
97+
private static List<SplitTunnelingApp> GetClientApps(RegistryKey? parentKey)
9698
{
99+
List<SplitTunnelingApp> list = [];
97100
if (parentKey == null)
98101
{
99-
yield break;
102+
return list;
100103
}
101104

102105
string[] subKeys = parentKey.GetSubKeyNames();
@@ -105,9 +108,11 @@ private static IEnumerable<SplitTunnelingApp> GetClientApps(RegistryKey? parentK
105108
SplitTunnelingApp? app = GetClientApp(parentKey, subKey);
106109
if (app != null)
107110
{
108-
yield return app.Value;
111+
list.Add(app.Value);
109112
}
110113
}
114+
115+
return list;
111116
}
112117

113118
private static SplitTunnelingApp? GetClientApp(RegistryKey parentKey, string subKeyName)

src/OperatingSystems/NameResolutionPolicyTable/ProtonVPN.OperatingSystems.NRPT/NrptInvoker.cs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20-
using System.Management.Automation;
21-
using System.Text;
2220
using ProtonVPN.IssueReporting.Contracts;
2321
using ProtonVPN.Logging.Contracts;
2422
using ProtonVPN.Logging.Contracts.Events.OperatingSystemLogs;
@@ -46,24 +44,19 @@ public bool CreateRule(string nameServers)
4644
return false;
4745
}
4846

49-
return StaticNrptInvoker.CreateRule(nameServers, OnNrptException, OnError, OnSuccess);
47+
return StaticNrptInvoker.CreateRule(nameServers, OnException, OnCreateError, OnSuccess);
5048
}
5149

52-
private void OnNrptException(string errorMessage, Exception ex)
50+
private void OnException(string errorMessage, Exception ex)
5351
{
5452
_logger.Error<OperatingSystemNrptLog>(errorMessage, ex);
5553
_issueReporter.CaptureError(new Exception(errorMessage, ex));
5654
}
5755

58-
private void OnError(string message, List<ErrorRecord> errors)
56+
private void OnCreateError(string errorMessage)
5957
{
60-
StringBuilder errorMessageBuilder = new();
61-
foreach (ErrorRecord error in errors)
62-
{
63-
_logger.Error<OperatingSystemNrptLog>($"{message}: {error}");
64-
errorMessageBuilder.AppendLine(error.ToString());
65-
}
66-
_issueReporter.CaptureError(message, errorMessageBuilder.ToString());
58+
_logger.Error<OperatingSystemNrptLog>(errorMessage);
59+
_issueReporter.CaptureError("Error when creating NRPT rule", errorMessage);
6760
}
6861

6962
private void OnSuccess(string message)
@@ -74,6 +67,6 @@ private void OnSuccess(string message)
7467
/// <returns>If the NRPT rule was removed successfully</returns>
7568
public bool DeleteRule()
7669
{
77-
return StaticNrptInvoker.DeleteRule(OnNrptException, OnError, OnSuccess);
70+
return StaticNrptInvoker.DeleteRule(OnException, OnSuccess);
7871
}
7972
}

src/OperatingSystems/NameResolutionPolicyTable/ProtonVPN.OperatingSystems.NRPT/ProtonVPN.OperatingSystems.NRPT.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
<Compile Include="..\..\..\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" />
1313
</ItemGroup>
1414
<ItemGroup>
15-
<PackageReference Include="Microsoft.PowerShell.SDK" />
1615
<PackageReference Include="System.Formats.Asn1" />
1716
<PackageReference Include="System.Text.Json" />
1817
</ItemGroup>

src/OperatingSystems/NameResolutionPolicyTable/ProtonVPN.OperatingSystems.NRPT/StaticNrptInvoker.cs

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,50 @@
1717
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20-
using System.Management.Automation;
21-
using System.Management.Automation.Runspaces;
22-
using Microsoft.PowerShell;
20+
using Microsoft.Win32;
2321

2422
namespace ProtonVPN.OperatingSystems.NRPT;
2523

2624
public static class StaticNrptInvoker
2725
{
28-
private const string NRPT_COMMENT = "Force all DNS requests via Proton VPN";
29-
private const string NRPT_ADD_SCRIPT = "Add-DnsClientNrptRule -Namespace \".\" -NameServers {0} -Comment \"" + NRPT_COMMENT + "\"";
30-
private const string NRPT_REMOVE_SCRIPT = "Get-DnsClientNrptRule | Where-Object { $_.Comment -eq \"" + NRPT_COMMENT + "\" } | Remove-DnsClientNrptRule -Force";
31-
26+
private const string NRPT_COMMENT_KEY_NAME = "Comment";
27+
private const string NRPT_DISPLAY_NAME_KEY_NAME = "DisplayName";
28+
29+
private const string NRPT_COMMENT_VALUE = "Force all DNS requests via Proton VPN";
30+
private const string NRPT_DISPLAY_NAME_VALUE = "Proton VPN";
31+
32+
private const string NRPT_RULES_PATH = @"SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig";
33+
34+
private static readonly string[] _allDomains = ["."];
3235
private static readonly object _lock = new();
3336

3437
/// <returns>If the NRPT rule was added successfully</returns>
35-
public static bool CreateRule(string nameServers, Action<string, Exception> onException = null,
36-
Action<string, List<ErrorRecord>> onError = null, Action<string> onSuccess = null)
38+
public static bool CreateRule(string nameServers, Action<string, Exception> onException, Action<string> onError, Action<string> onSuccess)
3739
{
3840
try
3941
{
40-
string script = string.Format(NRPT_ADD_SCRIPT, nameServers);
41-
return ExecuteNrptPowershellCommand("Add", ps => ps.AddScript(script), onError, onSuccess);
42+
lock (_lock)
43+
{
44+
string ruleGuid = Guid.NewGuid().ToString().ToUpper();
45+
string rulePath = $"{NRPT_RULES_PATH}\\{{{ruleGuid}}}";
46+
using (RegistryKey ruleKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).CreateSubKey(rulePath, writable: true))
47+
{
48+
if (ruleKey == null)
49+
{
50+
onError($"Failed to open or create NRPT registry path {NRPT_RULES_PATH}.");
51+
return false;
52+
}
53+
54+
ruleKey.SetValue(NRPT_COMMENT_KEY_NAME, NRPT_COMMENT_VALUE, RegistryValueKind.String);
55+
ruleKey.SetValue("ConfigOptions", 8, RegistryValueKind.DWord);
56+
ruleKey.SetValue(NRPT_DISPLAY_NAME_KEY_NAME, NRPT_DISPLAY_NAME_VALUE, RegistryValueKind.String);
57+
ruleKey.SetValue("GenericDNSServers", nameServers, RegistryValueKind.String);
58+
ruleKey.SetValue("IPSECCARestriction", string.Empty, RegistryValueKind.String);
59+
ruleKey.SetValue("Name", _allDomains, RegistryValueKind.MultiString);
60+
ruleKey.SetValue("Version", 2, RegistryValueKind.DWord);
61+
return true;
62+
}
63+
}
4264
}
4365
catch (Exception ex)
4466
{
@@ -50,54 +72,74 @@ public static bool CreateRule(string nameServers, Action<string, Exception> onEx
5072
}
5173
}
5274

53-
/// <returns>If the NRPT script was executed successfully</returns>
54-
private static bool ExecuteNrptPowershellCommand(string actionVerb, Func<PowerShell, PowerShell> value,
55-
Action<string, List<ErrorRecord>> onError = null, Action<string> onSuccess = null)
75+
/// <returns>If the NRPT rule was removed successfully</returns>
76+
public static bool DeleteRule(Action<string, Exception> onException, Action<string> onSuccess)
5677
{
57-
lock(_lock)
78+
try
5879
{
59-
InitialSessionState iss = InitialSessionState.CreateDefault();
60-
iss.ExecutionPolicy = ExecutionPolicy.Bypass;
61-
62-
using (Runspace runspace = RunspaceFactory.CreateRunspace(iss))
80+
lock (_lock)
6381
{
64-
runspace.Open();
65-
using (PowerShell ps = PowerShell.Create(runspace))
66-
{
67-
ps.AddCommand("Import-Module").AddArgument("DnsClient").Invoke();
68-
ps.Commands.Clear();
69-
70-
value(ps).Invoke();
82+
bool result = false;
7183

72-
if (ps.HadErrors)
84+
using (RegistryKey pathKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(NRPT_RULES_PATH, writable: true))
85+
{
86+
if (pathKey == null)
7387
{
74-
List<ErrorRecord> errors = ps.Streams.Error.ToList();
75-
if (onError is not null)
76-
{
77-
onError($"Error when performing NRPT rule '{actionVerb}' action.", errors);
78-
}
79-
return false;
88+
return false; // NRPT path doesn't exist, nothing to do here
8089
}
81-
else
90+
91+
string[] nrptRulesKeyNames = pathKey.GetSubKeyNames();
92+
93+
foreach (string nrptRuleKeyName in nrptRulesKeyNames)
8294
{
83-
if (onSuccess is not null)
84-
{
85-
onSuccess($"Success when performing NRPT rule '{actionVerb}' action.");
86-
}
87-
return true;
95+
result = CheckAndDeleteRule(nrptRuleKeyName, pathKey, onException, onSuccess: onSuccess) || result;
8896
}
8997
}
98+
99+
return result;
100+
}
101+
}
102+
catch (Exception ex)
103+
{
104+
if (onException is not null)
105+
{
106+
onException("Exception thrown when removing the NRPT rule", ex);
90107
}
108+
return false;
91109
}
92110
}
93111

94-
/// <returns>If the NRPT rule was removed successfully</returns>
95-
public static bool DeleteRule(Action<string, Exception> onException = null,
96-
Action<string, List<ErrorRecord>> onError = null, Action<string> onSuccess = null)
112+
/// <returns>Was the NRPT rule removed successfully (Failure doesn't mean anything wrong, might not be our rule)</returns>
113+
private static bool CheckAndDeleteRule(string nrptRuleKeyName, RegistryKey pathKey,
114+
Action<string, Exception> onException, Action<string> onSuccess)
97115
{
98116
try
99117
{
100-
return ExecuteNrptPowershellCommand("Remove", ps => ps.AddScript(NRPT_REMOVE_SCRIPT), onError, onSuccess);
118+
using (RegistryKey nrptRuleKey = pathKey.OpenSubKey(nrptRuleKeyName))
119+
{
120+
if (nrptRuleKey == null)
121+
{
122+
return false; // NRPT rule key name doesn't exist
123+
}
124+
125+
object displayNameObj = nrptRuleKey.GetValue(NRPT_DISPLAY_NAME_KEY_NAME);
126+
string displayName = displayNameObj?.ToString();
127+
if (displayName is not null && displayName.Equals(NRPT_DISPLAY_NAME_VALUE, StringComparison.InvariantCultureIgnoreCase))
128+
{
129+
DeleteKey(pathKey, nrptRuleKeyName, onSuccess);
130+
return true;
131+
}
132+
133+
object commentObj = nrptRuleKey.GetValue(NRPT_COMMENT_KEY_NAME);
134+
string comment = commentObj?.ToString();
135+
if (comment is not null && comment.Equals(NRPT_COMMENT_VALUE, StringComparison.InvariantCultureIgnoreCase))
136+
{
137+
DeleteKey(pathKey, nrptRuleKeyName, onSuccess);
138+
return true;
139+
}
140+
}
141+
142+
return false; // This NRPT rule exists but is not ours, leave it as is
101143
}
102144
catch (Exception ex)
103145
{
@@ -108,4 +150,10 @@ public static bool DeleteRule(Action<string, Exception> onException = null,
108150
return false;
109151
}
110152
}
153+
154+
private static void DeleteKey(RegistryKey pathKey, string nrptRuleKeyName, Action<string> onSuccess)
155+
{
156+
pathKey.DeleteSubKey(nrptRuleKeyName);
157+
onSuccess($"Successfully deleted the NRPT rule '{nrptRuleKeyName}'.");
158+
}
111159
}

src/ProtonVPN.Common.Legacy/OS/Registry/SystemProxy.cs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,30 @@
1717
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20+
using Microsoft.Win32;
21+
2022
namespace ProtonVPN.Common.Legacy.OS.Registry;
2123

2224
public class SystemProxy : ISystemProxy
2325
{
24-
private const string RegKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
26+
private const string REG_KEY = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
2527

2628
public bool Enabled()
2729
{
28-
var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(RegKey, false);
29-
if (key == null)
30+
using (RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(REG_KEY, false))
3031
{
31-
return false;
32-
}
32+
if (key == null)
33+
{
34+
return false;
35+
}
3336

34-
var value = key.GetValue("ProxyEnable") as int?;
35-
if (value == null)
36-
{
37-
return false;
38-
}
37+
int? value = key.GetValue("ProxyEnable") as int?;
38+
if (value == null)
39+
{
40+
return false;
41+
}
3942

40-
return value == 1;
43+
return value == 1;
44+
}
4145
}
4246
}

0 commit comments

Comments
 (0)