/* * 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.Threading; using System.Threading.Tasks; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.ConnectLogs; using ProtonVPN.Logging.Contracts.Events.ProtocolLogs; namespace ProtonVPN.Vpn.WireGuard; public class WireGuardStateMonitor : IWireGuardStateMonitor { private const int SKIP_LOG_CHARACTERS = 27; private const int MAX_SOCKET_ERRORS = 5; private const string NT_HANDSHAKE_SUCCESS_MESSAGE = "Receiving handshake response from peer"; private const string WINTUN_HANDSHAKE_SUCCESS_MESSAGE = "Received handshake response"; private const string ADAPTER_IN_USE_MESSAGE = "Unable to configure adapter network settings: unable to set ips: The object already exists"; private readonly ILogger _logger; private readonly RingLogger _ringLogger; private VpnError _lastError = VpnError.None; private bool _isHandshakeResponseHandled; private int _socketErrorCount; public WireGuardStateMonitor(ILogger logger, IStaticConfiguration config) { _logger = logger; _ringLogger = new RingLogger(config.WireGuard.LogFilePath); } public async IAsyncEnumerable WatchStatesAsync([EnumeratorCancellation] CancellationToken cancellationToken) { _socketErrorCount = 0; _lastError = VpnError.None; _ringLogger.Start(); _isHandshakeResponseHandled = false; await foreach (VpnState state in ReceiveLogsAsync(cancellationToken).WithCancellation(cancellationToken)) { yield return state; } } private async IAsyncEnumerable ReceiveLogsAsync([EnumeratorCancellation] CancellationToken cancellationToken) { uint cursor = RingLogger.CursorAll; using PeriodicTimer timer = new(TimeSpan.FromMilliseconds(300)); try { while (!cancellationToken.IsCancellationRequested) { List lines = _ringLogger.FollowFromCursor(ref cursor); foreach (VpnState state in CreateStates(lines)) { yield return state; } if (!await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) { yield break; } } } finally { _ringLogger.Stop(); } } private List CreateStates(List lines) { bool isAdapterInUse = IsAdapterInUse(lines); List states = []; foreach (string line in lines) { _logger.Info(GetFormattedMessage(line)); if (TryCreateState(line, out VpnState? vpnState) && vpnState is not null) { // When a native WireGuard client is connected, we receive "Startup complete" and then immediately fail with // "unable to set ips: The object already exists" before shutting down with WireGuardAdapterInUseError. // The interface never actually comes up, so we don't want to send AssigningIp state in this case. if (isAdapterInUse && vpnState.Status == VpnStatus.AssigningIp) { continue; } states.Add(vpnState); } } return states; } private static bool IsAdapterInUse(List lines) { foreach (string line in lines) { if (line.Contains(ADAPTER_IN_USE_MESSAGE)) { return true; } } return false; } private bool TryCreateState(string line, out VpnState? vpnState) { vpnState = null; bool isHandshakeSuccess = line.Contains(NT_HANDSHAKE_SUCCESS_MESSAGE) || line.Contains(WINTUN_HANDSHAKE_SUCCESS_MESSAGE); if (isHandshakeSuccess && !_isHandshakeResponseHandled) { _logger.Info("Invoking connected state after receiving successful handshake response."); _isHandshakeResponseHandled = true; vpnState = CreateState(VpnStatus.Connected); return true; } if (line.Contains("Shutting down")) { vpnState = CreateState(VpnStatus.Disconnected, _socketErrorCount > 0 ? VpnError.Unknown : _lastError); _lastError = VpnError.None; _socketErrorCount = 0; return true; } if (line.Contains("The RPC server is unavailable")) { _lastError = VpnError.RpcServerUnavailable; return false; } if (line.Contains("Could not install driver")) { _lastError = VpnError.NoTapAdaptersError; return false; } if (line.Contains(ADAPTER_IN_USE_MESSAGE)) { _lastError = VpnError.WireGuardAdapterInUseError; return false; } if (line.Contains("interface has Forwarding/WeakHostSend enabled")) { vpnState = CreateState(VpnStatus.Disconnected, VpnError.InterfaceHasForwardingEnabled); return true; } if (line.Contains("Startup complete")) { vpnState = CreateState(VpnStatus.AssigningIp); return true; } if (line.Contains("SOCKET ERROR:")) { if (_socketErrorCount >= MAX_SOCKET_ERRORS) { _logger.Info($"Invoking disconnected state after {MAX_SOCKET_ERRORS} socket errors."); _socketErrorCount = 0; vpnState = CreateState(VpnStatus.Disconnected, VpnError.Unknown); return true; } _socketErrorCount++; } return false; } private VpnState CreateState(VpnStatus status, VpnError error = VpnError.None) { return new VpnState(status, error, VpnProtocol.WireGuardUdp); } private string GetFormattedMessage(string message) { return message.Length > SKIP_LOG_CHARACTERS ? message.Substring(SKIP_LOG_CHARACTERS, message.Length - SKIP_LOG_CHARACTERS).Trim() : message; } }