/* * 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.Threading.Channels; using ProtonVPN.Common.Core.Networking; using ProtonVPN.IssueReporting.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.ConnectionLogs; using ProtonVPN.Logging.Contracts.Events.ProtocolLogs; using ProtonVPN.ProTun.Generated; using static ProtonVPN.ProTun.Generated.State; namespace ProtonVPN.ProTun.StateChanges; public class ProTunStateChangeHandler : IProTunStateChangeHandler { private readonly ILogger _logger; private readonly IIssueReporter _issueReporter; public Channel StateChannel { get; } = Channel.CreateUnbounded(); private CancellationToken? _cancellationToken; public ProTunStateChangeHandler(ILogger logger, IIssueReporter issueReporter) { _logger = logger; _issueReporter = issueReporter; } public void SetCancellationToken(CancellationToken cancellationToken) { _cancellationToken = cancellationToken; } public async void OnStateChanged(State state) { if (state is Disconnected disconnectedState) { if (disconnectedState.error is null) { await InvokeStateAsync(new(VpnStatus.Disconnected, VpnProtocol.Smart)); } else { _logger.Error($"ProTUN disconnected with error: {disconnectedState.error}"); await InvokeStateAsync(new(VpnStatus.Disconnected, VpnError.Unknown, VpnProtocol.Smart)); } } else if (state is Connected connectedState) { await InvokeStateWithPeerAsync(VpnStatus.Connected, connectedState.peer); } else if (state is WaitingForAction) { await InvokeStateAsync(new(VpnStatus.Waiting, VpnProtocol.Smart)); } else if (state is Connecting connectingState) { PeerConnectionInfo? peer = connectingState.peers.FirstOrDefault(); if (peer is null) // ProTUN sends connecting without peers on a change of peers, or network availability change { await InvokeStateAsync(new(VpnStatus.Connecting, VpnProtocol.Smart)); } else { await InvokeStateWithPeerAsync(VpnStatus.Connecting, peer); } } else { string message = $"The ProTUN state '{state?.GetType().FullName}' is not implemented."; _logger.Error(message); _issueReporter.CaptureError(message); } } private async Task InvokeStateWithPeerAsync(VpnStatus vpnStatus, PeerConnectionInfo peer) { await InvokeStateAsync(new(vpnStatus, remoteIp: peer.entryIp, endpointPort: peer.port, vpnProtocol: MapProtocol(peer.protocol), openVpnAdapter: null, label: GetLabelFromId(peer.peerId))); } private VpnProtocol MapProtocol(Protocol protocol) { switch (protocol) { case Protocol.WireguardUdp: return VpnProtocol.ProTunUdp; case Protocol.WireguardTcp: return VpnProtocol.ProTunTcp; case Protocol.Stealth: return VpnProtocol.ProTunTls; default: _logger.Error($"The protocol '{protocol}' is not implemented in the mapper."); return VpnProtocol.Smart; } } private string GetLabelFromId(string peerId) { string[] parts = peerId.Split('@'); if (parts.Length < 2) { _logger.Error($"Received a peer ID with only {parts.Length} parts (Peer ID: {peerId}) and therefore no Label."); return string.Empty; } if (parts.Length > 2) { _logger.Error($"Received a peer ID with more parts than expected (Received {parts.Length}, Expected 2) (Peer ID: {peerId})."); } return parts[1]; } private async Task InvokeStateAsync(VpnState vpnState) { try { CancellationToken? cancellationToken = _cancellationToken; if (cancellationToken is not null) { await StateChannel.Writer.WriteAsync(vpnState, cancellationToken.Value); } } catch (OperationCanceledException) { } } }