/*
* 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.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Common.Core.Helpers;
using ProtonVPN.Common.Core.LocalAgent;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Common.Legacy.Vpn;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.LocalAgentLogs;
using ProtonVPN.Logging.Contracts.Events.VpnStateMachineLogs;
using ProtonVPN.Service.SplitTunneling;
using ProtonVPN.Service.StateMachine.Messages;
using ProtonVPN.Service.StateMachine.SideEffects;
using ProtonVPN.Service.Vpn;
using ProtonVPN.Vpn.Common;
using ProtonVPN.Vpn.Connection;
using ProtonVPN.Vpn.LocalAgent;
using Stateless;
namespace ProtonVPN.Service.StateMachine;
internal sealed partial class VpnConnectionStateMachine : IVpnConnectionStateMachine
{
private readonly ILogger _logger;
private readonly ILocalAgent _localAgent;
private readonly ILocalAgentTlsCredentialsCache _localAgentTlsCredentialsCache;
private readonly ILocalAgentEventReceiver _localagentEventReceiver;
private readonly IVpnEndpointCandidates _candidates;
private readonly IConnectionProbe _connectionProbe;
private readonly ITunnelOrchestrator _tunnelOrchestrator;
private readonly IVpnStateSideEffects _vpnStateSideEffects;
private readonly StateMachine _machine;
private readonly object _disconnectTaskLock = new();
private readonly object _sessionStateLock = new();
private static readonly TimeSpan _connectedStateTimeout = TimeSpan.FromSeconds(10);
private CancellationTokenSource _sessionCts = new();
private CancellationTokenSource _localAgentChannelsCts = new();
private Task? _disconnectTask;
private bool _connectionCredentialsMonitorStarted;
private long _connectionCredentialsSubscribedVersion;
private bool _hasPortForwardingError;
private bool _keepConnectedDuringLocalAgentReconnect;
private IReadOnlyList _servers = [];
private VpnConfig? _vpnConfig;
private VpnCredentials? _credentials;
private VpnEndpoint? _selectedEndpoint;
private VpnError _lastError = VpnError.None;
private LocalAgentState? _localAgentState;
private ConnectionCertificate? _lastConnectionCertificate;
private int _machineStateSnapshot = (int)State.Disconnected;
private State MachineState => (State)Volatile.Read(ref _machineStateSnapshot);
public bool IsConnected => MachineState == State.Connected;
public VpnError LastError => _lastError;
private readonly List _disconnectOnVpnErrors =
[
VpnError.SessionKilledDueToMultipleKeys,
VpnError.CertificateRevoked,
VpnError.CertCARevokedOrExpired,
VpnError.PlanNeedsToBeUpgraded,
VpnError.SessionLimitReachedFree,
VpnError.SessionLimitReachedBasic,
VpnError.SessionLimitReachedPlus,
VpnError.SessionLimitReachedVisionary,
VpnError.SessionLimitReachedPro,
VpnError.SessionLimitReachedUnknown,
VpnError.SystemErrorOnTheServer,
VpnError.ServerSessionDoesNotMatch,
VpnError.ServerSessionError,
];
private readonly List _waitForUserActionOnVpnErrors =
[
VpnError.TwoFactorRequiredReasonUnknown,
VpnError.TwoFactorExpired,
VpnError.TwoFactorNewConnection,
];
private readonly Dictionary> _typedTriggerDispatch;
public VpnConnectionStateMachine(
ILogger logger,
ILocalAgent localAgent,
ILocalAgentTlsCredentialsCache localAgentTlsCredentialsCache,
ILocalAgentEventReceiver localAgentEventReceiver,
IVpnEndpointCandidates candidates,
IConnectionProbe connectionProbe,
ITunnelOrchestrator tunnelOrchestrator,
IVpnStateSideEffects vpnStateSideEffects)
{
_logger = logger;
_localAgent = localAgent;
_localAgentTlsCredentialsCache = localAgentTlsCredentialsCache;
_localagentEventReceiver = localAgentEventReceiver;
_candidates = candidates;
_connectionProbe = connectionProbe;
_tunnelOrchestrator = tunnelOrchestrator;
_vpnStateSideEffects = vpnStateSideEffects;
_machine = new StateMachine(State.Disconnected);
_connectTrigger = _machine.SetTriggerParameters(Trigger.ConnectRequested);
_availabilitySucceededTrigger = _machine.SetTriggerParameters(Trigger.AvailabilitySucceeded);
_endpointSelectedTrigger = _machine.SetTriggerParameters(Trigger.EndpointSelected);
_endpointSelectionFailedTrigger = _machine.SetTriggerParameters(Trigger.EndpointSelectionFailed);
_clientSecretKeyChangedTrigger = _machine.SetTriggerParameters(Trigger.ClientSecretKeyChanged);
_localAgentConnectionRequestedTrigger = _machine.SetTriggerParameters(Trigger.LocalAgentConnectionRequested);
_connectionCertificateChangedTrigger = _machine.SetTriggerParameters(Trigger.ConnectionCertificateChanged);
_typedTriggerDispatch = new()
{
[Trigger.ConnectRequested] = ct => _machine.FireAsync(_connectTrigger, ct),
[Trigger.AvailabilitySucceeded] = ct => _machine.FireAsync(_availabilitySucceededTrigger, ct),
[Trigger.EndpointSelectionFailed] = ct => _machine.FireAsync(_endpointSelectionFailedTrigger, ct),
[Trigger.EndpointSelected] = ct => _machine.FireAsync(_endpointSelectedTrigger, ct),
[Trigger.ClientSecretKeyChanged] = ct => _machine.FireAsync(_clientSecretKeyChangedTrigger, ct),
[Trigger.LocalAgentConnectionRequested] = ct => _machine.FireAsync(_localAgentConnectionRequestedTrigger, ct),
[Trigger.ConnectionCertificateChanged] = ct => _machine.FireAsync(_connectionCertificateChangedTrigger, ct),
};
Configure();
_machine.OnTransitionedAsync(HandleTransitionedAsync);
_machine.OnUnhandledTrigger((s, trigger) =>
{
_logger.Warn($"VPN state machine ignored trigger {trigger} while in state {s}.");
});
StartMessageSupervisor();
}
public void Connect(IReadOnlyList servers, VpnConfig config, VpnCredentials credentials)
{
PostMessage(new ConnectRequestMessage(servers, config, credentials));
}
public void Reconnect()
{
PostMessage(new ReconnectRequestMessage());
}
public void Disconnect(VpnError error = VpnError.None)
{
CancelSession();
PostMessage(new DisconnectRequestMessage(error));
}
// This method should only be used by the service shutdown logic
// to synchronously wait for the tunnel to disconnect before allowing the process to exit.
// It will not modify state in the state machine, therefore SubscribeToStateChanged callbacks will not be triggered.
public async Task DisconnectAsync()
{
_messagesCts.Cancel();
CancelSession();
await _tunnelOrchestrator.DisconnectAsync();
State machineState = State.Disconnected;
VpnState vpnState = await MapStateAsync(machineState);
await _vpnStateSideEffects.ApplyAsync(vpnState, machineState);
}
public void ReportDisconnected(VpnError error)
{
PostMessage(new ReportDisconnectedMessage(error));
}
public void UpdateVpnConfig(VpnFeatures vpnFeatures)
{
PostMessage(new UpdateVpnFeaturesMessage(vpnFeatures));
}
private void OnConnected()
{
SetKeepConnectedDuringLocalAgentReconnect(false);
}
private CancellationToken GetSessionToken()
{
lock (_sessionStateLock)
{
return _sessionCts.Token;
}
}
private bool IsCurrentSession(CancellationToken token)
{
lock (_sessionStateLock)
{
return token == _sessionCts.Token && !token.IsCancellationRequested;
}
}
private void CancelSession()
{
CancellationTokenSource cts;
lock (_sessionStateLock)
{
cts = _sessionCts;
}
try
{
cts.Cancel();
}
catch (ObjectDisposedException)
{
}
}
private bool GetKeepConnectedDuringLocalAgentReconnect()
{
lock (_sessionStateLock)
{
return _keepConnectedDuringLocalAgentReconnect;
}
}
private void SetKeepConnectedDuringLocalAgentReconnect(bool value)
{
lock (_sessionStateLock)
{
_keepConnectedDuringLocalAgentReconnect = value;
}
}
private void ResetSessionContext()
{
CancellationTokenSource previousSessionCts;
lock (_sessionStateLock)
{
previousSessionCts = _sessionCts;
_sessionCts = new();
_connectionCredentialsMonitorStarted = false;
_connectionCredentialsSubscribedVersion = 0;
_keepConnectedDuringLocalAgentReconnect = false;
}
previousSessionCts.Cancel();
previousSessionCts.Dispose();
}
private void StartMonitoringCredentialsUpdates()
{
CancellationToken sessionToken;
lock (_sessionStateLock)
{
if (_connectionCredentialsMonitorStarted)
{
return;
}
_connectionCredentialsMonitorStarted = true;
_connectionCredentialsSubscribedVersion = _localAgentTlsCredentialsCache.CurrentVersion;
sessionToken = _sessionCts.Token;
}
Task credentialUpdateMonitorTask = MonitorChannelAsync(
_localAgentTlsCredentialsCache.LocalAgentTlsCredentialsChannel,
HandleCredentialsUpdateAsync,
sessionToken,
sessionToken);
_ = Task.Run(() => credentialUpdateMonitorTask, sessionToken);
}
private Task HandleCredentialsUpdateAsync(LocalAgentTlsCredentialsUpdate credentialsUpdate, CancellationToken ct)
{
PostMessage(new CredentialsUpdatedMessage(credentialsUpdate, ct));
return Task.CompletedTask;
}
private void OnExitConnectedState(StateMachine.Transition transition)
{
switch (transition.Trigger)
{
case Trigger.ConnectionCertificateChanged:
SetKeepConnectedDuringLocalAgentReconnect(true);
break;
}
}
private Task StartDisconnectingTunnelAsync()
{
lock (_disconnectTaskLock)
{
if (_disconnectTask is { IsCompleted: false })
{
return _disconnectTask;
}
_disconnectTask = Task.Run(async() =>
{
_hasPortForwardingError = false;
await _tunnelOrchestrator.DisconnectAsync();
await RunVpnStateSideEffectsAsync(State.Disconnected);
});
return _disconnectTask;
}
}
private async Task RunVpnStateSideEffectsAsync(State state)
{
VpnState vpnState = await MapStateAsync(state);
// MachineState is used intentionally here instead of the state parameter. When the
// machine has already advanced to a new connection (e.g. AvailabilityCheck) while the
// previous tunnel is still tearing down, MachineState will not equal State.Disconnected,
// so _killSwitch.OnVpnDisconnected is suppressed — preventing the firewall from
// dropping during a reconnect and leaking DNS/traffic.
await _vpnStateSideEffects.ApplyAsync(vpnState, MachineState);
}
private async Task EstablishLocalAgentChannelAsync(StateMachine.Transition transition, CancellationToken ct)
{
if (_vpnConfig is null)
{
_logger.Error("Can't connect to local agent channel due to a missing VPN config.");
_lastError = VpnError.Unknown;
Fire(Trigger.DisconnectRequested, ct);
return;
}
if (transition.Trigger == Trigger.ConnectionCertificateChanged)
{
_localAgent.CloseTlsChannel();
}
StartMonitoringCredentialsUpdates();
ConnectionCertificate? certificate = (await _localAgentTlsCredentialsCache.GetAsync(ct))?.ConnectionCertificate;
string? clientSecretPem = _credentials?.ClientKeyPair.SecretKey.Pem;
if (certificate is null || clientSecretPem is null || _selectedEndpoint is null)
{
string? missingFields = NullFieldFormatter.FormatNullFields(
(nameof(certificate), certificate),
(nameof(clientSecretPem), clientSecretPem),
(nameof(_selectedEndpoint), _selectedEndpoint));
_logger.Error($"Can't connect to local agent channel due to missing data: {missingFields}.");
_lastError = VpnError.Unknown;
Fire(Trigger.DisconnectRequested, ct);
return;
}
_ = DisconnectIfNotConnectedAfterTimeoutAsync(ct);
_lastConnectionCertificate = certificate;
bool result = _localAgent.ConnectToTlsChannel(new LocalAgentConnectParams()
{
Server = _selectedEndpoint.Server,
ClientCertPem = certificate.Pem,
ClientSecretPem = clientSecretPem,
VpnConfig = _vpnConfig,
});
if (result)
{
StartMonitoringLocalAgentChannels(ct);
}
else
{
_lastError = _localAgent.LastError;
Fire(Trigger.DisconnectRequested, ct);
}
}
private async Task DisconnectIfNotConnectedAfterTimeoutAsync(CancellationToken ct)
{
try
{
await Task.Delay(_connectedStateTimeout, ct);
if (ct.IsCancellationRequested || _lastError == VpnError.TwoFactorNewConnection)
{
return;
}
if (!IsConnected)
{
_logger.Info("Connected state not reached within 10 seconds after local agent channel setup.");
_lastError = VpnError.ServerUnreachable;
Fire(Trigger.DisconnectRequested, ct);
}
}
catch (OperationCanceledException)
{
}
}
private void StartMonitoringLocalAgentChannels(CancellationToken sessionToken)
{
CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(sessionToken);
_localAgentChannelsCts.Cancel();
_localAgentChannelsCts.Dispose();
_localAgentChannelsCts = cts;
// Don't pass cts.Token to the task as we want WatchEventsAsync to drain all the events after cancelling the token
_ = Task.Run(() => _localagentEventReceiver.WatchEventsAsync(cts.Token));
_ = Task.Run(() => MonitorChannelAsync(_localagentEventReceiver.StateChannel, HandleLocalAgentStateAsync, cts.Token, sessionToken), cts.Token);
_ = Task.Run(() => MonitorChannelAsync(_localagentEventReceiver.ErrorChannel, HandleLocalAgentErrorAsync, cts.Token, sessionToken), cts.Token);
}
private async Task MonitorChannelAsync(
Channel channel,
Func entityHandler,
CancellationToken channelReadToken,
CancellationToken sessionToken)
{
try
{
while (!channelReadToken.IsCancellationRequested)
{
T entity = await channel.Reader.ReadAsync(channelReadToken);
await entityHandler(entity, sessionToken);
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logger.Error("Local agent channel observer threw an exception.", ex);
}
}
private Task HandleLocalAgentStateAsync(LocalAgentState state, CancellationToken ct)
{
PostMessage(new LocalAgentStateChangedMessage(state, ct));
return Task.CompletedTask;
}
private Task HandleLocalAgentErrorAsync(VpnError error, CancellationToken ct)
{
PostMessage(new LocalAgentErrorMessage(error, ct));
return Task.CompletedTask;
}
private async Task ProcessLocalAgentErrorAsync(VpnError error, CancellationToken ct)
{
if (_waitForUserActionOnVpnErrors.Contains(error))
{
// When local agent connects, we receive one event with error for two factor to be completed.
// Then the local agent library sends request to set NetShield level and a second message
// with the same error is received. So we want to skip that.
if (_lastError == VpnError.TwoFactorNewConnection)
{
return;
}
_logger.Info("Two factor required. Waiting for user action.");
_lastError = error;
Fire(Trigger.TwoFactorRequested, ct);
}
else if (_disconnectOnVpnErrors.Contains(error))
{
Disconnect(error);
}
else if (error == VpnError.CertificateNotYetProvided)
{
_logger.Info("Reconnecting to TLS channel.");
Fire(Trigger.ConnectionCertificateChanged, ct);
}
else if (error == VpnError.CertificateExpired)
{
await HandleConnectionCertificateExpirationAsync(ct);
}
else if (error == VpnError.PortForwardingNotSupported)
{
_hasPortForwardingError = true;
}
else
{
_logger.Info($"Ignoring error {error}.");
}
}
private async Task HandleConnectionCredentialsChangeAsync(CancellationToken ct)
{
_lastError = VpnError.None;
LocalAgentTlsCredentials? tlsCredentials = await _localAgentTlsCredentialsCache.GetAsync(ct);
if (tlsCredentials is not null &&
_credentials is not null &&
tlsCredentials.ClientKeyPair.SecretKey.Pem != _credentials.Value.ClientKeyPair.SecretKey.Pem)
{
_credentials = new(
tlsCredentials.ConnectionCertificate.Pem,
tlsCredentials.ConnectionCertificate.ExpirationDateUtc,
tlsCredentials.ClientKeyPair,
_credentials.Value.Username,
_credentials.Value.Password);
_logger.Info("The client secret key has changed, reconnecting the tunnel.");
Fire(Trigger.ClientSecretKeyChanged, ct);
return;
}
if (_lastConnectionCertificate?.Pem != tlsCredentials?.ConnectionCertificate.Pem)
{
_logger.Info("The connection certificate has changed.");
Fire(Trigger.ConnectionCertificateChanged, ct);
}
}
private async Task HandleConnectionCertificateExpirationAsync(CancellationToken ct)
{
if (!IsCurrentSession(ct))
{
return;
}
ConnectionCertificate? lastCertificate = _lastConnectionCertificate;
LocalAgentTlsCredentials? currentCredentials = await _localAgentTlsCredentialsCache.GetAsync(ct);
if (string.IsNullOrWhiteSpace(currentCredentials?.ConnectionCertificate.Pem) ||
currentCredentials?.ConnectionCertificate.Pem == lastCertificate?.Pem)
{
_lastError = VpnError.CertificateExpired;
SetKeepConnectedDuringLocalAgentReconnect(true);
Fire(Trigger.RequireCertificateUpdate, ct);
}
else
{
_logger.Info("The current connection certificate is not null and is different from the " +
"last certificate used. Closing existing TLS channel and reconnecting.");
Fire(Trigger.ConnectionCertificateChanged, ct);
}
}
private async Task StartAvailabilityCheckAsync(CancellationToken ct)
{
if (_vpnConfig == null)
{
_logger.Error("VPN state machine: availability check failed because config is null.");
_lastError = VpnError.Unknown;
Fire(Trigger.AvailabilityFailed, ct);
return;
}
_localAgent.CloseTlsChannel();
await StartDisconnectingTunnelAsync();
if (!IsCurrentSession(ct))
{
return;
}
_lastError = VpnError.None;
try
{
_candidates.Reset();
ProbeAvailabilityResult result = await _connectionProbe.ProbeAvailabilityAsync(_candidates, _vpnConfig, ct);
if (!IsCurrentSession(ct))
{
return;
}
_lastError = result.Error;
if (result.Success)
{
Fire(Trigger.AvailabilitySucceeded, ct);
}
else
{
Fire(Trigger.AvailabilityFailed, ct);
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
if (!IsCurrentSession(ct))
{
return;
}
_logger.Error("VPN state machine availability check failed unexpectedly.", ex);
_lastError = VpnError.Unknown;
Fire(Trigger.AvailabilityFailed, ct);
}
}
private async Task StartSelectingEndpointAsync(CancellationToken ct)
{
if (_vpnConfig == null)
{
_logger.Error("VPN state machine: endpoint selection failed because config is null.");
_lastError = VpnError.Unknown;
Fire(Trigger.DisconnectRequested, ct);
return;
}
try
{
_candidates.Reset();
VpnEndpoint endpoint = await _connectionProbe.SelectEndpointAsync(_candidates, _vpnConfig, ct);
if (!IsCurrentSession(ct))
{
return;
}
if (endpoint.Server.IsEmpty())
{
_lastError = VpnError.PingTimeoutError;
Fire(Trigger.EndpointSelectionFailed, ct);
return;
}
_selectedEndpoint = endpoint;
if (_vpnConfig is not null)
{
_vpnStateSideEffects.UpdateSplitTunnelContext(new SplitTunnelContext(_vpnConfig, _selectedEndpoint));
}
Fire(Trigger.EndpointSelected, ct);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
if (!IsCurrentSession(ct))
{
return;
}
_logger.Error("VPN state machine endpoint selection failed unexpectedly.", ex);
_lastError = VpnError.Unknown;
Fire(Trigger.EndpointSelectionFailed, ct);
}
}
private async Task EstablishTunnelAsync(CancellationToken ct)
{
if (!IsCurrentSession(ct))
{
return;
}
if (_credentials == null || _vpnConfig == null || _selectedEndpoint == null)
{
string? missingFields = NullFieldFormatter.FormatNullFields(
(nameof(_credentials), _credentials),
(nameof(_vpnConfig), _vpnConfig),
(nameof(_selectedEndpoint), _selectedEndpoint));
_logger.Error($"Can't establish tunnel due to missing data: {missingFields}.");
Disconnect(VpnError.Unknown);
return;
}
VpnEndpoint endpoint = _selectedEndpoint;
VpnCredentials credentials = _credentials.Value;
VpnConfig vpnConfig = _vpnConfig;
vpnConfig.UpdateVpnProtocol(endpoint.VpnProtocol);
_ = Task.Run(() => MonitorTunnelStatesAsync(ct), ct);
VpnError error;
try
{
error = await _tunnelOrchestrator.ConnectAsync(endpoint, credentials, vpnConfig, ct);
if (!IsCurrentSession(ct))
{
return;
}
}
catch (OperationCanceledException)
{
_logger.Info("Tunnel establishment was canceled.");
return;
}
catch (Exception ex)
{
_logger.Error("Tunnel establishment failed.", ex);
error = VpnError.Unknown;
}
if (error == VpnError.None)
{
LocalAgentTlsCredentials? localAgentTlsCredentials = await _localAgentTlsCredentialsCache.GetAsync(ct);
if (localAgentTlsCredentials is null || string.IsNullOrEmpty(localAgentTlsCredentials.ConnectionCertificate.Pem))
{
Fire(Trigger.ConnectedToGuestHole, ct);
}
else
{
Fire(Trigger.LocalAgentConnectionRequested, ct);
}
}
else
{
Disconnect(error);
}
}
private async Task MonitorTunnelStatesAsync(CancellationToken ct)
{
try
{
while (!ct.IsCancellationRequested)
{
VpnState state = await _tunnelOrchestrator.StateChannel.Reader.ReadAsync(ct);
if (state.Error == VpnError.None)
{
if (state.Status == VpnStatus.AssigningIp)
{
await _vpnStateSideEffects.ApplyAsync(
CreateDecoratedState(VpnStatus.AssigningIp, state.Error, state.ConnectionCertificate),
MachineState);
}
}
else
{
Disconnect(state.Error);
}
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logger.Error("Tunnel state monitor failed.", ex);
}
}
private VpnState CreateDecoratedState(VpnStatus status, VpnError error, ConnectionCertificate? connectionCertificate = null)
{
VpnHost? server = _selectedEndpoint?.Server ?? (_servers.Count > 0 ? _servers[0] : null);
VpnProtocol vpnProtocol = _vpnConfig?.VpnProtocol ?? VpnProtocol.Smart;
string? remoteIp = server?.Ip;
if (string.IsNullOrEmpty(remoteIp) && server?.RelayIpByProtocol.ContainsKey(vpnProtocol) == true)
{
remoteIp = server?.RelayIpByProtocol[vpnProtocol];
}
return new VpnState(
status: status,
error: error,
localIp: _tunnelOrchestrator.VpnConnection?.LocalIpv4Address ?? string.Empty,
remoteIp: remoteIp,
endpointPort: _selectedEndpoint?.Port ?? 0,
vpnProtocol: vpnProtocol,
portForwarding: !_hasPortForwardingError && (_vpnConfig?.PortForwarding ?? false),
openVpnAdapter: _vpnConfig?.VpnProtocol.IsOpenVpn() == true
? _vpnConfig.OpenVpnAdapter
: null,
label: server?.Label ?? string.Empty,
connectionCertificate: connectionCertificate);
}
public void SubscribeToStateChanged(Func action)
{
_machine.OnTransitioned(async transition =>
{
try
{
VpnState state = await MapStateAsync(transition.Destination);
await action(state);
}
catch (OperationCanceledException)
{
}
});
}
private async Task HandleTransitionedAsync(StateMachine.Transition transition)
{
_logger.Debug($"VPN state machine transitioned from {transition.Source} " +
$"to {transition.Destination} state, " +
$"Trigger: ({transition.Trigger}).");
VpnState sideEffectState = await MapStateAsync(transition.Destination);
await _vpnStateSideEffects.ApplyAsync(sideEffectState, transition.Destination);
}
private async Task MapStateAsync(State state)
{
ConnectionCertificate? connectionCertificate = null;
try
{
connectionCertificate = (await _localAgentTlsCredentialsCache.GetAsync(GetSessionToken()))?.ConnectionCertificate;
}
catch (OperationCanceledException)
{
}
return CreateDecoratedState(MapStatus(state), _lastError, connectionCertificate);
}
private VpnStatus MapStatus(State state)
{
bool keepConnectedDuringLocalAgentReconnect = GetKeepConnectedDuringLocalAgentReconnect();
if (keepConnectedDuringLocalAgentReconnect && state is State.EstablishingLocalAgentChannel)
{
state = State.Connected;
}
return state switch
{
State.Disconnected => VpnStatus.Disconnected,
State.AvailabilityCheck or
State.SelectingEndpoint => VpnStatus.Pinging,
State.EstablishingTunnel => VpnStatus.Connecting,
State.EstablishingLocalAgentChannel => VpnStatus.AssigningIp,
State.Connected => VpnStatus.Connected,
State.ActionRequired => VpnStatus.ActionRequired,
_ => throw new NotImplementedException(),
};
}
}