diff --git a/README.md b/README.md index 9881f3ce3..c17cc583e 100644 --- a/README.md +++ b/README.md @@ -929,6 +929,7 @@ The arguments passed to the constructor are different from the ones you use to c - `role` (optional) with a value of `slave` will return a random slave from the Sentinel group. - `preferredSlaves` (optional) can be used to prefer a particular slave or set of slaves based on priority. It accepts a function or array. - `enableTLSForSentinelMode` (optional) set to true if connecting to sentinel instances that are encrypted +- `enableDynamicSNIForSentinelMode` (optional) set to true if Redis instances require SNI support for TLS connections ioredis **guarantees** that the node you connected to is always a master even after a failover. When a failover happens, instead of trying to reconnect to the failed node (which will be demoted to slave when it's available again), ioredis will ask sentinels for the new master node and connect to it. All commands sent during the failover are queued and will be executed when the new connection is established so that none of the commands will be lost. diff --git a/lib/connectors/SentinelConnector/index.ts b/lib/connectors/SentinelConnector/index.ts index be40ef5b2..e3b05e402 100644 --- a/lib/connectors/SentinelConnector/index.ts +++ b/lib/connectors/SentinelConnector/index.ts @@ -51,6 +51,7 @@ export interface SentinelConnectionOptions { disconnectTimeout?: number; sentinelCommandTimeout?: number; enableTLSForSentinelMode?: boolean; + enableDynamicSNIForSentinelMode?: boolean; sentinelTLS?: ConnectionOptions; natMap?: NatMap; updateSentinels?: boolean; @@ -167,9 +168,16 @@ export default class SentinelConnector extends AbstractConnector { ); if (this.options.enableTLSForSentinelMode && this.options.tls) { - Object.assign(resolved, this.options.tls); - this.stream = createTLSConnection(resolved); - this.stream.once("secureConnect", this.initFailoverDetector.bind(this)); + const resolvedTls = resolved as ConnectionOptions; + Object.assign(resolvedTls, this.options.tls); + if (this.options.enableDynamicSNIForSentinelMode) { + resolvedTls.servername = resolved.host; + } + this.stream = createTLSConnection(resolvedTls); + this.stream.once( + "secureConnect", + this.initFailoverDetector.bind(this) + ); } else { this.stream = createConnection(resolved); this.stream.once("connect", this.initFailoverDetector.bind(this)); diff --git a/test/functional/tls.ts b/test/functional/tls.ts index ed23eacbd..620bd8853 100644 --- a/test/functional/tls.ts +++ b/test/functional/tls.ts @@ -92,6 +92,54 @@ describe("tls option", () => { }); }); + it("supports enableDynamicSNIForSentinelMode", (done) => { + new MockServer(27379, (argv) => { + if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { + return ["localhost", "17380"]; + } + }); + + new MockServer(17380); + + // @ts-expect-error + const stub = sinon.stub(tls, "connect").callsFake((op) => { + // @ts-expect-error + if (op.port === 17380) { + // @ts-expect-error + expect(op.ca).to.eql("1234"); + // @ts-expect-error + expect(op.servername).to.eql("localhost"); + // @ts-expect-error + expect(op.rejectUnauthorized).to.eql(false); + // @ts-expect-error + expect(op.port).to.eql(17380); + } + const stream = net.createConnection(op); + stream.on("connect", (data) => { + stream.emit("secureConnect", data); + }); + return stream; + }); + + const redis = new Redis({ + sentinels: [{ port: 27379 }], + name: "my", + enableDynamicSNIForSentinelMode: true, + enableTLSForSentinelMode: true, + tls: { ca: "1234", rejectUnauthorized: false }, + sentinelTLS: { + ca: "123", + servername: "localhost", + rejectUnauthorized: false, + }, + }); + redis.once("ready", () => { + redis.disconnect(); + stub.restore(); + redis.on("end", () => done()); + }); + }); + it("supports sentinelTLS", (done) => { new MockServer(27379, (argv) => { if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") {