Comunicação entre processos com o gRPC

Os processos em execução no mesmo computador podem ser projetados para se comunicar uns com os outros. Os sistemas operacionais fornecem tecnologias para habilitar uma comunicação entre processos (IPC) rápida e eficiente. Exemplos populares de tecnologias de IPC são soquetes de domínio Unix e pipes nomeados.

O .NET fornece suporte para comunicação entre processos usando o gRPC.

O suporte interno para pipes nomeados no ASP.NET Core requer o .NET 8 ou versões posteriores.

Introdução

As chamadas IPC são enviadas de um cliente para um servidor. Para se comunicar entre aplicativos em um computador com o gRPC, pelo menos um aplicativo deve hospedar um servidor gRPC do ASP.NET Core.

Um servidor gRPC do ASP.NET Core geralmente é criado a partir do modelo gRPC. O arquivo de projeto criado pelo modelo usa o Microsoft.NET.SDK.Web como o 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>

O valor do SDK Microsoft.NET.SDK.Web adiciona automaticamente uma referência à estrutura do ASP.NET Core. A referência permite que o aplicativo use os tipos ASP.NET Core necessários para hospedar um servidor.

Também é possível adicionar um servidor aos projetos existentes não sejam do ASP.NET Core, comoi Windows Services, aplicativos WPF ou aplicativos WinForms. Confira Hospedar gRPC em projetos que não são do ASP.NET Core para mais informações.

Transportes de IPC (comunicação entre processos)

As chamadas gRPC entre um cliente e um servidor em computadores diferentes geralmente são enviadas por meio de soquetes TCP. TCP é uma ótima opção para se comunicar em uma rede ou na Internet. No entanto, os transportes IPC oferecem vantagens ao se comunicar entre os processos no mesmo computador:

  • Menos sobrecarga e velocidades de transferência mais rápidas.
  • Integração com os recursos de segurança do sistema operacional.
  • Não usa portas TCP, que são um recurso limitado.

O .NET dá suporte a vários transportes IPC:

Os aplicativos multiplataforma podem escolher diferentes transportes IPC, dependendo do sistema operacional. Um aplicativo pode verificar o sistema operacional na inicialização e escolher o transporte desejado para essa 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;
    });
});

Considerações sobre segurança

Os aplicativos IPC enviam e recebem chamadas RPC. A comunicação externa é um possível vetor de ataque para aplicativos IPC e deve ser adequadamente protegida.

Proteja o aplicativo do servidor IPC contra chamadas inesperadas

O aplicativo do servidor IPC hospeda os serviços RPC para serem chamados por outros aplicativos. Os chamadores que chegam devem ser autenticados para evitar que clientes não confiáveis façam chamadas RPC para o servidor.

A segurança do transporte é uma opção para proteger um servidor. Os transportes IPC, como soquetes de domínio Unix e pipes nomeados, dão suporte à limitação de acesso com base nas permissões do sistema operacional:

  • Os pipes nomeados dão suporte à proteção de um pipe com o modelo de controle de acesso do Windows. Os direitos de acesso podem ser configurados no .NET quando um servidor é iniciado, utilizando a classe PipeSecurity.
  • Os soquetes de domínio Unix dão suporte à proteção de um soquete com permissões de arquivo.

Uma opção para proteger um servidor IPC é utilizar a autenticação e a autorização predefinidas do ASP.NET Core. Por exemplo, o servidor pode ser configurado para exigir a autenticação do certificado. As chamadas RPC feitas por aplicativos clientes sem o certificado exigido falham com uma resposta não autorizada.

Validar o servidor no aplicativo cliente IPC

É importante que o aplicativo cliente valide a identidade do servidor que está chamando. A validação é necessária para proteger contra a possibilidade de um agente mal-intencionado interromper o servidor confiável, executar o seu próprio servidor e aceitar a entrada de dados dos clientes.

Os pipes nomeados fornecem suporte para obter a conta sob a qual um servidor está sendo executado. Um cliente pode validar que o servidor foi iniciado pela conta esperada:

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

Outra opção para validar o servidor é proteger seus pontos de extremidade com HTTPS dentro do ASP.NET Core. O cliente pode configurar SocketsHttpHandler para validar se o servidor está usando o certificado esperado quando a conexão for estabelecida.

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.
        }
    }
};

Proteger contra o escalonamento de privilégios do pipe nomeado

Os pipes nomeados dão suporte a um recurso chamado representação. Usando a representação, o servidor de pipes nomeados pode executar códigos com os privilégios do usuário cliente. Essa é uma funcionalidade poderosa, mas pode permitir que um servidor de baixo privilégio se passe por um agente de alto privilégio e, em seguida, execute código malicioso.

Os clientes podem se proteger contra esse ataque não permitindo a representação ao se conectarem a um servidor. A menos que seja exigido por um servidor, um valor TokenImpersonationLevel de None ou Anonymous deve ser utilizado ao criar uma conexão do cliente:

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

TokenImpersonationLevel.None é o valor padrão em NamedPipeClientStream construtores que não têm um parâmetro impersonationLevel.

Configurar cliente e servidor

O cliente e o servidor devem ser configurados para usar um transporte de IPC (comunicação entre processos). Para obter mais informações sobre como configurar Kestrel e SocketsHttpHandler para usar o IPC:

Observação

O suporte interno para pipes nomeados no ASP.NET Core requer o .NET 8 ou versões posteriores.

Os processos em execução no mesmo computador podem ser projetados para se comunicar uns com os outros. Os sistemas operacionais fornecem tecnologias para habilitar uma comunicação entre processos (IPC) rápida e eficiente. Exemplos populares de tecnologias de IPC são soquetes de domínio Unix e pipes nomeados.

O .NET fornece suporte para comunicação entre processos usando o gRPC.

Observação

O suporte interno para pipes nomeados no ASP.NET Core requer o .NET 8 ou versões posteriores.
Para obter mais informações, confira o .NET 8 ou uma versão posterior deste tópico

Introdução

As chamadas gRPC são enviadas de um cliente para um servidor. Para se comunicar entre aplicativos em um computador com o gRPC, pelo menos um aplicativo deve hospedar um servidor gRPC do ASP.NET Core.

ASP.NET Core e gRPC podem ser hospedados em qualquer aplicativo usando o .NET Core 3.1 ou posterior adicionando a estrutura Microsoft.AspNetCore.App ao projeto.

<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>

O arquivo de projeto anterior:

  • Adiciona uma referência de estrutura a Microsoft.AspNetCore.App. A referência de estrutura permite que os aplicativos que não são do ASP.NET Core, como os Serviços Windows, aplicativos WPF ou aplicativos WinForms, usem o ASP.NET Core e hospedem um servidor do ASP.NET Core.
  • Adiciona uma referência do pacote NuGet a Grpc.AspNetCore.
  • Adiciona um arquivo .proto.

Configurar soquetes de domínio do Unix

As chamadas gRPC entre um cliente e um servidor em computadores diferentes geralmente são enviadas por meio de soquetes TCP. O TCP foi projetado para se comunicar em uma rede. Os UDS (soquetes de domínio do Unix) são uma tecnologia de IPC com amplo suporte que são mais eficientes do que o TCP quando o cliente e o servidor estão no mesmo computador. O .NET fornece suporte interno para UDS em aplicativos cliente e servidor.

Requisitos:

Configuração de Servidor

Os UDSs (soquetes de domínio do Unix) são compatíveis com Kestrel, que está configurado em 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;
                });
            });
        });

O exemplo anterior:

Configuração do cliente

GrpcChannel dá suporte à realização de chamadas gRPC em transportes personalizados. Quando um canal é criado, ele pode ser configurado com um SocketsHttpHandler que tem um ConnectCallback personalizado. O retorno de chamada permite que o cliente faça conexões por meio de transportes personalizados e envie solicitações HTTP por esse transporte.

Exemplo de fábrica de conexões de soquetes de domínio 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;
        }
    }
}

Usar a fábrica de conexões personalizadas para criar um 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
    });
}

Os canais criados usando o código anterior enviam chamadas gRPC por soquetes de domínio Unix. O suporte para outras tecnologias de IPC pode ser implementado usando a extensibilidade no Kestrel e SocketsHttpHandler.