Skip to content

Commit a32eee1

Browse files
Mindaugas VeblauskasThomas Felices
authored andcommitted
Handle invalid IP addresses and applications for split tunneling [VPNWIN-3065]
1 parent 0953749 commit a32eee1

File tree

15 files changed

+290
-134
lines changed

15 files changed

+290
-134
lines changed

src/Client/Localization/ProtonVPN.Client.Localization/Strings/en-US/Resources.resw

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,6 +2452,9 @@
24522452
<data name="Settings_Connection_SplitTunneling_Apps_Remove" xml:space="preserve">
24532453
<value>Remove app</value>
24542454
</data>
2455+
<data name="Settings_Connection_SplitTunneling_Apps_NotFound" xml:space="preserve">
2456+
<value>Application not found</value>
2457+
</data>
24552458
<data name="Settings_Connection_SplitTunneling_Description" xml:space="preserve">
24562459
<value>Customize your connection by deciding which apps and IP addresses are protected by VPN.</value>
24572460
</data>

src/Client/ProtonVPN.Client/UI/Main/Features/SplitTunneling/SplitTunnelingWidgetViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ private async Task InvalidateAppsAndIpsAsync()
218218
_ => []
219219
};
220220

221-
foreach (SplitTunnelingApp app in apps.Where(a => a.IsActive))
221+
foreach (SplitTunnelingApp app in apps.Where(a => a.IsActive && File.Exists(a.AppFilePath)))
222222
{
223223
items.Add(await _splitTunnelingItemFactory.GetAppAsync(app, splitTunnelingMode));
224224
}

src/Client/ProtonVPN.Client/UI/Main/Settings/Pages/Connection/SplitTunnelingAppViewModel.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ public partial class SplitTunnelingAppViewModel : ViewModelBase
3131
private readonly SplitTunnelingPageViewModel _parentViewModel;
3232

3333
[ObservableProperty]
34-
private bool _isActive;
34+
private bool _isActive;
35+
36+
[ObservableProperty]
37+
private bool _isValidPath;
3538

3639
public string AppFilePath { get; }
3740

@@ -63,6 +66,8 @@ public SplitTunnelingAppViewModel(
6366

6467
AppFilePath = appFilePath;
6568
AlternateAppFilePaths = alternateAppFilePaths ?? new List<string>();
69+
70+
IsValidPath = File.Exists(AppFilePath);
6671
}
6772

6873
[RelayCommand]
@@ -73,6 +78,11 @@ public void RemoveApp()
7378

7479
public async Task InitializeAsync()
7580
{
81+
if (!IsValidPath)
82+
{
83+
return;
84+
}
85+
7686
AppName = AppFilePath.GetAppName();
7787
OnPropertyChanged(nameof(AppName));
7888

src/Client/ProtonVPN.Client/UI/Main/Settings/Pages/Connection/SplitTunnelingPageView.xaml

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -136,23 +136,39 @@ along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
136136
<ColumnDefinition Width="*" />
137137
<ColumnDefinition Width="Auto" />
138138
</Grid.ColumnDefinitions>
139-
<CheckBox Grid.Column="0"
140-
IsChecked="{x:Bind IsActive, Mode=TwoWay}"
141-
ToolTipService.ToolTip="{x:Bind AppFilePath}">
142-
<Grid ColumnSpacing="8">
143-
<Grid.ColumnDefinitions>
144-
<ColumnDefinition Width="Auto" />
145-
<ColumnDefinition Width="*" />
146-
</Grid.ColumnDefinitions>
147-
<Image Grid.Column="0"
148-
Width="20"
149-
Height="20"
150-
Source="{x:Bind AppIcon}" />
151-
<TextBlock Grid.Column="1"
152-
Text="{x:Bind AppName}"
153-
TextTrimming="CharacterEllipsis" />
154-
</Grid>
155-
</CheckBox>
139+
<Border Grid.Column="0"
140+
Background="Transparent"
141+
ToolTipService.ToolTip="{x:Bind AppFilePath}">
142+
<CheckBox IsChecked="{x:Bind IsActive, Mode=TwoWay}"
143+
IsEnabled="{x:Bind IsValidPath}">
144+
<Grid ColumnSpacing="8">
145+
<Grid.ColumnDefinitions>
146+
<ColumnDefinition Width="Auto" />
147+
<ColumnDefinition Width="*" />
148+
</Grid.ColumnDefinitions>
149+
<Image Grid.Column="0"
150+
Width="20"
151+
Height="20"
152+
Source="{x:Bind AppIcon}"
153+
Visibility="{x:Bind IsValidPath, Converter={StaticResource BooleanToVisibilityConverter}}" />
154+
<TextBlock Grid.Column="1"
155+
Text="{x:Bind AppName}"
156+
TextTrimming="CharacterEllipsis"
157+
Visibility="{x:Bind IsValidPath, Converter={StaticResource BooleanToVisibilityConverter}}" />
158+
<pathicons:ExclamationCircleFilled Grid.Column="0"
159+
Foreground="{ThemeResource SignalDangerColorBrush}"
160+
Opacity="0.6"
161+
Size="Pixels20"
162+
Visibility="{x:Bind IsValidPath, Converter={StaticResource NotBooleanToVisibilityConverter}}" />
163+
<TextBlock Grid.Column="1"
164+
Foreground="{ThemeResource SignalDangerColorBrush}"
165+
Opacity="0.6"
166+
Text="{x:Bind Localizer.Get('Settings_Connection_SplitTunneling_Apps_NotFound')}"
167+
TextTrimming="CharacterEllipsis"
168+
Visibility="{x:Bind IsValidPath, Converter={StaticResource NotBooleanToVisibilityConverter}}" />
169+
</Grid>
170+
</CheckBox>
171+
</Border>
156172
<custom:GhostButton Grid.Column="1"
157173
AutomationProperties.AutomationId="RemoveAppButton"
158174
AutomationProperties.Name="{x:Bind Localizer.Get('Settings_Connection_SplitTunneling_Apps_Remove')}"
@@ -181,23 +197,39 @@ along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
181197
<ColumnDefinition Width="*" />
182198
<ColumnDefinition Width="Auto" />
183199
</Grid.ColumnDefinitions>
184-
<CheckBox Grid.Column="0"
185-
IsChecked="{x:Bind IsActive, Mode=TwoWay}"
186-
ToolTipService.ToolTip="{x:Bind AppFilePath}">
187-
<Grid ColumnSpacing="8">
188-
<Grid.ColumnDefinitions>
189-
<ColumnDefinition Width="Auto" />
190-
<ColumnDefinition Width="*" />
191-
</Grid.ColumnDefinitions>
192-
<Image Grid.Column="0"
193-
Width="20"
194-
Height="20"
195-
Source="{x:Bind AppIcon}" />
196-
<TextBlock Grid.Column="1"
197-
Text="{x:Bind AppName}"
198-
TextTrimming="CharacterEllipsis" />
199-
</Grid>
200-
</CheckBox>
200+
<Border Grid.Column="0"
201+
Background="Transparent"
202+
ToolTipService.ToolTip="{x:Bind AppFilePath}">
203+
<CheckBox IsChecked="{x:Bind IsActive, Mode=TwoWay}"
204+
IsEnabled="{x:Bind IsValidPath}">
205+
<Grid ColumnSpacing="8">
206+
<Grid.ColumnDefinitions>
207+
<ColumnDefinition Width="Auto" />
208+
<ColumnDefinition Width="*" />
209+
</Grid.ColumnDefinitions>
210+
<Image Grid.Column="0"
211+
Width="20"
212+
Height="20"
213+
Source="{x:Bind AppIcon}"
214+
Visibility="{x:Bind IsValidPath, Converter={StaticResource BooleanToVisibilityConverter}}" />
215+
<TextBlock Grid.Column="1"
216+
Text="{x:Bind AppName}"
217+
TextTrimming="CharacterEllipsis"
218+
Visibility="{x:Bind IsValidPath, Converter={StaticResource BooleanToVisibilityConverter}}" />
219+
<pathicons:ExclamationCircleFilled Grid.Column="0"
220+
Foreground="{ThemeResource SignalDangerColorBrush}"
221+
Opacity="0.6"
222+
Size="Pixels20"
223+
Visibility="{x:Bind IsValidPath, Converter={StaticResource NotBooleanToVisibilityConverter}}" />
224+
<TextBlock Grid.Column="1"
225+
Foreground="{ThemeResource SignalDangerColorBrush}"
226+
Opacity="0.6"
227+
Text="{x:Bind Localizer.Get('Settings_Connection_SplitTunneling_Apps_NotFound')}"
228+
TextTrimming="CharacterEllipsis"
229+
Visibility="{x:Bind IsValidPath, Converter={StaticResource NotBooleanToVisibilityConverter}}" />
230+
</Grid>
231+
</CheckBox>
232+
</Border>
201233
<custom:GhostButton Grid.Column="1"
202234
AutomationProperties.AutomationId="RemoveAppButton"
203235
AutomationProperties.Name="{x:Bind Localizer.Get('Settings_Connection_SplitTunneling_Apps_Remove')}"
@@ -300,8 +332,8 @@ along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
300332
HorizontalAlignment="Left"
301333
AutomationProperties.AutomationId="ShowIpv6DisabledWarningButton"
302334
AutomationProperties.Name="{x:Bind Localizer.Get('Settings_Common_Ipv6WarningButton')}"
303-
ToolTipService.ToolTip="{x:Bind Localizer.Get('Settings_Common_Ipv6WarningButton')}"
304335
Command="{x:Bind ShowIpv6DisabledWarningCommand, Mode=OneTime}"
336+
ToolTipService.ToolTip="{x:Bind Localizer.Get('Settings_Common_Ipv6WarningButton')}"
305337
Visibility="{x:Bind IsInactiveDueToIpv6Disabled, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneTime}">
306338
<custom:GhostButton.LeftIcon>
307339
<pathicons:ExclamationCircle />
@@ -322,8 +354,8 @@ along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
322354
</ItemsRepeater>
323355

324356
<TextBlock Grid.Row="1"
325-
Style="{StaticResource CaptionStrongTextBlockStyle}"
326357
Margin="0,0,0,8"
358+
Style="{StaticResource CaptionStrongTextBlockStyle}"
327359
Text="{x:Bind ViewModel.Localizer.Get('Settings_Connection_SplitTunneling_IpAddresses_AddNew')}" />
328360

329361
<TextBox Grid.Row="2"

src/Logging/ProtonVPN.Logging.Contracts/Categories/AppServiceLogCategory.cs

Lines changed: 0 additions & 2 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 ProtonVPN.Logging.Contracts;
21-
2220
namespace ProtonVPN.Logging.Contracts.Categories
2321
{
2422
public class AppServiceLogCategory : ILogCategory
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2025 Proton AG
3+
*
4+
* This file is part of ProtonVPN.
5+
*
6+
* ProtonVPN is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* ProtonVPN is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
namespace ProtonVPN.Logging.Contracts.Categories;
21+
22+
public class SplitTunnelLogCategory : ILogCategory
23+
{
24+
public string Category => "SPLIT.TUNNEL";
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2025 Proton AG
3+
*
4+
* This file is part of ProtonVPN.
5+
*
6+
* ProtonVPN is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* ProtonVPN is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
using ProtonVPN.Logging.Contracts.Categories;
21+
22+
namespace ProtonVPN.Logging.Contracts.Events.SplitTunnelLogs;
23+
24+
public class SplitTunnelLog : LogEventBase<SplitTunnelLogCategory>
25+
{
26+
protected override string Event => null;
27+
}

src/ProtonVPN.IpFilterLib/filter.cpp

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -250,30 +250,37 @@ unsigned int IPFilterCreateAppFilter(
250250
{
251251
std::vector<ipfilter::condition::Condition> conditions{};
252252

253-
if (appIdentifier != nullptr)
253+
try
254254
{
255-
// Check if it's a file path (contains backslash or drive letter) vs package family name
256-
// Package family names have format like "PackageName_PublisherId" and don't contain path separators
257-
bool isFilePath = (wcschr(appIdentifier, L'\\') != nullptr) ||
258-
(wcschr(appIdentifier, L'/') != nullptr) ||
259-
(wcslen(appIdentifier) >= 3 && appIdentifier[1] == L':');
260-
261-
if (isFilePath)
262-
{
263-
conditions.push_back(
264-
ipfilter::condition::applicationId(
265-
ipfilter::matcher::equal(),
266-
ipfilter::value::ApplicationId::fromFilePath(appIdentifier)));
267-
}
268-
else
255+
if (appIdentifier != nullptr)
269256
{
270-
// Assume it's a package family name
271-
conditions.push_back(
272-
ipfilter::condition::packageFamilyName(
273-
ipfilter::matcher::equal(),
274-
ipfilter::value::SecurityIdentifier::fromPackageFamilyName(appIdentifier)));
257+
// Check if it's a file path (contains backslash or drive letter) vs package family name
258+
// Package family names have format like "PackageName_PublisherId" and don't contain path separators
259+
bool isFilePath = (wcschr(appIdentifier, L'\\') != nullptr) ||
260+
(wcschr(appIdentifier, L'/') != nullptr) ||
261+
(wcslen(appIdentifier) >= 3 && appIdentifier[1] == L':');
262+
263+
if (isFilePath)
264+
{
265+
conditions.push_back(
266+
ipfilter::condition::applicationId(
267+
ipfilter::matcher::equal(),
268+
ipfilter::value::ApplicationId::fromFilePath(appIdentifier)));
269+
}
270+
else
271+
{
272+
// Assume it's a package family name
273+
conditions.push_back(
274+
ipfilter::condition::packageFamilyName(
275+
ipfilter::matcher::equal(),
276+
ipfilter::value::SecurityIdentifier::fromPackageFamilyName(appIdentifier)));
277+
}
275278
}
276279
}
280+
catch (...)
281+
{
282+
return E_INVALIDARG;
283+
}
277284

278285
if (isDnsPortExcluded)
279286
{
@@ -416,24 +423,31 @@ unsigned int IPFilterCreateRemoteNetworkIPFilter(
416423
{
417424
std::vector<ipfilter::condition::Condition> conditions{};
418425

419-
if (addr->isIpv6)
426+
try
420427
{
421-
auto address = ipfilter::ip::makeAddressV6(addr->address, addr->prefix);
422-
auto networkAddrCondition = ipfilter::condition::remoteIpV6AddressWithPrefix(
423-
ipfilter::matcher::equal(),
424-
ipfilter::value::IpAddressV6WithPrefix(address));
428+
if (addr->isIpv6)
429+
{
430+
auto address = ipfilter::ip::makeAddressV6(addr->address, addr->prefix);
431+
auto networkAddrCondition = ipfilter::condition::remoteIpV6AddressWithPrefix(
432+
ipfilter::matcher::equal(),
433+
ipfilter::value::IpAddressV6WithPrefix(address));
425434

426-
conditions.push_back(networkAddrCondition);
435+
conditions.push_back(networkAddrCondition);
436+
}
437+
else
438+
{
439+
auto address = ipfilter::ip::makeAddressV4(addr->address);
440+
auto mask = ipfilter::ip::makeAddressV4(addr->mask);
441+
auto networkAddrCondition = ipfilter::condition::remoteIpNetworkAddressV4(
442+
ipfilter::matcher::equal(),
443+
ipfilter::value::IpNetworkAddressV4(address, mask));
444+
445+
conditions.push_back(networkAddrCondition);
446+
}
427447
}
428-
else
448+
catch (...)
429449
{
430-
auto address = ipfilter::ip::makeAddressV4(addr->address);
431-
auto mask = ipfilter::ip::makeAddressV4(addr->mask);
432-
auto networkAddrCondition = ipfilter::condition::remoteIpNetworkAddressV4(
433-
ipfilter::matcher::equal(),
434-
ipfilter::value::IpNetworkAddressV4(address, mask));
435-
436-
conditions.push_back(networkAddrCondition);
450+
return E_INVALIDARG;
437451
}
438452

439453
return IPFilterCreateFilter(
@@ -809,4 +823,4 @@ unsigned int PermitInboundIpv6Dhcp(
809823
},
810824
persistent,
811825
filterKey);
812-
}
826+
}

0 commit comments

Comments
 (0)