How to Use Named Pipes with ASP.NET Core and HTTPClient

This post explains Windows named pipes: what they are, how to use them with ASP.NET Core, and how to use HttpClient to call an ASP.NET Core application that has a named pipe.

What are Windows named pipes?

A named, one-way or duplex pipe for communication between a client and a server is provided by Windows named pipes. They offer a channel of communication between various processes, usually running on the same machine.

Named pipes also allow you to call distant servers by their network name, though from what I’ve heard, this is less common.

A duplex named pipe can be compared to a TCP/IP connection in that it allows data transmission and reception from the server. Named pipes are not unique to.NET; rather, they are a Windows kernel feature that has been available from.NET since (at least).NET Framework 3.5.

Every named pipe has a distinct name that resembles this:

\\<ServerName>\pipe\<PipeName>

where <PipeName> is the name of the pipe, which is up-to 256 characters long, can contain any character except \, and is not case-sensitive. If you’re using a named pipe on the same server (the typical usage), then you must use . for the server name. For example:

\\.\pipe\my-pipe-name

Note that ASP.NET Core only support local named-pipes, so the server name will always be ..

You can create named pipes that are write-only, for example (think UDP equivalent), thanks to the different modes and types available for named pipes. However, duplex—where clients can send requests and receive responses—is presumably more prevalent.

There are many more modes and configuration options for named pipes, but I won’t cover them all in this post because I don’t fully understand them. One last thing to keep in mind, though, is that named pipes will only function synchronously unless you specifically choose to enable asynchronous operations for them.

Why use named pipes?

Named pipes are primarily designed for communication within a single machine, but they also facilitate inter-process communication. Why then would you pick them over TCP/IP, for instance, when single-server communication could be accomplished using the loopback address (localhost)?

Listed pipes are a good option for the following reasons:

  • Windows security and named pipes work well together to restrict which clients can access the pipe.
  • Named pipes enable impersonation, which allows the server to run code with the client user’s permissions. Of course, there are security dragons here
  • It is possible for certain environments to restrict TCP port access across process tree boundaries.
  • Reusing TCP ports may present issues.

The first two things are highly specific to Windows. If that’s what you require, using named pipes to obtain these features might be simpler, though I doubt there’s much demand for that these days.

Specifically, impersonation is a feature that seems like a great idea on the surface, but it also raises questions about potential privilege escalation. Fortunately, the client can prevent this by setting a flag when it connects, and the server must explicitly decide whether to impersonate a client.

Using ASP.NET Core and Kestrel to create a named pipe server

Although it’s not exactly simple or easy, you could implement a named pipe server prior to.NET 8. If you want to use the HTTP protocol for communication, you would first need to implement your own HTTP server on top of it.

Fortunately, direct support for Windows named pipes was added by ASP.NET Core to Kestrel in .NET 8, allowing you to use all the features and programming model of ASP.NET Core that you are accustomed to from TCP.

It’s easy to set up your application to listen via named pipes by doing one of the following:

  • Configuring Kestrel in code using ListenNamedPipe()
  • Setting the URLs for your application to http://pipe:/<pipename>

A very basic .NET 8 test application is displayed in the example below, which was created using the dotnet new web empty template. The only thing that has changed is that I specifically set up the application to use named pipes for listening:

var builder = WebApplication.CreateBuilder(args);

//    Configure Kestrel to listen on \\.\pipe\my-test-pipe
// 👇 Note that you only provide the final part of the pipe name
builder.WebHost.ConfigureKestrel(
    opts => opts.ListenNamedPipe("my-test-pipe"));

var app = builder.Build();

app.MapGet("/test", () =>  "Hello world!");

app.Run();

When you launch the application using dotnet run, the listen address will be displayed as follows in the logs:

info: Microsoft.Hosting.Lifetime[14]                 
      Now listening on: http://pipe:/my-pipe-name

Although it appears a little strange, the http://pipe: makes some sense. Ultimately, HTTP requests are still being sent; they are just being sent over a named pipe rather than a TCP socket.

You can also configure Kestrel entirely using IConfiguration, so instead of adding the ConfigureKestrel() call above, you could instead define the listen URL in your appsettings.json file like this:

{
  "Kestrel": {
    "Endpoints": {
      "NamedPipeEndpoint": {
        "Url": "http://pipe:/my-pipe-name"
      }
    }
  }
}

In addition, you can opt-in to binding named pipes using the same binding address pattern as the configuration above by utilizing any of the other methods I outlined in my previous post. As an illustration:

export ASPNETCORE_URLS="http://pipe:/my-pipe-name"

Just in case you were wondering, Kestrel will use HTTPS for requests even if you specify https in your URLs!

ASP.NET Core exposes various settings for customizing the named pipe, such as buffer sizes and pipe security options. You can configure these using IConfiguration or by passing a configuration lambda to builder.WebHost.UseNamedPipes():

var builder = WebApplication.CreateBuilder(args);

// Customize the named pipe configuration
builder.WebHost.UseNamedPipes(opts =>
{ 
    // Bump the buffer sizes to 4MB (defaults to 1MB)
    opts.MaxWriteBufferSize = 4 * 1024 * 1024;
    opts.MaxReadBufferSize = 4 * 1024 * 1024;
});

// ...

In fact, using named pipes requires calling UseNamedPipes() because it adds the necessary services. It is added to the DI container automatically by default, so you don’t need to call it explicitly.

Calling the named pipe server with HttpClient

Thus, it’s surprisingly simple to create an HTTP server (or gRPC server, or anything else supported by ASP.NET Core!) that listens over named pipes in.NET 8. However, we need a client because you probably also want to be able to send requests to the server.

We will need SocketsHttpHandler and our reliable HttpClient for this. Although SocketsHttpHandler was first introduced in.NET Core 2.1, the ConnectCallback property—which was added in.NET 5—must be used specifically.

The code below is based on a NamedPipeClientStream that is technically available as far back as.NET Framework 3.5. However, in order to use this directly, you must create your own HTTP client. Similar to constructing your own HTTP server, I advise against it if at all possible!

To configure an HttpClient to use named pipes you need to specify a custom ConnectCallback() which will create a NamedPipeClientStream instance and connect to the server. After configuring the client, you just use it like you would to call any other ASP.NET Core app.

using System.IO.Pipes;

var httpHandler = new SocketsHttpHandler
{
    // Called to open a new connection
    ConnectCallback = async (ctx, ct) =>
    {
        // Configure the named pipe stream
        var pipeClientStream = new NamedPipeClientStream(
            serverName: ".", // 👈 this machine
            pipeName: "my-test-pipe", // 👈 
            PipeDirection.InOut, // We want a duplex stream 
            PipeOptions.Asynchronous); // Always go async

        // Connect to the server!
        await pipeClientStream.ConnectAsync(ct);
        
        return pipeClientStream;
    }
};

// Create an HttpClient using the named pipe handler
var httpClient = new HttpClient(httpHandler)
{
    BaseAddress = new Uri("http://localhost"); // 👈 Use localhost as the base address
};

var result = await httpClient.GetStringAsync("/test");
Console.WriteLine(result);

The only “oddity” here is that you need to give use a valid hostname in the BaseAddress of the HttpClient, or alternatively in the GetStringAsync() if you don’t specify a BaseAddress. I’ve used localhost for simplicity, but it doesn’t really matter what we use here. If you run the app, it should print "Hello World!" 🎉

If you have set up ASP.NET Core to listen for HTTPS over named pipes, don’t forget to use https: in the BaseAddress!

And that’s the sum total of it. With.NET 8, using named pipes is so much simpler than it was in previous iterations—it should Just WorkTM. Having said that, since the named pipe support is clearly still in its infancy, it’s wise to keep a watch out for any problems and notify the.NET team of them!

Conclusion

I discussed Windows named pipes in this post. I talked about what they are, how they function, and a few situations in which they might be helpful. They are especially helpful in situations where cross-process TCP communication is an issue or when you wish to use particular Windows security features. I then went over how to set up your application to listen on a named pipe and the named pipe support that was added to ASP.NET Core in.NET 8. Finally, I showed how you can use HttpClient with a custom SocketsHttpHandler.ConnectCallback to make requests to a named pipe HTTP server.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *