/*
* 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.ComponentModel;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.IssueReporting.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.AppServiceLogs;
using ProtonVPN.Logging.Contracts.Events.ConnectionLogs;
using ProtonVPN.Logging.Contracts.Events.OperatingSystemLogs;
using ProtonVPN.OperatingSystems.NRPT.Contracts;
using ProtonVPN.OperatingSystems.PowerEvents.Contracts;
using ProtonVPN.ProcessCommunication.Contracts;
using ProtonVPN.ProTun.Contracts;
using ProtonVPN.Service.Firewall;
using ProtonVPN.Service.StateMachine;
using ProtonVPN.Vpn.Common;
namespace ProtonVPN.Service;
internal partial class VpnService : ServiceBase
{
public CancellationToken CancellationToken { get; private set; }
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ILogger _logger;
private readonly IIssueReporter _issueReporter;
private readonly IStaticConfiguration _staticConfig;
private readonly IEnumerable _vpnConnections;
private readonly IIpv6 _ipv6;
private readonly IGrpcServer _grpcServer;
private readonly INrptInvoker _nrptInvoker;
private readonly IProTunManager _proTunManager;
private readonly INrptWatchdogScheduler _nrptWatchdogScheduler;
private readonly INrptWatchdogStarter _nrptWatchdogStarter;
private readonly IVpnConnectionStateMachine _vpnConnectionStateMachine;
public VpnService(
ILogger logger,
IIssueReporter issueReporter,
IStaticConfiguration staticConfig,
IEnumerable vpnConnections,
IIpv6 ipv6,
IGrpcServer grpcServer,
IPowerEventNotifier powerEventNotifier,
INrptInvoker nrptInvoker,
IProTunManager proTunManager,
INrptWatchdogScheduler nrptWatchdogScheduler,
INrptWatchdogStarter nrptWatchdogStarter,
IVpnConnectionStateMachine vpnConnectionStateMachine)
{
_logger = logger;
_issueReporter = issueReporter;
_staticConfig = staticConfig;
_vpnConnections = vpnConnections;
_ipv6 = ipv6;
_grpcServer = grpcServer;
_nrptInvoker = nrptInvoker;
_proTunManager = proTunManager;
_nrptWatchdogScheduler = nrptWatchdogScheduler;
_nrptWatchdogStarter = nrptWatchdogStarter;
_vpnConnectionStateMachine = vpnConnectionStateMachine;
powerEventNotifier.OnResume += OnPowerEventResume;
_grpcServer.InvokingServiceStop += OnInvokingServiceStop;
_cancellationTokenSource = new CancellationTokenSource();
CancellationToken = _cancellationTokenSource.Token;
CanHandleSessionChangeEvent = true;
AutoLog = false; // To disable the event logs "PowerEvent handled successfully by the service."
InitializeComponent();
}
private void OnInvokingServiceStop(object? sender, EventArgs e)
{
Stop();
}
protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
_logger.Info($"Session changed, reason: {changeDescription.Reason}");
if (changeDescription.Reason == SessionChangeReason.SessionLogoff)
{
_logger.Info("Stopping the service due to SessionLogoff.");
Stop();
}
base.OnSessionChange(changeDescription);
}
protected override void OnStart(string[] args)
{
LogEvent("Service is starting");
try
{
if (!IsBfeServiceRunningAndEnabled())
{
_vpnConnectionStateMachine.ReportDisconnected(VpnError.BaseFilteringEngineServiceNotRunning);
return;
}
_grpcServer.CreateAndStart();
_proTunManager.InitializeAsync().FireAndForget();
TriggerDisconnectAsync().FireAndForget();
_nrptInvoker.DeleteRule();
_nrptWatchdogScheduler.Schedule();
_nrptWatchdogStarter.Start();
}
catch (Exception ex)
{
_logger.Error("An error occurred when starting VPN Service.", ex);
LogEvent($"OnStart: {ex}");
_issueReporter.CaptureError(ex);
}
}
protected override async void OnStop()
{
try
{
_logger.Info("Service is stopping");
LogEvent("Service is stopping");
await _vpnConnectionStateMachine.DisconnectAsync();
if (!_ipv6.IsEnabled)
{
_ipv6.Enable(_ipv6.VpnProtocol);
}
await _grpcServer.StopAsync();
}
catch (Exception ex)
{
_logger.Error("An error occurred when stopping VPN Service.", ex);
LogEvent($"OnStop: {ex}");
_issueReporter.CaptureError(ex);
}
finally
{
_cancellationTokenSource.Cancel();
}
}
private async Task TriggerDisconnectAsync()
{
foreach (IVpnConnection vpnConnection in _vpnConnections)
{
await vpnConnection.DisconnectAsync();
}
_vpnConnectionStateMachine.ReportDisconnected(VpnError.None);
}
protected override bool OnPowerEvent(PowerBroadcastStatus powerStatus)
{
_logger.Debug($"Power status changed to {powerStatus}");
if (powerStatus == PowerBroadcastStatus.ResumeSuspend && _vpnConnectionStateMachine.IsConnected)
{
_logger.Info("Resetting connection due to resume from sleep.");
_vpnConnectionStateMachine.Reconnect();
}
return true;
}
private void OnPowerEventResume(object? sender, EventArgs e)
{
_logger.Info($"{nameof(OnPowerEventResume)}");
}
private void LogEvent(string message)
{
try
{
EventLog.WriteEntry(message.Replace('%', '_'));
}
catch (Exception e) when (e is InvalidOperationException or Win32Exception)
{
}
}
private bool IsBfeServiceRunningAndEnabled()
{
string bfeServiceName = _staticConfig.BaseFilteringEngineServiceName;
try
{
using ServiceController serviceController = new(bfeServiceName);
ServiceStartMode bfeServiceStartType = serviceController.StartType;
ServiceControllerStatus bfeServiceStatus = serviceController.Status;
_logger.Info($"{bfeServiceName} Service - Start type: {bfeServiceStartType}, Status: {bfeServiceStatus}");
return bfeServiceStartType != ServiceStartMode.Disabled
&& bfeServiceStatus == ServiceControllerStatus.Running;
}
catch (Exception e)
{
string errorMessage = $"Error checking BFE service status. Service name: {bfeServiceName}.";
_logger.Error(errorMessage, e);
_issueReporter.CaptureError(errorMessage, e.Message);
return true;
}
}
}