/* * Copyright (c) 2026 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.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Common.Legacy; using ProtonVPN.Common.Legacy.NetShield; using ProtonVPN.Common.Legacy.PortForwarding; using ProtonVPN.Common.Legacy.Restrictions; using ProtonVPN.Common.Legacy.Vpn; using ProtonVPN.EntityMapping.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppServiceLogs; using ProtonVPN.Logging.Contracts.Events.ConnectLogs; using ProtonVPN.Logging.Contracts.Events.ProcessCommunicationLogs; using ProtonVPN.ProcessCommunication.Contracts.Controllers; using ProtonVPN.ProcessCommunication.Contracts.Entities.NetShield; using ProtonVPN.ProcessCommunication.Contracts.Entities.PortForwarding; using ProtonVPN.ProcessCommunication.Contracts.Entities.Restrictions; using ProtonVPN.ProcessCommunication.Contracts.Entities.Settings; using ProtonVPN.ProcessCommunication.Contracts.Entities.Update; using ProtonVPN.ProcessCommunication.Contracts.Entities.Vpn; using ProtonVPN.Service.KillSwitch; using ProtonVPN.Service.Settings; using ProtonVPN.Service.StateMachine; using ProtonVPN.Vpn.Connection; using ProtonVPN.Vpn.LocalAgent; using ProtonVPN.Vpn.PortMapping; namespace ProtonVPN.Service.ProcessCommunication; public class ClientControllerSender : IClientController, IClientControllerSender, IServiceSettingsAware { private readonly IKillSwitch _killSwitch; private readonly ILogger _logger; private readonly IEntityMapper _entityMapper; private readonly ILocalAgent _localAgent; private readonly ILocalAgentEventReceiver _localAgentEventReceiver; private readonly IVpnConnectionStateMachine _vpnControllerStateMachine; private readonly IPortMappingProtocolClient _portMappingProtocolClient; private VpnState _vpnState = VpnState.Default; private PortForwardingState? _portForwardingState; private CancellationTokenSource? _vpnStateCancellationTokenSource; private CancellationTokenSource? _portForwardingStateCancellationTokenSource; private CancellationTokenSource? _connectionDetailsCancellationTokenSource; private CancellationTokenSource? _netShieldStatisticCancellationTokenSource; private CancellationTokenSource? _restrictionsCancellationTokenSource; private CancellationTokenSource? _updateStateCancellationTokenSource; private readonly object _streamCancellationTokenLock = new(); private readonly Channel _vpnStateChannel = Channel.CreateUnbounded(); private readonly Channel _portForwardingStateChannel = Channel.CreateUnbounded(); private readonly Channel _updateStateChannel = Channel.CreateUnbounded(); public ClientControllerSender( IKillSwitch killSwitch, ILogger logger, IEntityMapper entityMapper, ILocalAgent localAgent, ILocalAgentEventReceiver localAgentEventReceiver, IVpnConnectionStateMachine vpnControllerStateMachine, IPortMappingProtocolClient portMappingProtocolClient) { _killSwitch = killSwitch; _logger = logger; _entityMapper = entityMapper; _localAgent = localAgent; _localAgentEventReceiver = localAgentEventReceiver; _vpnControllerStateMachine = vpnControllerStateMachine; _vpnControllerStateMachine.SubscribeToStateChanged(OnVpnStateChangedAsync); _portMappingProtocolClient = portMappingProtocolClient; _portMappingProtocolClient.StateChanged += OnPortForwardingStateChangedAsync; } public IAsyncEnumerable StreamVpnStateChangeAsync(CancellationToken cancelToken) { CancellationTokenSource cts = new(); lock (_streamCancellationTokenLock) { _vpnStateCancellationTokenSource?.Cancel(); _vpnStateCancellationTokenSource = cts; } return StreamAsync(_vpnStateChannel.Reader, cts.Token); } private async IAsyncEnumerable StreamAsync(ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { T entity = await reader.ReadAsync(cancellationToken); yield return entity; } } private async IAsyncEnumerable MapStreamAsync( ChannelReader reader, Func mapper, [EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (TIn entity in StreamAsync(reader, cancellationToken)) { yield return mapper(entity); } } public IAsyncEnumerable StreamPortForwardingStateChangeAsync(CancellationToken cancelToken) { CancellationTokenSource cts = new(); lock (_streamCancellationTokenLock) { _portForwardingStateCancellationTokenSource?.Cancel(); _portForwardingStateCancellationTokenSource = cts; } return StreamAsync(_portForwardingStateChannel.Reader, cts.Token); } public IAsyncEnumerable StreamConnectionDetailsChangeAsync(CancellationToken cancelToken) { CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken); lock (_streamCancellationTokenLock) { _connectionDetailsCancellationTokenSource?.Cancel(); _connectionDetailsCancellationTokenSource = cts; } return MapStreamAsync( _localAgentEventReceiver.ConnectionDetailsChannel.Reader, MapConnectionDetails, cts.Token); } public IAsyncEnumerable StreamNetShieldStatisticChangeAsync(CancellationToken cancelToken) { CancellationTokenSource cts = new(); lock (_streamCancellationTokenLock) { _netShieldStatisticCancellationTokenSource?.Cancel(); _netShieldStatisticCancellationTokenSource = cts; } return MapStreamAsync( _localAgentEventReceiver.NetShieldStatsChannel.Reader, MapNetShieldStatistic, cts.Token); } public IAsyncEnumerable StreamRestrictionsChangeAsync(CancellationToken cancelToken) { CancellationTokenSource cts = new(); lock (_streamCancellationTokenLock) { _restrictionsCancellationTokenSource?.Cancel(); _restrictionsCancellationTokenSource = cts; } return MapStreamAsync( _localAgentEventReceiver.RestrictionsChannel.Reader, MapRestrictions, cts.Token); } public IAsyncEnumerable StreamUpdateStateChangeAsync(CancellationToken cancelToken) { CancellationTokenSource cts = new(); lock (_streamCancellationTokenLock) { _updateStateCancellationTokenSource?.Cancel(); _updateStateCancellationTokenSource = cts; } return StreamAsync(_updateStateChannel.Reader, cts.Token); } public async Task SendCurrentVpnStateAsync() { await SendStateChangeAsync(_vpnState); } private async Task OnVpnStateChangedAsync(VpnState state) { _vpnState = state; _logger.Info($"VPN state changed - {GetVpnStatusLogMessage(state)}"); await SendStateChangeAsync(state); } private static string GetVpnStatusLogMessage(VpnState state) { return $"Status '{state.Status}', Error: '{state.Error}', LocalIp: '{state.LocalIp}', " + $"RemoteIp: '{state.RemoteIp}', Port: {state.EndpointPort}, Label: '{state.Label}', " + $"VpnProtocol: '{state.VpnProtocol}', OpenVpnAdapter: '{state.OpenVpnAdapter}'"; } private async Task SendStateChangeAsync(VpnState state) { _logger.Debug($"Sending VPN state - {GetVpnStatusLogMessage(state)}"); await _vpnStateChannel.Writer.WriteAsync(CreateVpnStateIpcEntity(state)); } private VpnStateIpcEntity CreateVpnStateIpcEntity(VpnState state) { bool killSwitchEnabled = _killSwitch.GetExpectedLeakProtectionStatus(state); if (!killSwitchEnabled) { _vpnState = new VpnState(state.Status, state.Error, state.VpnProtocol); } return new VpnStateIpcEntity { Status = _entityMapper.Map(state.Status), Error = _entityMapper.Map(state.Error), EndpointIp = state.RemoteIp, EndpointPort = state.EndpointPort, NetworkBlocked = killSwitchEnabled, OpenVpnAdapterType = _entityMapper.MapNullableStruct(state.OpenVpnAdapter), VpnProtocol = _entityMapper.Map(state.VpnProtocol), Label = state.Label, ConnectionCertificatePem = state.ConnectionCertificate?.Pem, }; } private ConnectionDetailsIpcEntity MapConnectionDetails(ConnectionDetails connectionDetails) { _logger.Info("Sending ConnectionDetails change while connected " + $"to server with '{connectionDetails.ServerIpAddress}'"); return _entityMapper.Map(connectionDetails); } public async Task SendCurrentPortForwardingStateAsync() { if (_portForwardingState is not null) { await SendPortForwardingStateChangeAsync(_portForwardingState); } } public async Task SendUpdateStateAsync(UpdateStateIpcEntity updateState) { await _updateStateChannel.Writer.WriteAsync(updateState); } private async void OnPortForwardingStateChangedAsync(object? sender, EventArgs e) { PortForwardingState state = e.Data; _logger.Debug($"Port Forwarding state changed - {GetPortForwardingStateLogMessage(state)}"); _portForwardingState = state; await SendPortForwardingStateChangeAsync(state); } private string GetPortForwardingStateLogMessage(PortForwardingState state) { StringBuilder logMessage = new StringBuilder() .Append($"Status '{state.Status}' triggered at '{state.TimestampUtc}'"); if (state.MappedPort?.MappedPort is not null) { TemporaryMappedPort mappedPort = state.MappedPort; logMessage.Append($", Port pair {mappedPort.MappedPort}, expiring in " + $"{mappedPort.Lifetime} at {mappedPort.ExpirationDateUtc}"); } return logMessage.ToString(); } private async Task SendPortForwardingStateChangeAsync(PortForwardingState state) { _logger.Debug($"Sending Port Forwarding state - {GetPortForwardingStateLogMessage(state)}"); PortForwardingStateIpcEntity stateIpcEntity = _entityMapper.Map(state); await _portForwardingStateChannel.Writer.WriteAsync(stateIpcEntity); } private NetShieldStatisticIpcEntity MapNetShieldStatistic(NetShieldStatistic stats) { _logger.Info($"Sending NetShield statistic triggered at '{stats.TimestampUtc}' " + $"[Ads: '{stats.NumOfAdvertisementUrlsBlocked}']" + $"[Malware: '{stats.NumOfMaliciousUrlsBlocked}']" + $"[Trackers: '{stats.NumOfTrackingUrlsBlocked}']" + $"[Adult content: '{stats.NumOfAdultContentUrlsBlocked}']"); return _entityMapper.Map(stats); } private RestrictionListIpcEntity MapRestrictions(RestrictionsList restrictions) { _logger.Info($"Sending restrictions '{string.Join(',', restrictions.Restrictions)}'"); return _entityMapper.Map(restrictions); } public async void OnServiceSettingsChanged(MainSettingsIpcEntity settings) { VpnState vpnState = _vpnState; if (vpnState.Status == VpnStatus.Disconnected) { _logger.Info($"Sending VPN Service Settings Change. " + $"Status: '{vpnState.Status}' (Error: '{vpnState.Error}')"); await SendStateChangeAsync(vpnState); } else if (vpnState.Status == VpnStatus.Connected) { if (!settings.PortForwarding) { _logger.Debug("Requesting NAT-PMP client to stop."); await _portMappingProtocolClient.StopAsync(); } VpnFeatures vpnFeatures = CreateVpnFeatures(settings); _vpnControllerStateMachine.UpdateVpnConfig(vpnFeatures); _localAgent.SetFeatures(vpnFeatures); } } private static VpnFeatures CreateVpnFeatures(MainSettingsIpcEntity settings) { return new() { SplitTcp = settings.SplitTcp, NetShieldMode = settings.NetShieldMode, PortForwarding = settings.PortForwarding, ModerateNat = settings.ModerateNat, }; } }