/* * Copyright (c) 2024 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.IO.Pipes; using System.Security.AccessControl; using System.Security.Principal; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ProtoBuf.Grpc.Server; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Logging.Contracts.Events.ProcessCommunicationLogs; using ProtonVPN.Crypto; using ProtonVPN.IssueReporting.Contracts; using ProtonVPN.OperatingSystems.Processes.Contracts; using ProtonVPN.OperatingSystems.Registries.Contracts; using ProtonVPN.ProcessCommunication.Common; using ProtonVPN.ProcessCommunication.Contracts; using ProtonVPN.ProcessCommunication.Contracts.Controllers; using ILogger = ProtonVPN.Logging.Contracts.ILogger; namespace ProtonVPN.ProcessCommunication.Service; public class GrpcServer : IGrpcServer { // AUTHENTICATED_USERS - A group that includes all users whose identities were authenticated // when they logged on. Users authenticated as Guest or Anonymous are not members of this group. private const string AUTHENTICATED_USERS_SID = "S-1-5-11"; private readonly RegistryUri _registryUri = RegistryUri.CreateLocalMachineUri( NamedPipeConfiguration.REGISTRY_PATH, NamedPipeConfiguration.REGISTRY_KEY); private readonly ILogger _logger; private readonly IIssueReporter _issueReporter; private readonly IClientController _clientController; private readonly IUpdateController _updateController; private readonly IVpnController _vpnController; private readonly IPipeStreamProcessIdentifier _pipeStreamProcessIdentifier; private readonly IConfiguration _config; private readonly IRegistryEditor _registryEditor; private WebApplication _app; private CancellationTokenSource _cancellationTokenSource = new(); public event EventHandler InvokingServiceStop; public GrpcServer(ILogger logger, IIssueReporter issueReporter, IClientController clientController, IUpdateController updateController, IVpnController vpnController, IPipeStreamProcessIdentifier pipeStreamProcessIdentifier, IConfiguration configuration, IRegistryEditor registryEditor) { _logger = logger; _issueReporter = issueReporter; _clientController = clientController; _updateController = updateController; _vpnController = vpnController; _pipeStreamProcessIdentifier = pipeStreamProcessIdentifier; _config = configuration; _registryEditor = registryEditor; } public void CreateAndStart() { if (!_cancellationTokenSource.Token.IsCancellationRequested) { _app = Create(); RunAppAsync().FireAndForget(); } } private async Task RunAppAsync() { try { await _app.RunAsync(); } catch (OperationCanceledException) { _logger.Info("gRPC server stopped."); } catch (Exception ex) { _logger.Error("gRPC server stopped unexpectedly.", ex); } if (_cancellationTokenSource.Token.IsCancellationRequested) { await DisposeAppAsync(); } else { await WaitAndRestartAppAsync(); } } private async Task WaitAndRestartAppAsync() { await DisposeAppAsync(); try { await Task.Delay(TimeSpan.FromSeconds(3), _cancellationTokenSource.Token); } catch (OperationCanceledException) { _logger.Info("gRPC server restart cancelled."); return; } CreateAndStart(); } private async Task DisposeAppAsync() { WebApplication app = _app; if (app is not null) { await app.DisposeAsync(); } } public async Task StopAsync() { _cancellationTokenSource.Cancel(); DeletePipeNameFromRegistry(); // The code below is kept commented to remind why it should not be done. Stopping the gRPC server gracefully // leads to the client side Stream to take a long time to detect the server is down. When the gRPC server // suddenly dies as in this case or if the service is killed, the client side Stream immediately detects it. //await _app?.StopAsync(); } private WebApplication Create() { DeletePipeNameFromRegistry(); string pipeName = GeneratePipeName(); WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); ConfigureKestrel(builder, pipeName); PipeSecurity pipeSecurity = CreatePipeSecurity(); ConfigureNamedPipes(builder, pipeSecurity); builder.Services.AddSingleton(_clientController); builder.Services.AddSingleton(_updateController); builder.Services.AddSingleton(_vpnController); builder.Services.AddCodeFirstGrpc(); builder.Services.AddGrpc(options => { options.EnableDetailedErrors = true; }); WebApplication app = builder.Build(); app.UseMiddleware(_logger, _issueReporter, _config, _pipeStreamProcessIdentifier, _registryEditor, InvokingServiceStop, WaitAndRestartAppAsync); app.MapGrpcService(); app.MapGrpcService(); app.MapGrpcService(); app.Lifetime.ApplicationStarted.Register(() => WritePipeNameToRegistry(pipeName)); app.Lifetime.ApplicationStopping.Register(DeletePipeNameFromRegistry); return app; } private void ConfigureKestrel(WebApplicationBuilder builder, string pipeName) { builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Listen(new NamedPipeEndPoint(pipeName), listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; }); }); } private string GeneratePipeName() { return $"ProtonVPN-{HashGenerator.GenerateRandomString(32)}"; } private PipeSecurity CreatePipeSecurity() { SecurityIdentifier targetSid = new(AUTHENTICATED_USERS_SID); PipeSecurity pipeSecurity = new(); pipeSecurity.AddAccessRule( new PipeAccessRule( targetSid, PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow ) ); return pipeSecurity; } private void ConfigureNamedPipes(WebApplicationBuilder builder, PipeSecurity pipeSecurity) { builder.WebHost.UseNamedPipes(serverOptions => { serverOptions.PipeSecurity = pipeSecurity; serverOptions.CurrentUserOnly = false; serverOptions.MaxWriteBufferSize = 100 * 1024 * 1024; // 100MB serverOptions.MaxReadBufferSize = 100 * 1024 * 1024; // 100MB serverOptions.ListenerQueueCount = 1; }); } private void WritePipeNameToRegistry(string pipeName) { _registryEditor.WriteString(_registryUri, pipeName); } private void DeletePipeNameFromRegistry() { _registryEditor.Delete(_registryUri); } }