/* * 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; using System.Net; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.NetworkFilter; using ProtonVPN.OperatingSystems.Network.Contracts; using ProtonVPN.ProcessCommunication.Contracts.Entities.Settings; using ProtonVPN.ProcessCommunication.Contracts.Entities.Vpn; using ProtonVPN.ProTun.Contracts.Adapters; using ProtonVPN.Service.Firewall; using ProtonVPN.Service.Settings; using ProtonVPN.Service.SplitTunneling; using ProtonVPN.Vpn.SplitTunnel; namespace ProtonVPN.Service.Tests.SplitTunneling; [TestClass] public class SplitTunnelTest { private ILogger _logger; private ISplitTunnelRouting _splitTunnelRouting; private INetworkUtilities _networkUtilities; private ISystemNetworkInterfaces _networkInterfaces; private IConfiguration _config; private IServiceSettings _serviceSettings; private ISplitTunnelClient _splitTunnelClient; private IAppFilter _appFilter; private IPermittedRemoteAddress _permittedRemoteAddress; private IAdapterDetailsCache _proTunAdapterDetailsCache; [TestInitialize] public void TestInitialize() { _logger = Substitute.For(); _splitTunnelRouting = Substitute.For(); _networkUtilities = Substitute.For(); _networkInterfaces = Substitute.For(); _config = Substitute.For(); _serviceSettings = Substitute.For(); _splitTunnelClient = Substitute.For(); _appFilter = Substitute.For(); _permittedRemoteAddress = Substitute.For(); _proTunAdapterDetailsCache = Substitute.For(); } [TestMethod] public void OnVpnConnecting_WhenBlockMode_DisableReversed() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Block }); SplitTunnel splitTunnel = GetSplitTunnel(false, true); // Act splitTunnel.OnVpnConnecting(GetConnectingVpnState()); // Assert _splitTunnelClient.Received(1).Disable(); } [TestMethod] public void OnVpnConnecting_WhenBlockMode_Disable() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Permit }); SplitTunnel splitTunnel = GetSplitTunnel(true); // Act splitTunnel.OnVpnConnecting(GetConnectingVpnState()); // Assert _splitTunnelClient.Received(1).Disable(); } [TestMethod] public void OnVpnConnected_PermitRemoteAddressesOnBlockMode() { // Arrange string[] addresses = ["127.0.0.1", "192.168.0.1", "8.8.8.8"]; _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Block, Ips = addresses, AppPaths = [], }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnConnected(GetConnectedVpnState()); // Assert _permittedRemoteAddress.Received(1).Add(addresses, NetworkFilter.Action.HardPermit); } [TestMethod] public void OnVpnConnected_WhenBlockMode_CallEnable() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Block, AppPaths = [], Ips = [], }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnConnected(GetConnectedVpnState()); // Assert _splitTunnelClient.Received(1).EnableExcludeMode(Arg.Any(), Arg.Any(), Arg.Any()); } [TestMethod] public void OnVpnConnected_WhenBlockMode_CalloutDriverStart() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Block, AppPaths = [], Ips = [], }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnConnected(GetConnectedVpnState()); } [TestMethod] public void OnVpnConnected_WhenPermitMode_CalloutDriverStart() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Permit }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnConnected(GetConnectedVpnState()); } [TestMethod] public void OnVpnConnected_WhenDisabled_CalloutDriverDoNotStart() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Disabled }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnConnected(GetConnectedVpnState()); } [TestMethod] public void OnVpnConnected_WhenDisabled_DoNotEnable() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Disabled }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnConnected(GetConnectedVpnState()); // Assert _splitTunnelClient.Received(0); } [TestMethod] public void OnVpnConnected_WhenPermitMode_EnableReversed() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Permit }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnConnected(GetConnectedVpnState()); // Assert _splitTunnelClient .Received(1) .EnableIncludeMode(Arg.Any(), Arg.Any(), Arg.Any()); } [TestMethod] public void OnVpnConnected_PermitAppsOnBlockMode() { // Arrange string[] apps = ["app1", "app2", "app3"]; _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Block, AppPaths = apps, Ips = [], }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnConnected(GetConnectedVpnState()); // Assert _appFilter.Received(1).Add(apps, Arg.Any[]>()); } [TestMethod] public void OnVpnConnecting_ShouldBlockApps_WhenModeIsPermit() { // Arrange string[] apps = ["app1", "app2", "app3"]; _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Permit, AppPaths = apps }); SplitTunnel splitTunnel = GetSplitTunnel(true); // Act splitTunnel.OnVpnConnecting(GetConnectingVpnState()); // Assert _appFilter.Received(1).Add(apps, Arg.Any[]>()); } [TestMethod] public void OnVpnDisconnected_ManualDisconnect_ShouldDisable() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Block }); SplitTunnel splitTunnel = GetSplitTunnel(true); // Act splitTunnel.OnVpnDisconnected(GetDisconnectedVpnState(true)); // Assert _splitTunnelClient.Received(1).Disable(); } [TestMethod] public void OnVpnDisconnected_ManualDisconnect_ShouldDisableReversed() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Permit }); SplitTunnel splitTunnel = GetSplitTunnel(false, true); // Act splitTunnel.OnVpnDisconnected(GetDisconnectedVpnState(true)); // Assert _splitTunnelClient.Received(1).Disable(); } [TestMethod] public void OnVpnDisconnected_ManualDisconnect_ShouldStopCalloutDriver() { // Arrange _serviceSettings.SplitTunnelSettings.Returns(new SplitTunnelSettingsIpcEntity { Mode = SplitTunnelModeIpcEntity.Permit }); SplitTunnel splitTunnel = GetSplitTunnel(); // Act splitTunnel.OnVpnDisconnected(GetDisconnectedVpnState(true)); } private SplitTunnel GetSplitTunnel(bool enabled = false, bool reverseEnabled = false) { return new SplitTunnel( enabled, reverseEnabled, _logger, _splitTunnelRouting, _networkUtilities, _networkInterfaces, _config, _serviceSettings, _splitTunnelClient, _appFilter, _permittedRemoteAddress, _proTunAdapterDetailsCache); } private VpnState GetConnectedVpnState() { return new VpnState( VpnStatus.Connected, VpnError.None, "1.1.1.1", "2.2.2.2", 443, VpnProtocol.Smart); } private VpnState GetDisconnectedVpnState(bool manualDisconnect = false) { return new VpnState( VpnStatus.Disconnected, manualDisconnect ? VpnError.None : VpnError.Unknown, "1.1.1.1", "2.2.2.2", 443, VpnProtocol.Smart); } private VpnState GetConnectingVpnState() { return new VpnState( VpnStatus.Disconnected, VpnError.None, "1.1.1.1", "2.2.2.2", 443, VpnProtocol.Smart); } }