Comunicación entre procesos con gRPC

Los procesos que se ejecutan en la misma máquina se pueden diseñar para comunicarse entre sí. Los sistemas operativos proporcionan tecnologías para permitir una comunicación entre procesos (IPC) rápida y eficaz. Ejemplos populares de tecnologías de IPC son los sockets de dominio de Unix y las canalizaciones con nombre.

.NET proporciona asistencia para la comunicación entre procesos mediante gRPC.

La compatibilidad integrada con canalizaciones con nombre en ASP.NET Core requiere .NET 8 o posterior.

Introducción

Las llamadas IPC se envían desde un cliente a un servidor. Para comunicarse entre aplicaciones en un equipo con gRPC, al menos una aplicación debe hospedar un servidor gRPC de ASP.NET Core.

Normalmente, un servidor gRPC de ASP.NET Core se crea a partir de la plantilla de gRPC. El archivo de proyecto creado por la plantilla usa Microsoft.NET.SDK.Web como SDK:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>

El valor del SDK Microsoft.NET.SDK.Web agrega automáticamente una referencia al marco ASP.NET Core. La referencia permite que la aplicación use tipos de ASP.NET Core necesarios para hospedar un servidor.

También es posible agregar un servidor a proyectos existentes que no sean de ASP.NET Core, como servicios de Windows, aplicaciones WPF o aplicaciones WinForms. Para obtener más información, consulte Hospedaje de gRPC en proyectos que no son de ASP.NET Core.

Transportes de comunicación entre procesos (IPC)

Las llamadas gRPC entre un cliente y un servidor en equipos diferentes se suelen enviar mediante sockets TCP. TCP es una buena opción para comunicarse a través de una red o Internet. Pero los transportes IPC ofrecen ventajas al comunicarse entre procesos en la misma máquina:

  • Menos sobrecarga y velocidades de transferencia más rápidas.
  • Integración con las características de seguridad del sistema operativo.
  • No usa puertos TCP, que son un recurso limitado.

.NET admite varios transportes IPC:

En función del sistema operativo, las aplicaciones multiplataforma pueden elegir diferentes transportes IPC. Una aplicación puede comprobar el sistema operativo al iniciarse y elegir el transporte deseado para esa plataforma:

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    if (OperatingSystem.IsWindows())
    {
        serverOptions.ListenNamedPipe("MyPipeName");
    }
    else
    {
        var socketPath = Path.Combine(Path.GetTempPath(), "socket.tmp");
        serverOptions.ListenUnixSocket(socketPath);
    }

    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
    });
});

Consideraciones sobre la seguridad

Las aplicaciones IPC envían y reciben llamadas RPC. La comunicación externa es un vector de ataque potencial para las aplicaciones IPC y se debe proteger correctamente.

Protección de la aplicación de servidor IPC contra autores de llamada inesperados

La aplicación de servidor IPC hospeda servicios RPC que otras aplicaciones pueden llamar. Los autores de la llamada entrantes se deben autenticar para evitar que los clientes que no sean de confianza realicen llamadas RPC al servidor.

La seguridad de transporte es una opción para proteger un servidor. Los transportes IPC, como los sockets de dominio Unix y las canalizaciones con nombre, admiten la limitación del acceso en función de los permisos del sistema operativo:

  • Las canalizaciones con nombre admiten la protección de una canalización con el modelo de control de acceso de Windows. Los derechos de acceso se pueden configurar en .NET cuando se inicia un servidor mediante la clase PipeSecurity.
  • Los sockets de dominio de Unix admiten la protección de un socket con permisos de archivo.

Otra opción para proteger un servidor IPC consiste en usar la autenticación y la autorización integradas en ASP.NET Core. Por ejemplo, el servidor se podría configurar para exigir la autenticación de certificados. Las llamadas RPC realizadas por las aplicaciones cliente sin el certificado necesario generan un error con una respuesta no autorizada.

Validación del servidor en la aplicación cliente IPC

Es importante que la aplicación cliente valide la identidad del servidor al que llama. La validación es necesaria para protegerse y evitar que un actor malintencionado detenga el servidor de confianza, ejecute sus propios datos y aceptar los datos entrantes de los clientes.

Las canalizaciones con nombre proporcionan compatibilidad para obtener la cuenta en la que se ejecuta un servidor. Un cliente puede validar que la cuenta esperada ha iniciado el servidor:

internal static bool CheckPipeConnectionOwnership(
    NamedPipeClientStream pipeStream, SecurityIdentifier expectedOwner)
{
    var remotePipeSecurity = pipeStream.GetAccessControl();
    var remoteOwner = remotePipeSecurity.GetOwner(typeof(SecurityIdentifier));
    return expectedOwner.Equals(remoteOwner);
}

Otra opción para validar el servidor consiste en proteger sus puntos de conexión con HTTPS dentro de ASP.NET Core. El cliente puede configurar SocketsHttpHandler para validar que el servidor usa el certificado esperado cuando se establece la conexión.

var socketsHttpHandler = new SocketsHttpHandler()
{
    SslOptions = new SslOptions()
    {
        RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
        {
            if (sslPolicyErrors != SslPolicyErrors.None)
            {
                return false;
            }

            // Validate server cert thumbprint matches the expected thumbprint.
        }
    }
};

Protección contra la elevación de privilegios de canalizaciones con nombre

Las canalizaciones con nombre admiten una característica denominada suplantación. Con la suplantación, el servidor de canalizaciones con nombre puede ejecutar código con los privilegios del usuario cliente. Se trata de una característica eficaz, pero puede permitir que un servidor con pocos privilegios suplante a un autor de la llamada con privilegios elevados y, después, ejecutar código malintencionado.

El cliente puede protegerse contra este ataque al no permitir la suplantación al conectarse a un servidor. A menos que sea necesario para un servidor, se debe usar un valor TokenImpersonationLevel de None o Anonymous al crear una conexión de cliente:

using var pipeClient = new NamedPipeClientStream(
    serverName: ".", pipeName: "testpipe", PipeDirection.In, PipeOptions.None, TokenImpersonationLevel.None);
await pipeClient.ConnectAsync();

TokenImpersonationLevel.None es el valor predeterminado en constructores NamedPipeClientStream que no tienen un parámetro impersonationLevel.

Configuración del cliente y del servidor

El cliente y el servidor deben configurarse para usar un transporte de comunicación entre procesos (IPC). Para obtener más información acerca de cómo configurar Kestrel y SocketsHttpHandler para usar IPC:

Nota

La compatibilidad integrada con canalizaciones con nombre en ASP.NET Core requiere .NET 8 o posterior.

Los procesos que se ejecutan en la misma máquina se pueden diseñar para comunicarse entre sí. Los sistemas operativos proporcionan tecnologías para permitir una comunicación entre procesos (IPC) rápida y eficaz. Ejemplos populares de tecnologías de IPC son los sockets de dominio de Unix y las canalizaciones con nombre.

.NET proporciona asistencia para la comunicación entre procesos mediante gRPC.

Nota:

La compatibilidad integrada con canalizaciones con nombre en ASP.NET Core requiere .NET 8 o posterior.
Para más información, consulte .NET 8 o una versión posterior de este tema.

Introducción

Las llamadas gRPC se envían desde un cliente a un servidor. Para comunicarse entre aplicaciones en un equipo con gRPC, al menos una aplicación debe hospedar un servidor gRPC de ASP.NET Core.

ASP.NET Core y gRPC se pueden hospedar en cualquier aplicación mediante .NET Core 3.1 o posterior agregando el marco de trabajo Microsoft.AspNetCore.App al proyecto.

<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>

El archivo del proyecto anterior:

  • Agrega una referencia de marco a Microsoft.AspNetCore.App. La referencia de marco permite a aplicaciones no ASP.NET Core, como servicios de Windows, aplicaciones WPF o aplicaciones WinForms, usar ASP.NET Core y hospedar un servidor ASP.NET Core.
  • Agrega una referencia de paquete NuGet a Grpc.AspNetCore.
  • Agrega un archivo .proto.

Configuración de sockets de dominio de Unix

Las llamadas gRPC entre un cliente y un servidor en equipos diferentes se suelen enviar mediante sockets TCP. TCP se ha diseñado para comunicarse a través de una red. Los sockets de dominio Unix (UDS) son una tecnología IPC ampliamente compatible que es más eficaz que TCP cuando el cliente y el servidor están en la misma máquina. .NET proporciona compatibilidad integrada con UDS en aplicaciones cliente y servidor.

Requisitos:

Configuración del servidor

Los sockets de dominio Unix (UDS) son compatibles con Kestrel, que se configura en Program.cs:

public static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "socket.tmp");

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(options =>
            {
                if (File.Exists(SocketPath))
                {
                    File.Delete(SocketPath);
                }
                options.ListenUnixSocket(SocketPath, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http2;
                });
            });
        });

Ejemplo anterior:

Configuración de cliente

GrpcChannel admite la realización de llamadas gRPC a través de transportes personalizados. Cuando se crea un canal, se puede configurar con un elemento SocketsHttpHandler que tenga un elemento ConnectCallback personalizado. La devolución de llamada permite al cliente realizar conexiones a través de transportes personalizados y, después, enviar solicitudes HTTP a través de ese transporte.

Ejemplo de fábrica de conexiones de sockets de dominio de Unix:

public class UnixDomainSocketConnectionFactory
{
    private readonly EndPoint _endPoint;

    public UnixDomainSocketConnectionFactory(EndPoint endPoint)
    {
        _endPoint = endPoint;
    }

    public async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext _,
        CancellationToken cancellationToken = default)
    {
        var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);

        try
        {
            await socket.ConnectAsync(_endPoint, cancellationToken).ConfigureAwait(false);
            return new NetworkStream(socket, true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    }
}

Uso del generador de conexiones personalizadas para crear un canal:

public static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "socket.tmp");

public static GrpcChannel CreateChannel()
{
    var udsEndPoint = new UnixDomainSocketEndPoint(SocketPath);
    var connectionFactory = new UnixDomainSocketConnectionFactory(udsEndPoint);
    var socketsHttpHandler = new SocketsHttpHandler
    {
        ConnectCallback = connectionFactory.ConnectAsync
    };

    return GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
    {
        HttpHandler = socketsHttpHandler
    });
}

Los canales creados con el código anterior envían llamadas gRPC a través de sockets de dominio de Unix. Se puede implementar la compatibilidad con otras tecnologías IPC por medio de la extensibilidad en Kestrel y SocketsHttpHandler.