Tuesday, December 27, 2011

Establishing a SSL connection to RabbitMQ using the .NET client

First, I'm making the assumption that you've read, re-read, and followed the SSL tutorial on rabbitmq's website.  If you haven't done everything that it's instructed you to do (including adding your certificates to the Windows Certificate Store using certmgr), none of this code is going to work for you.  If you have, this code should "Just Work™".

Here's the complete code file that works for me.  You will (obviously) need to change the names of the servers, and the thumbprint of your certificate.

Note that this code only uses your client and server's certificates to establish a secure connection.  You are still logging in as guest.  I will show you how to use your client certificate to authenticate yourself below.
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;

using RabbitMQ.Client;
using RabbitMQ.Util;


namespace RabbitSslTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Program p = new Program();
            p.Start();

            Console.WriteLine();
            Console.WriteLine("Press <ENTER> to exit.");
            Console.ReadLine();
        }


        public void Start()
        {
            try
            {
                // we use the rabbit connection factory, just like normal
                ConnectionFactory cf = new ConnectionFactory();

                // set the hostname and the port
                cf.HostName = "rabbitmq-server";
                cf.Port = AmqpTcpEndpoint.DefaultAmqpSslPort;

                // I've imported my certificate into my certificate store
                // (the Personal/Certificates folder in the certmgr mmc snap-in)
                // Let's open that store right now.
                X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
                store.Open(OpenFlags.ReadOnly);

                // and find my certificate by its thumbprint.
                X509Certificate cert = store.Certificates
                    .Find(
                        X509FindType.FindByThumbprint,
                        "6d 36 c0 f9 c2 f8 72 05 d1 01 6c 54 e1 91 bb 2d 66 7b ac 94",
                        true
                    )
                    .OfType<X509Certificate>()
                    .First();

                // now, let's set the connection factory's ssl-specific settings
                // NOTE: it's absolutely required that what you set as Ssl.ServerName be
                //       what's on your rabbitmq server's certificate (its CN - common name)
                cf.Ssl.Certs = new X509CertificateCollection(new X509Certificate[] { cert });
                cf.Ssl.ServerName = "rabbitmq-server";
                cf.Ssl.Enabled = true;

                // we're ready to create an SSL connection to the rabbitmq server
                using (IConnection conn = cf.CreateConnection())
                {
                    using (IModel channel = conn.CreateModel())
                    {
                        channel.QueueDeclare("rabbitmq-dotnet-test", false, false, false, null);
                       
                        // create some basic properties for the message we're going to send
                        IBasicProperties props = channel.CreateBasicProperties();
                        props.ContentEncoding = "utf-8";
                        props.MessageId = Guid.NewGuid().ToString();

                        // publish a hello world message
                        channel.BasicPublish("", "rabbitmq-dotnet-test", props, Encoding.UTF8.GetBytes("Hello, World"));

                        // and go get it
                        BasicGetResult result = channel.BasicGet("rabbitmq-dotnet-test", true);

                        if (null == result)
                        {
                            Console.WriteLine("No message received.");
                        }
                        else
                        {
                            Console.WriteLine("-----==<([ Received ])>==-----");
                            DebugUtil.DumpProperties(result, Console.Out, 0);
                        }

                        channel.QueueDelete("rabbitmq-dotnet-test");
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}
 And the output:

Using your certificate to authenticate
If you want to authenticate yourself with your certificate as well, you need to change the code and your rabbitmq server's configuration.

First, update your rabbitmq server's configuration.  If you don't know where to look for that, read this page.  Here's what mine looks like:
  [
  {rabbit, [
     {auth_mechanisms,['EXTERNAL']},
     {ssl_listeners, [5671]},
     {ssl_options, [{cacertfile,"C:/Path/To/Your/cacert.pem"},
                    {certfile,"C:/Path/To/Your/cert.pem"},
                    {keyfile,"C:/Path/To/Your/key.pem"},
                    {verify,verify_peer},
                    {fail_if_no_peer_cert,true}]}
   ]}
].
The thing to note there is the addition of {auth_mechanisms,['EXTERNAL']}.  Don't forget to stop your service, re-install it, and start it after making these changes.
rabbitmq-service.bat stop
rabbitmq-service.bat install
rabbitmq-service.bat start
Next, let's update our client code just a tad by setting the connection factory's auth mechanisms.

// we use the rabbit connection factory, just like normal ConnectionFactory cf = new ConnectionFactory();

// set the hostname and the port
cf.HostName = "rabbitmq-server";
cf.Port = AmqpTcpEndpoint.DefaultAmqpSslPort;
cf.AuthMechanisms = new AuthMechanismFactory[] { new ExternalMechanismFactory() };

// I've imported my certificate into my certificate store
// (the Personal/Certificates folder in the certmgr mmc snap-in)
// Let's open that store right now.
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
The other change I made was to insert a Console.ReadLine() just before all of my using () statements cause everything to close down.  By doing this, I can view the RabbitMQ management web app to ensure that I'm authenticating using my certificate.


Wednesday, December 21, 2011

Windows does not always honor DNS order

I was having a problem where some internal server names would become unresolvable after being resolvable.  After becoming tired of flushing the dns resolver cache, I finally opened wireshark to see what was going on.

To my surprise, windows was using my secondary DNS (8.8.8.8) instead of my primary, internal DNS!  After some searching, I finally found this knowledge base article.
This behavior occurs because the Windows XP DNS Client service (Dnscache) follows a certain algorithm when it decides the order in which it uses the DNS servers configured in the TCP/IP properties. If the DNS server list is reprioritized, the Windows XP DNS Client service resets the server priority at periodic intervals. By default, the server priorities are reset every 15 minutes.
 Luckily, the workaround in that same article fixes the issues I was having.
To work around this behavior, modify the registry so that the DNS server that is configured first is tried first on each query. Follow these steps, and then quit Registry Editor:
  1. Click Start, click Run, type regedit, and then click OK.
  2. Locate and then click the following key in the registry:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters
  3. On the Edit menu, point to New, and then click REG_DWORD.
  4. Type ServerPriorityTimeLimit, and then press ENTER.
  5. On the Edit menu, click Modify.
  6. Type 0, and then click OK.
When you set ServerPriorityTimeLimit to 0 (zero), the server priorities are reset before the DNS Client service decides which DNS server to use. You must restart Windows XP for these changes to take affect. Any other value used in this field will cause the default behavior.
Even though this article is for Windows XP, this worked for me on Windows 7.