New information has been added to this article since publication.
Refer to the Editor's Update below.

.NET Remoting

Secure Your .NET Remoting Traffic by Writing an Asymmetric Encryption Channel Sink

Stephen Toub

Code download available at:NETRemoting.exe(150 KB)

This article assumes you're familiar with C# and .NET Remoting

Level of Difficulty123

SUMMARY

As .NET Remoting gains popularity in the enterprise space, it must meet business demands for trustworthy computing. Remoting traffic can be secured when objects are hosted in IIS, but when they aren't hosted in IIS, custom security solutions can be developed to secure them. This article provides an in-depth look at writing channel sinks for .NET. It also details the flow of data through custom channel sinks and explains the kinds of manipulations that can be performed on that data.

Contents

Support for HTTPS
Remoting Execution and Extensibility
One Sink or Two?
Designing the Protocol
Implementing the Key Exchange
Asynchronous Processing
Server Cleanup
Creating the Sinks
Configuration
Conclusion

Remoting is the .NET solution to distributed computing. As a successor to DCOM, .NET Remoting fills many of the gaps left by its predecessor and at the same time adds an extraordinary amount of well-conceived functionality. As with any product, however, the initial release rarely contains the entire feature set desired by its users, and in fact user needs may continue to evolve ahead of product releases throughout the life of the product. To this end, .NET Remoting has been built from the ground up to be extensible in the sense that functionality can be added with relative ease by those who need it. One such feature is the ability to secure and encrypt remoting traffic.

With the popularity of remoting in the enterprise space on the rise, there are questions concerning the capabilities of the Microsoft® .NET Framework to encrypt remoting traffic. The most common question I've seen in this arena is whether .NET Remoting supports secure sockets layer (SSL). Unfortunately, the answer is not a simple yes or no.

Support for HTTPS

In conversations about remoting, the word "channel" is used frequently. Its definition can vary, but in general, a channel is the series of sinks through which a remoting call is made. The HTTP channel for .NET is actually a combination of two channels, one used by the client and one used by the server. Each has its own set of sinks, referred to as a "sink chain." The client sink chain ends with the HttpClientTransportSink, which uses the HttpWebRequest and HttpWebReponse classes from the System.Net namespace to handle all HTTP traffic. This brings with it a bonus—since HttpWebRequest and HttpWebResponse support HTTPS, so too does the HTTP client channel. This means that if the server channel supported SSL, you'd be all set.

Just as the client transport sink is an HTTP client, the HttpServerTransportSink is nothing more than a specialized Web server. You can see this by setting up a simple remoting host and connecting to it with a Web browser.

At a bare minimum, a remoting server that can serve up an object requires the registration of a channel and the publishing of an object or class type in some fashion (well-known, activated, and so on). This is done by using classes from the System.Runtime.Remoting namespace:

static void Main(string[] args) { ChannelServices.RegisterChannel(new HttpChannel(8124)); RemotingConfiguration.RegisterWellKnownServiceType( typeof(Person), "Person.soap", WellKnownObjectMode.Singleton); Console.WriteLine ("Press enter to stop the server..."); Console.ReadLine(); }

To show you that this application is indeed a Web server, I'll use Microsoft Internet Explorer to connect to the designated endpoint on the running server in order to request the Web Services Description Language (WSDL) document for the remoted type (as you can do with a Web Service hosted in ASP.NET). The resulting document is shown in Figure 1.

Figure 1 The WSDL

Figure 1** The WSDL **

I connect to the remoting server and make an HTTP request for "/Person.soap?wsdl" in order to retrieve the WSDL for Person, a custom class that derives from MarshalByRefObject. This request is handled on the server by the SdlChannelSink, a member of the server channel sink chain, which generates the appropriate WSDL for the Person class. This description is sent back to the client as an HTTP response:

C:\telnet localhost 8124 GET /Person.soap?wsdl HTTP/1.0 HTTP/1.1 200 OK Content-Type: text/xml Server: MS .NET Remoting, MS .NET CLR 1.0.3705.288 Content-Length: 8683 Connection: Close <?xml version='1.0' encoding='UTF-8'?> <definitions name='Person' targetNamespace='https://schemas.microsoft.com/clr/nsa ...

So now that you know that HttpServerTransportSink is a Web server, the question remains—can you connect to the server using HTTPS? Unfortunately, you can't. The HttpServerTransportSink does not support SSL. However, custom Windows services and other applications like the one I just showed are not the only vehicles for hosting remoted objects. Microsoft Internet Information Services (IIS) in conjunction with ASP.NET can also remote objects, and when that happens, the framework benefits from the support of HTTPS in IIS. An ASP.NET-hosted object can be accessed with a URL such as https://localhost:8124/MyApplication/Person.soap, forcing all sensitive traffic to be encrypted.

Unfortunately, using IIS to host objects will limit you to using HTTP. So if you're using a different transport, or if you're not hosting the objects in IIS, how can you ensure that prying eyes are unable to read your data? One of the beauties of the .NET Remoting framework is its extensibility—the number of ways that you can extend and modify its functionality.

Remoting Execution and Extensibility

When a client requests a reference to a remote object, it is returned a reference to a TransparentProxy—an object that appears to have the same methods and properties as the remote object and that appears to behave in a similar fashion, but which begins the process of delegation when a call is placed.

A call made on the TransparentProxy is passed as a MessageData object into another proxy, an object of a type derived from RealProxy (often, but not necessarily, a RemotingProxy). This proxy in turn creates an IMessage from the MessageData and forwards the message containing information on the method call through a series of message sinks (classes that implement IMessageSink or IDynamicMessageSink) which can all modify or substitute for the message being sent.

The final IMessageSink in the chain should be the formatter sink, which will often implement IClientFormatterSink, a combination of IClientChannelSink and IMessageSink. This sink is responsible for creating transport headers as well as for taking the IMessage and serializing it into a stream. This stream and headers collection, along with the IMessage, which is used only for reference, are then passed through a series of IClientChannelSinks, each of which can substitute a new stream of data to replace the one supplied by the previous sink (at this stage of processing, the IMessage has already been serialized into the stream and thus only serves an informational purpose). The last of these sinks, the transport sink, takes the headers and the stream and uses a means of transport, such as an HTTP connection or a named pipe, to send the data along to the server.

A similar process happens on the server side, but in reverse. The server's transport sink receives both the headers and the stream from the client's transport sink. This data is processed by a series of IServerChannelSink objects (including the SdlChannelSink, which generates the WSDL you saw earlier) just like it was in the client. The formatter, also an IServerChannelSink, takes the headers collection and the stream and deserializes them into a clone of the original IMessage, which is then forwarded through a series of IServerChannelSink, IMessageSink, and IDynamicMessageSink objects until it reaches the final sink, the StackBuilderSink. This sink uses reflection to execute the request and to obtain a return message. The return message works its way back through the sinks, across the transport layer, and back through the client-side sinks until it reaches the proxies where the return value and the out parameters are set.

As you can see, a lot goes on behind the scenes when a remoted object is used. One benefit of this, as shown in Figure 2, is that there are many places to plug in custom functionality. Formatters can be written to serialize and deserialize the IMessage in a special format (if the built-in SOAP and Binary formatters do not suit one's needs). Additional IMessageSinks can be written to modify the actual message being sent. IClientChannelSinks and IServerChannelSinks can be written to modify the streams and the headers collection. Custom RealProxies can be written to trace or modify which methods actually get executed. And Transport sinks can be written to send and receive the data using any transport layer the developer deems appropriate.

Figure 2 Remoting Flow and Interception Points

For encryption sinks, you need to modify the data being transferred between client and server. You don't need any special transport or formatter, nor do you need to modify the message before it is serialized. The best place to plug in (and the most common interception point I've seen used) is between the formatter and the transport sink. This can be accomplished by implementing custom IClientChannelSink and IServerChannelSink classes to handle the encryption of the streams passing through them.

One Sink or Two?

Sometimes it is necessary to create both a client sink and a server sink. When to do this depends on the application as there is no rule stating that all sinks must come in pairs. For applications that require work to be done on both sides, such as an encryption or compression channel, a sink pair is, of course, required. But there are many situations where only one sink is necessary. For example, ASP.NET has a great output caching system whereby the result of the execution of a page is cached. This cached result can be served up in the future when that page is requested rather than having to execute the page again. The remoting framework does not ship with any such caching system built into it, but a server-side sink could be written to perform this processing. The sink could examine the IMessage object passed to it, look in its own cache object to determine whether a previously stored execution exists, and either forward the request up the chain if one doesn't, or immediately return a ReturnMessage based on what is in the cache. The client doesn't need to know that caching is taking place and therefore a client sink isn't necessary (of course, one could also implement a client-side cache, in which case only the client sink would be necessary). A sink that does just this task is available with the code download for this article at the link at the top of this article. The SdlChannelSink mentioned earlier also executes exclusively on the server side.

It's important to note from this example that a sink isn't required to forward anything up the sink chain. While most sinks do so in most cases, the decision is up to the sink. As such, a sink can determine that a message or stream should not be forwarded along the chain, that it should create its own response stream and headers, and immediately send those back down the chain without the message ever reaching the actual destination. For a caching sink, this makes it possible to serve up information from a cache rather than always forwarding on to the next sink. On the client side, a sink can choose to create its own stream and headers and send those to the server instead of forwarding on what was supplied to it by the previous sink. This enables a sink pair to send multiple messages back and forth without those messages even making it out of the chain. Sounds like it would be useful for a security handshake, doesn't it?

Designing the Protocol

Developing a symmetric encryption channel to use 3DES, RC2, or any of the other symmetric encryption algorithms provided in the System.Security.Cryptography namespace, requires two sinks. One will be placed on each side between the formatter and the transport; because you want to work on the serialized stream, you need the sink to come after the formatter on the client and before the formatter on the server. Figure 3 shows the various important parts of these sinks.

Figure 3 Client and Server Sinks

// IClientChannelSink.ProcessMessage public void ProcessMessage(IMessage msg, ITransportHeaders requestHeaders, Stream requestStream, out ITransportHeaders responseHeaders, out Stream responseStream) { // Encrypt the stream requestStream = CryptoHelper.Encrypt(requestStream, _provider); // Send the stream to the next sink in the chain _next.ProcessMessage(msg, requestHeaders, requestStream, out responseHeaders, out responseStream); // Decrypt the response stream responseStream = CryptoHelper.Decrypt(responseStream, _provider); } // IServerChannelSink.ProcessMessage public ServerProcessing ProcessMessage( IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream) { // Decrypt the incoming request stream requestStream = CryptoHelper.Decrypt(requestStream, _provider); // Forward on the request ServerProcessing result = _next.ProcessMessage( sinkStack, requestMsg, requestHeaders, requestStream, out responseMsg, out responseHeaders, out responseStream); // Encrypt the response stream responseStream = CryptoHelper.Encrypt(responseStream, _provider); return result; }

Because this is symmetric encryption, both sinks must share the same key and initialization vector (IV) information (used to populate the SymmetricAlgorithm provider object passed to the CryptoHelper functions). Either both sinks must be provided with this key information up front, or one of them must be able to dynamically generate the key information and securely send it to the other sink.

Algorithms such as RSA, ElGamal, and Rabin are asymmetric encryption algorithms—also known as public key encryption. These algorithms are based on the concept of key pairs, a set of keys mathematically related such that a message encrypted with one key can only be decrypted by the other. Anyone who wants the ability to receive and decrypt encrypted messages creates a key pair. One key, the public key, is distributed to the world. The private key, on the other hand, should be carefully guarded. Anyone can use the public key to encrypt a message, and this encrypted message can only be decrypted by the private key.

Figure 4 Key Exchange Algorithm

Figure 4** Key Exchange Algorithm **

Using asymmetric encryption, the two sinks can solve their key exchange predicament using the process shown in Figure 4. The client sink dynamically generates a key pair and sends the public key to the server sink. The server sink then dynamically creates a symmetric key, encrypts it with the client's public key, and sends the encrypted key back to the client. The client sink can now use his private key to decrypt the symmetric key; almost magically, both sinks now have the same symmetric key and can proceed to share information. Those familiar with this process will note that it is very similar to how a browser establishes a secure connection to a Web server using SSL.

Implementing the Key Exchange

As noted earlier, formatters take an IMessage and generate two output objects, an ITransportHeaders and a Stream. The stream contains a serialized version of the IMessage while the ITransportHeaders object contains out-of-band headers (name/value pairs) to be sent to the server by the transport sink. Initially, this collection is populated with formatter-supplied headers, such as a SOAPAction. The use of this collection, however, is in no way limited to the formatter. Any of the sinks between the formatter and the transport can add headers into this collection and they will all be dealt with similarly by the transport sink. This is a great place to put handshake information that the client and server will need for communication, such as handshake state, encryption keys, and a connection identifier.

You have to keep in mind when implementing the solution that both the client and the server must be thread-safe. When remote requests are made on an object on multiple threads, those requests are not serialized in any fashion before reaching your sink (unless a previous sink has decided to synchronize access). You need to be mindful of this fact when storing state information. In addition, one server can be used by many clients. Since the client and server will need to store a shared encryption key (and since it would not be secure for the server to distribute the same shared key to all of its clients), the server will need to maintain a list of all clients and their associated information. Both the client and the server will also need to store a unique identifier so that the server can look up the client's key.

Implementation on the client is now fairly clear cut (see Figure 5). ProcessEncryptedMessage is called by ProcessMessage to handle most of the work. When ProcessMessage is called, the client must check to see if it has already received a key from the server. If it has, it can encrypt the stream containing the serialized message and forward it to the server, decrypting the response it gets back. The client must also be mindful of the fact that the server may have deleted its connection information (or possibly the server may have been restarted, causing it to lose all of its connection information), and as such the client must be ready to try again should the transaction fail. If the client has not received a shared key from the server, or if the server sends back a message informing the client that it was an unknown entity, the client must initiate a handshake with the server. As I've outlined, the client will generate a unique identifier for itself as well as an RSA key pair, and it will send the identifier and the public key to the server. When it receives the encrypted shared key back from the server, it can use its private key to retrieve the shared key, store it for later use, and continue by encrypting the original message and sending it to the server.

Figure 5 Client Sink's ProcessEncryptedMessage

private bool ProcessEncryptedMessage( IMessage msg, ITransportHeaders requestHeaders, Stream requestStream, out ITransportHeaders responseHeaders, out Stream responseStream) { // Encrypt the message. Guid id; lock(_transactionLock) { id = EnsureIDAndProvider(msg, requestHeaders); requestStream = SetupEncryptedMessage(requestHeaders, requestStream); } // Send the encrypted request to the server _next.ProcessMessage( msg, requestHeaders, requestStream, out responseHeaders, out responseStream); // Decrypt the response. lock(_transactionLock) { responseStream = DecryptResponse(responseStream, responseHeaders); if (responseStream == null && id.Equals(_transactID)) ClearSharedKey(); } // Return whether we were successful return responseStream != null; }

The server requires a connectionless implementation. It must be ready to receive requests in various handshake states from any number of clients. As shown in Figure 6, when a request comes in, the server will look up the ID and handshake state in the transport headers. Based on these two parameters it can choose the proper course of action. If the client is requesting a shared key, the server will create one and send it back to the client encrypted with the client's public key (also making sure to store the key for later use). If the client is requesting the processing of an encrypted message, the server will attempt to locate the client's key, and if found, use it to decrypt the request, forwarding it along the sink chain, encrypting the received result, and sending it back to the client. If the key wasn't found, it must alert the client to this fact. An example of this initial conversation is diagrammed in Figure 7.

Figure 6 Server Sink Process Message

public ServerProcessing ProcessMessage( IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream) { // Get header information about transaction string strTransactID = (string)requestHeaders[CommonHeaders.ID]; Guid transactID = (strTransactID == null) ? Guid.Empty : new Guid(strTransactID); SecureTransaction transactType = (SecureTransaction)Convert.ToInt32( (string)requestHeaders[CommonHeaders.Transaction]); // For reference, find out who is connecting to us. IPAddress clientAddress = requestHeaders[CommonTransportKeys.IPAddress] as IPAddress; // Push this sink onto the sink stack sinkStack.Push(this, null); // Process the transaction based on its type ServerProcessing processingResult; switch(transactType) { // Client is requesting a shared key case SecureTransaction.SendingPublicKey: processingResult = MakeSharedKey( transactID, requestHeaders, out responseMsg, out responseHeaders, out responseStream); break; // Client is sending us an encrypted message case SecureTransaction.SendingEncryptedMessage: if (PreviousTransactionWithClient(transactID)) { processingResult = ProcessEncryptedMessage( transactID, sinkStack, requestMsg, requestHeaders, requestStream, out responseMsg, out responseHeaders, out responseStream); } else { processingResult = SendEmptyToClient( SecureTransaction.UnknownIdentifier, out responseMsg, out responseHeaders, out responseStream); } break; // Client is sending us a plaintext message case SecureTransaction.Uninitialized: if (!RequireSecurity(clientAddress)) { processingResult = _next.ProcessMessage( sinkStack, requestMsg, requestHeaders, requestStream, out responseMsg, out responseHeaders, out responseStream); } else throw new SecureRemotingException("Security required."); break; // Uh oh. default: throw new SecureRemotingException("Invalid request."); } // Take us off the stack and return the result. sinkStack.Pop(this); return processingResult; }

Figure 7 Client/Server Conversation

Figure 7** Client/Server Conversation **

Your well-behaved server should also be conscious of the possibility that not all clients will be sending encrypted messages. If it detects that the necessary transport headers do not exist (suggesting that the client application is not using the client sink), it can pass the data on to the next sink without any modifications. Even though this can be done, you don't necessarily want to do so. What if you wanted to enforce a requirement that connecting clients must be encrypting traffic, or what if you only wanted to enforce it for some clients? Both the HttpServerTransportSink and the TcpServerTransportSink help you out here by providing the IP address of the connected client. You can grab the IP address from the transport headers, as shown here:

IPAddress clientAddress = requestHeaders[CommonTransportKeys.IPAddress] as IPAddress;

Then you can use this address to decide whether you are going to accept unencrypted traffic:

case SecureTransaction.Uninitialized: if (!RequireSecurity(clientAddress)) { processingResult = _next.ProcessMessage( sinkStack, requestMsg, requestHeaders, requestStream, out responseMsg, out responseHeaders, out responseStream); } else throw new SecureRemotingException("Security required."); break;

Asynchronous Processing

In version 1.0 of the .NET Framework, there is no server support for asynchronous processing in custom channel sinks (even though there is the conspicuously named method AsyncProcessResponse on the IServerChannelSink interface). All requests to your server sink will be synchronous, even if the client makes an asynchronous request; therefore, you don't need to add any functionality to your server sink to support asynchronous requests.

Implementing async handling on the client is not quite as simple. Client-side, ProcessMessage is only used for synchronous requests. For one-way and asynchronous calls, the processing logic is divided between two of the interface methods on IClientChannelSink: AsyncProcessRequest and AsyncProcessResponse. AsyncProcessRequest provides the sink an opportunity to modify the stream and headers being sent to the server, while AsyncProcessResponse allows the modification of the stream and headers being sent from the server.

In my implementation (see Figure 8) AsyncProcessRequest can't always be entirely asynchronous. If no connection information has been established with the server, AsyncProcessRequest calls EnsureIDAndProvider to ensure that this information is retrieved. To do so it must make a synchronous round-trip to the server just as ProcessMessage does. With any luck, this is only done once and the rest of the requests made through AsyncProcessRequest will all be asynchronous. Unlike with synchronous processing, here you can't make an additional asynchronous request should the initial one fail. Since AsyncProcessResponse must return the output headers and stream, and as you can't remake an asynchronous request, the second request must be synchronous to try again. To accomplish this, AsyncProcessRequest pushes the arguments supplied to it onto the sink stack as the state that will be given to AsyncProcessResponse. If AsyncProcessResponse gets back an invalid response from the server, it can use this state data to make a synchronous attempt using ProcessMessage.

Figure 8 Client's AsyncProcessRequest

public void AsyncProcessRequest( IClientChannelSinkStack sinkStack, IMessage msg, ITransportHeaders headers, Stream stream) { AsyncProcessingState state = null; Stream encryptedStream = null; Guid id; lock(_transactionLock) { // Establish connection information with the server // (hopefully only done on first request) id = EnsureIDAndProvider(msg, headers); // Saves state information just in case something goes wrong state = new AsyncProcessingState(msg, headers, ref stream, id); // Encrypting the stream encryptedStream = SetupEncryptedMessage(headers, stream); } // Push ourselves onto the stack with the necessary state and // forward on to the next sink sinkStack.Push(this, state); _next.AsyncProcessRequest(sinkStack, msg, headers, encryptedStream); }

I have purposely limited my discussion of one-way methods because they are not supported by this sink pair. Due to their very nature, the client sink never receives a response for a one-way method and therefore the required round-trip to the server to establish a shared key is not possible without implementing a good deal of workaround logic.

Server Cleanup

[Editor's Update - 12/8/2004: The code in Figure 9 has been fixed to correctly start the timer.] If a client whose information was removed later attempts to send an encrypted message, the server will send a response indicating that it has no information on the client's identity. The client and server will then proceed with a new handshake, starting the whole process over again.

Figure 9 Server's Connection Sweeper

private void StartConnectionSweeper() { // Create and start a timer that will execute SweepConnections if (_sweepTimer == null) { _sweepTimer = new System.Timers.Timer(_sweepFrequency*1000); _sweepTimer.Elapsed += new ElapsedEventHandler(SweepConnections); _sweepTimer.Start(); } } private void SweepConnections(object sender, ElapsedEventArgs e) { lock (_connections.SyncRoot) { ArrayList toDelete = new ArrayList(_connections.Count); // Find all entries that need to be deleted foreach(DictionaryEntry entry in _connections) { ClientConnectionInfo cci = (ClientConnectionInfo)entry.Value; if (cci.LastUsed.AddSeconds(_connectionAgeLimit).CompareTo(DateTime.UtcNow) < 0) { toDelete.Add(entry.Key); ((IDisposable)cci).Dispose(); } } // Delete the out-of-date entries found above foreach(Object obj in toDelete) _connections.Remove(obj); toDelete = null; } }

This same process could be used to provide another level of security. In addition to the last access time, the server could record the number of accesses by the client or the amount of data that had been transferred. When these counts hit a preset limit, the server could perform the same cleanup operation and delete the client's connection information. The client could do the same, deleting the shared key and proceeding as if it had never received one. This would force the client and server to renegotiate a new shared key, similar to the process used by IPSec's automatic key renewal.

Creating the Sinks

Now that I've written the sinks, I need to help the framework instantiate them. The .NET Remoting framework does not create the sinks directly. Instead, it creates "sink providers" that are then responsible for creating the actual sinks. Providers and sinks almost always form a one-to-one relationship, meaning that a single provider is used to create a single sink; however, there is nothing preventing a provider from creating more than one sink.

The biggest benefit of having the job of sink instantiation relegated to the providers is that it adds another level of indirection over which the developer of the sink has a lot of control. If the provider implements the correct constructor, the framework will pass to the provider an IDictionary containing attributes and elements that have been specified for the provider in the remoting configuration file (this dictionary can also be provided programmatically if a remoting file is not used). The provider can then take this information and use it to create a sink which can have any constructor the developer desires. The only requirement is that the provider must inform the sink of the next sink in the chain in order for it to be able to forward processing requests.

Figure 10 Using Sink Providers to Instantiate Sinks

Figure 10** Using Sink Providers to Instantiate Sinks **

When the providers are created by the framework, they themselves are created in a chain. The IClientChannelSinkProvider and IServerChannelSinkProvider interfaces both require the implementation of a Next property which is used by the framework to specify the next provider. The framework instantiates the providers and calls the first provider's CreateSink method. The CreateSink method is responsible for creating the actual sink and for returning a reference to it (see Figure 10). As the sink must be informed of the next sink in the chain, the CreateSink method must forward the call to the next provider in the chain, which will then return a reference to its sink:

public IServerChannelSink CreateSink(IChannelReceiver channel) { IServerChannelSink nextSink = null; if (_next != null) { if ((nextSink = _next.CreateSink(channel)) == null) return null; } return new SecureServerChannelSink(nextSink, ... ); }

The sink returned by the first provider's CreateSink now begins the complete linked list of sinks.

Configuration

Configuration of an HTTP channel can be done through a configuration file as follows:

<channels> <channel ref="http" port="8124" /> </channels>

This directs the framework to use the HTTP channel and its default formatter, the SOAP formatter. To override the default, you can be explicit about which formatter you want to use:

<channels> <channel ref="http" port="8124"> <serverProviders> <formatter ref="binary" /> </serverProviders> </channel> </channels>

When the <serverProviders/> tag (or <clientProviders/> on the client) is used as an element of <channel>, it tells the framework that instead of using the default sink chain, the channel should be set up to use the providers specified in this list and in the order that they're listed. In this case, you're telling the framework to use the binary formatter. To add more providers, you only need to add additional elements to the server provider list:

<serverProviders> <provider type= "MsdnMag.Remoting.SecureServerChannelSinkProvider,SecureChannel" /> <formatter ref="binary" /> </serverProviders>

Notice that I've added the provider before the formatter as I need to be able to modify the stream before it hits the formatter. In the client configuration file, the provider must come after the formatter for the opposite reason.

As I've created the provider with the constructor that allows it to take an IDictionary of parameters, I can add attributes to the providers tag that will modify the behavior of the provider and sink, by dictating things such as the encryption algorithm to use and how frequently the connection sweeper should run.

When I use RemotingConfiguration.Configure on the client and on the server with the appropriate configuration files, the custom sinks will be part of the sink chains.

Conclusion

The channel developed in this article isn't a perfect solution. The sinks take out some pretty big locks in order to achieve thread safety. There is no authentication or message signing. There are no attempts made to limit man-in-the-middle or denial of service attacks. But I hope the point is still clear. This article provides a glimpse into the types of solutions that are possible with .NET Remoting. While many security channels could benefit from these kinds of techniques, the possibilities for productivity and creativity are limitless: transport sinks to support named pipes or MSMQ, IMessageSinks to cache return values, compression channels, logging channels, notification channels—the list goes on. I imagine that in the near future the remoting community will start to see a wide variety of Microsoft and third-party remoting extensions to fill some of these voids, but that certainly doesn't prevent you from starting work today on your dream sink.

For related articles see:
.NET Remoting Security Solution, Part 2: Microsoft.Samples.Runtime.Remoting.Security Assembly
Advanced Remoting
Advanced .NET Remoting (C# Edition), by Ingo Rammer (APress, 2002)
Microsoft .NET Remoting, by Scott McLean, et al. (Microsoft Press, 2002)

For background information see:
.NET Remoting: Design and Develop Seamless Distributed Applications for the Common Language Runtime
An Introduction to Microsoft .NET Remoting Framework
Microsoft .NET Remoting: A Technical Overview

Stephen Toubis a developer and a consultant with Microsoft in New York. He is a graduate of and a former Teaching Fellow at Harvard University. Reach Steve at stoub@microsoft.com.