diff --git a/README.md b/README.md index 9881f3ce..c17cc583 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 be40ef5b..e3b05e40 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 ed23eacb..620bd885 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") {