/*
* 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.Collections.Generic;
using System.Net;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.SplitTunnelLogs;
using ProtonVPN.NetworkFilter;
using ProtonVPN.OperatingSystems.Network.Contracts;
using ProtonVPN.ProcessCommunication.Contracts.Entities.Vpn;
using ProtonVPN.ProTun.Contracts.Adapters;
using ProtonVPN.Service.Firewall;
using ProtonVPN.Service.Settings;
using ProtonVPN.Vpn.SplitTunnel;
using Action = ProtonVPN.NetworkFilter.Action;
namespace ProtonVPN.Service.SplitTunneling;
public class SplitTunnel : ISplitTunnel
{
private bool _reverseEnabled;
private bool _enabled;
private SplitTunnelContext? _context;
private readonly ILogger _logger;
private readonly ISplitTunnelRouting _splitTunnelRouting;
private readonly INetworkUtilities _networkUtilities;
private readonly ISystemNetworkInterfaces _networkInterfaces;
private readonly IConfiguration _config;
private readonly IServiceSettings _serviceSettings;
private readonly ISplitTunnelClient _splitTunnelClient;
private readonly IAppFilter _appFilter;
private readonly IPermittedRemoteAddress _permittedRemoteAddress;
private readonly IAdapterDetailsCache _proTunAdapterDetailsCache;
public SplitTunnel(
ILogger logger,
ISplitTunnelRouting splitTunnelRouting,
INetworkUtilities networkUtilities,
ISystemNetworkInterfaces networkInterfaces,
IConfiguration config,
IServiceSettings serviceSettings,
ISplitTunnelClient splitTunnelClient,
IAppFilter appFilter,
IPermittedRemoteAddress permittedRemoteAddress,
IAdapterDetailsCache proTunAdapterDetailsCache)
{
_logger = logger;
_splitTunnelRouting = splitTunnelRouting;
_networkUtilities = networkUtilities;
_networkInterfaces = networkInterfaces;
_config = config;
_serviceSettings = serviceSettings;
_splitTunnelClient = splitTunnelClient;
_appFilter = appFilter;
_permittedRemoteAddress = permittedRemoteAddress;
_proTunAdapterDetailsCache = proTunAdapterDetailsCache;
}
public SplitTunnel(
bool enabled,
bool reverseEnabled,
ILogger logger,
ISplitTunnelRouting splitTunnelRouting,
INetworkUtilities networkUtilities,
ISystemNetworkInterfaces networkInterfaces,
IConfiguration config,
IServiceSettings serviceSettings,
ISplitTunnelClient splitTunnelClient,
IAppFilter appFilter,
IPermittedRemoteAddress permittedRemoteAddress,
IAdapterDetailsCache proTunAdapterDetailsCache) :
this(logger,
splitTunnelRouting,
networkUtilities,
networkInterfaces,
config,
serviceSettings,
splitTunnelClient,
appFilter,
permittedRemoteAddress,
proTunAdapterDetailsCache)
{
_enabled = enabled;
_reverseEnabled = reverseEnabled;
}
public void OnVpnConnecting(VpnState vpnState)
{
DisableReversed();
Disable();
_appFilter.RemoveAll();
_permittedRemoteAddress.RemoveAll();
if (_serviceSettings.SplitTunnelSettings.Mode == SplitTunnelModeIpcEntity.Permit)
{
_appFilter.Add(_serviceSettings.SplitTunnelSettings.AppPaths, [
Tuple.Create(Layer.AppAuthConnectV4, Action.SoftBlock),
Tuple.Create(Layer.AppAuthConnectV6, Action.SoftBlock),
]);
}
}
public void OnVpnConnected(VpnState state)
{
if (_serviceSettings.SplitTunnelSettings.Mode == SplitTunnelModeIpcEntity.Disabled)
{
return;
}
SetUpApps(state);
SetUpIps(state);
}
private void SetUpApps(VpnState state)
{
switch (_serviceSettings.SplitTunnelSettings.Mode)
{
case SplitTunnelModeIpcEntity.Block:
DisableReversed();
Enable(state);
break;
case SplitTunnelModeIpcEntity.Permit:
_appFilter.RemoveAll();
Disable();
EnableReversed(state);
break;
}
}
public void UpdateContext(SplitTunnelContext context)
{
_context = context;
}
private void SetUpIps(VpnState state)
{
if (_context is null)
{
_logger.Warn("Split tunnel context is missing, routes won't be added.");
return;
}
string? localIpv4Address = state.LocalIp;
if (string.IsNullOrEmpty(localIpv4Address))
{
_logger.Warn("Local IPv4 address is missing, split tunneling routes won't be added.");
}
else
{
bool isIpv6Supported = _context.Config.IsIpv6Enabled && _context.Endpoint.Server.IsIpv6Supported;
_splitTunnelRouting.SetUpRoutingTable(_context.Config, localIpv4Address, isIpv6Supported);
}
}
public void OnVpnDisconnected(VpnState state)
{
if (state.Error == VpnError.None)
{
DisableSplitTunnel();
_appFilter.RemoveAll();
_context = null;
}
}
private void DisableSplitTunnel()
{
Disable();
DisableReversed();
if (_context is null)
{
_logger.Warn("Split tunnel context is missing, routes won't be removed.");
}
else
{
_splitTunnelRouting.DeleteRoutes(_context.Config);
}
}
private void Enable(VpnState state)
{
string excludedHardwareId = _config.GetHardwareId(state.VpnProtocol, _serviceSettings.OpenVpnAdapter);
IPAddress localIpv4Address = _networkUtilities.GetBestInterfaceIPv4Address(excludedHardwareId);
INetworkInterface bestInterface = _networkInterfaces.GetBestInterfaceExcludingHardwareId(excludedHardwareId);
IPAddress? localIpv6Address = null;
if (!string.IsNullOrEmpty(bestInterface.Id))
{
localIpv6Address = bestInterface.GetPreferredIpv6UnicastAddress();
}
string[] appPaths = _serviceSettings.SplitTunnelSettings.AppPaths ?? [];
_splitTunnelClient.EnableExcludeMode(appPaths, localIpv4Address, localIpv6Address);
if (appPaths.Length > 0)
{
List> appFilters = [
Tuple.Create(Layer.AppAuthConnectV4, Action.HardPermit),
Tuple.Create(Layer.AppAuthConnectV6, localIpv6Address is null ? Action.HardBlock : Action.HardPermit),
];
_appFilter.Add(appPaths, [.. appFilters]);
}
if (_serviceSettings.SplitTunnelSettings.Ips.Length > 0)
{
_permittedRemoteAddress.Add(_serviceSettings.SplitTunnelSettings.Ips, Action.HardPermit);
}
_enabled = true;
}
private void Disable()
{
if (_enabled)
{
_splitTunnelClient.Disable();
_appFilter.RemoveAll();
_permittedRemoteAddress.RemoveAll();
_enabled = false;
}
}
private void EnableReversed(VpnState vpnState)
{
IPAddress? localIpv6Address = null;
if (vpnState.VpnProtocol.IsWireGuard())
{
IPAddress.TryParse(_config.WireGuard.DefaultClientIpv6Address, out localIpv6Address);
}
else if (vpnState.VpnProtocol.IsProTun())
{
IPAddress.TryParse(_proTunAdapterDetailsCache.ClientIpv6Address, out localIpv6Address);
}
else if (vpnState.VpnProtocol.IsOpenVpn())
{
// ProtonVPN's OpenVPN server does not provide GUA IPv6 address, so we block all IPv6 tunnel traffic
_appFilter.Add(_serviceSettings.SplitTunnelSettings.AppPaths, [Tuple.Create(Layer.AppAuthConnectV6, Action.HardBlock)]);
}
string[] appPaths = _serviceSettings.SplitTunnelSettings.AppPaths ?? [];
_splitTunnelClient.EnableIncludeMode(appPaths, IPAddress.Parse(vpnState.LocalIp), localIpv6Address);
_reverseEnabled = true;
}
private void DisableReversed()
{
if (_reverseEnabled)
{
_splitTunnelClient.Disable();
_reverseEnabled = false;
}
}
}