cats on container

MongoDB SRV connection string and default tls parameter

Context

If we use MongoDB Atlas, the connection string provided to be used to connect the cluster is using SRV connection format. So instead of using mongodb:// in the connection string, it will be mongodb+srv://.

The advantage of using this is that if I have a MongoDB replica set with 3 nodes, using the standard connection format (the mongodb:// one) I will set the connection string like this:

  
    mongodb://username:password@db-1.example.com:27017,db-2.example.com:27017,db-3.example.com:27017/db?authSource=admin&replicaSet=myRepl
  

I think I can leave out the port to make it shorter, I am not sure. But yeah it's a bit too long. It's possible to just mention 1 node instead of all three but it's not recommended. If I use the db-1.example.com one and then that node is down then the db client will not be able to find the rest of the replica set and it will not be able to connect to it.

Using SRV connection format, I can use something like this:

  mongodb+srv://username:password@db-rs.example.com/db

So how does it work really? Seems like there is less information in that connection string and somehow it works. The client can find out all of the nodes inside the replica set and the parameters used for the connection string.

SRV DNS record

Turns out there is a type of DNS record called SRV. This page in Cloudflare learning site explains it well. In summary this type of record is a DNS service record, hence the name SRV I assume. The format is the following:

  _service._proto.name. TTL class type of record priority weight port target.

So let's say that I have a MongoDB replica set. It has 3 nodes. The nodes are accessible using these addresses: rs-node-1.rizaldi.net, rs-node-2.rizaldi.net, and rs-node-3.rizaldi.net. I want this replica set to be accessible through db.rizaldi.net domain. For this I will create 3 DNS records with type SRV for the host _mongodb._tcp.db.rizaldi.net. The way to create these records will probably differ for each DNS manager that you use. In Porkbun it looks like the following.

Form to add SRV record in Porkbun

Using the above form for my use case, I will need to create 3 DNS records. For the first one I will fill in the form with:

The other two records will be the same except for the rs-node-1 part. It will be rs-node-2 and rs-node-3 respectively.

Why do I use 0 for priority and weight? To be honest I just copy the setup from MongoDB Atlas. When I queried the DNS record for my MongoDB Atlas cluster's DNS I found that it uses 0 as priority and weight. I haven't experimented with changing the values yet.

Additional TXT record

Another thing to note is that MongoDB client will also look for a TXT record for the domain used in the connection string. The value of this TXT record will be the parameter of the connection string. For my sample use case I need to add a TXT record for db.rizaldi.net with value authSource=admin&replicaSet=rs0. This is to tell the MongoDB client to use admin database as the authentication database and the name of replica set is rs0.

The implied tls parameter

When I did the experiment of setting up the DNS for MongoDB replica set, I still couldn't connect to the replica set even though I was sure that I already configured the DNS records in the correct way.

The funny thing is I could connect to the replica set using standard MongoDB connection string, by mentioning one or all the nodes of the replica set in the connection string.

To investigate it I run a python script that I found in MongoDB developer site.


import srvlookup #pip install srvlookup
import sys
import dns.resolver #pip install dnspython

host = None

if len(sys.argv) > 1 :
   host = sys.argv[1]

if host :
   services = srvlookup.lookup("mongodb", domain=host)
   for i in services:
      print("%s:%i" % (i.hostname, i.port))
   for txtrecord in dns.resolver.query(host, 'TXT'):
      print("%s: %s" % ( host, txtrecord))

else:
  print("No host specified")


I ran the script first using my MongoDB Atlas cluster domain as host. Then using my newly setup MongoDB replica set as host. Then I compared the result. From my understanding my setup should work since the output of the script is as expected.

Then I got an idea to run a simple javascript program for connecting the replica set. The script is from the MongoDB official node client's README file with some modifications.


const { MongoClient } = require('mongodb');

// Connection URL
const url = 'mongodb+srv://user:pass@db.rizaldi.net';
const client = new MongoClient(url);

client.on('commandStarted', (event) => console.debug(event));
client.on('commandSucceeded', (event) => console.debug(event));
client.on('commandFailed', (event) => console.debug(event));

async function main() {
  // Use connect method to connect to the server
  await client.connect();
  console.log('Connected successfully to server');
  const db = client.db(dbName);

  return 'done.';
}

main()
  .then(console.log)
  .catch(e => {
    console.error(e)
  })
  .finally(() => client.close());

After running it I found this at the end of the error message:


[cause]: MongoNetworkError: read ECONNRESET
  at TLSSocket.<anonymous> (/Users/rizaldim/tmp/mongodb-srv/nodejs/node_modules/mongodb/lib/cmap/connect.js:285:44)
  at Object.onceWrapper (node:events:633:26)
  at TLSSocket.emit (node:events:518:28)
  at emitErrorNT (node:internal/streams/destroy:170:8)
  at emitErrorCloseNT (node:internal/streams/destroy:129:3)
  at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
[Symbol(errorLabels)]: Set(1) { 'ResetPool' },
[cause]: Error: read ECONNRESET
    at TLSWrap.onStreamRead (node:internal/stream_base_commons:216:20) {
  errno: -54,
  code: 'ECONNRESET',
  syscall: 'read'
}
}

TLS? I must have missed something. I was sure that there was no mention of tls or anything related to that after reading about SRV record or SRV connection string. After re-reading the MongoDB docs I found this:

Use of the +srv connection string modifier automatically sets the tls (or the equivalent ssl) option to true for the connection. You can override this behavior by explicitly setting the tls (or the equivalent ssl) option to false with tls=false (or ssl=false) in the query string.

So it turns out using SRV connection string implies that tls=true in the connection string. I haven't configured the TLS for my new replica set yet. That's why I can't connect to it. While the TLS is still not set up, I need to add tls=false to my connection string.

A couple of hours getting stuck trying to find out why my setup didn't work. The key takeaway is always read the docs. Read it thoroughly.