Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SentinelsClient: Use the given protocol to connect to sentinels #48

Merged
merged 1 commit into from
Aug 15, 2024

Conversation

jlledom
Copy link
Contributor

@jlledom jlledom commented Jan 29, 2024

I was trying to connect to some sentinels that require password authentication. For that, I created a new protocol as in the example: https://github.com/socketry/async-redis/blob/main/examples/auth/protocol.rb

class AuthenticatedRESP2
  def initialize(password)
    @password = password
  end

  def client(stream)
    client = Async::Redis::Protocol::RESP2.client(stream)

    client.write_request(["AUTH", *@password])
    client.read_response # Ignore response.

    client
  end
end

When SentinelsClient is used, it ignores the provided protocol when it tries to resolve address for master and slaves. As a result, password protected sentinels are not supported by async-redis.

This PR includes a small change to fix that.

Types of Changes

  • Bug fix.

Contribution

@@ -60,7 +60,7 @@ def resolve_master

def resolve_slave
@sentinel_endpoints.each do |sentinel_endpoint|
client = Client.new(sentinel_endpoint)
client = Client.new(sentinel_endpoint, protocol: @protocol)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be testable in some way? Can you think of a test that could check that this was the passed protocol?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this in Fedora following the instructions from #29.

Launch Redis master and replica as containers:

Master:

podman run --rm --net=host redis --port 6379 --requirepass p4ssW0rd --masterauth p4ssW0rd

Slave:

podman run --rm --net=host redis --port 6380 --requirepass p4ssW0rd --masterauth p4ssW0rd --slaveof localhost 6379

Create three sentinel.conf files with the next content:

port 26379
sentinel monitor redis-master 127.0.0.1 6379 2
sentinel down-after-milliseconds redis-master 5000
sentinel failover-timeout redis-master 60000
requirepass "p4ssW0rd"
sentinel auth-pass redis-master p4ssW0rd

But with a different port for each file: 26379, 26380, 26381.

Then launch the sentinels:

podman run --rm --net=host -v ./sentinel1:/data:z redis /data/sentinel.conf --port 26379 --sentinel
podman run --rm --net=host -v ./sentinel2:/data:z redis /data/sentinel.conf --port 26380 --sentinel
podman run --rm --net=host -v ./sentinel3:/data:z redis /data/sentinel.conf --port 26381 --sentinel

I created this small script to test the PR. Comment/uncomment the proper gemfile line to see the difference between master and this PR

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem 'async-redis'
  #gem 'async-redis', git: 'https://github.com/jlledom/async-redis', branch: 'sentinel-client-protocol'
end

class AuthenticatedRESP2
  def initialize(password)
    @password = password
  end

  def client(stream)
    client = Async::Redis::Protocol::RESP2.client(stream)

    client.write_request(["AUTH", *@password])
    client.read_response # Ignore response.

    client
  end
end

def main
  Async do
    sentinels = [{ host: "localhost", port: 26379 },
                 { host: "localhost", port: 26380 },
                 { host: "localhost", port: 26381 }]

    protocol = AuthenticatedRESP2.new('p4ssW0rd')

    client = Async::Redis::SentinelsClient.new('redis-master', sentinels, :master, protocol)

    while true
      begin
        sleep 1
        result = client.ping 'test'
        puts result
      rescue StandardError => e
        puts e.message
        retry
      end
    end
  end
end

main

@ioquatix
Copy link
Member

I am planning to introduce Async::Redis::Endpoint: #50.

It may provide a better foundation for this functionality.

We may wish to introduce Async::Redis::SentinelEndpoint which handles the indirection about how to connect.

@ioquatix ioquatix force-pushed the sentinel-client-protocol branch from c53d2ae to 6ee6d48 Compare August 15, 2024 04:41
@ioquatix ioquatix merged commit 32aaf45 into socketry:main Aug 15, 2024
6 of 10 checks passed
@ioquatix
Copy link
Member

Thanks for your contribution.

I'm sorry I did not attend to this PR sooner. However, now it's merged and I'll release it soon.

@jlledom
Copy link
Contributor Author

jlledom commented Aug 15, 2024

Thanks @ioquatix.

Note how this PR assumes the password for the sentinels and master is the same, it's reusing the same protocol for both. In order to support different credentials for sentinels and master, I ended up using @protocol only for master and extracting the sentinels credentials from the config (code). It's probably useful doing the same thing here.

@ioquatix
Copy link
Member

@jlledom Let me make a first pass over it, and then if you can give feedback on the design, that would be great.

@ioquatix
Copy link
Member

See #51 for the updated sentinels code.

@ioquatix
Copy link
Member

@ioquatix
Copy link
Member

A few thoughts.

Now it's possible to supply endpoints for each sentinel which allows them to have different credentials.

However, I'm not sure how TLS would or should work for the actual instances. GET-MASTER-ADDR-BY-NAME only returns a host and port, not a URL e.g. rediss:// which would allow us to deduce the connection parameters.

I suppose what we could do is make the address construction a method, and allow sub-classes to override it, because I'm not sure if there is any general approach here.

@jlledom
Copy link
Contributor Author

jlledom commented Aug 17, 2024

Now it's possible to supply endpoints for each sentinel which allows them to have different credentials.

I think that's not allowed. The docs say:

Note that Sentinel's authentication configuration should be applied to each of
the instances in your deployment, and all instances should use the same configuration.

I understood this as: it's mandatory that all instances use the same credentials. Also the example sentinels.conf file in the repo is more clear:

# You can configure Sentinel itself to require a password, however when doing
# so Sentinel will try to authenticate with the same password to all the
# other Sentinels. So you need to configure all your Sentinels in a given
# group with the same "requirepass" password. Check the following documentation
# for more info: https://redis.io/topics/sentinel

However, I'm not sure how TLS would or should work for the actual instances. GET-MASTER-ADDR-BY-NAME only returns a host and port, not a URL e.g. rediss:// which would allow us to deduce the connection parameters.

If I understood your code correctly, Redis::Endpoint doesn't currently accept SSL params, and the only param it sets is verify_mode. IMO, Redis::Endpoint should accept two more options: :ssl and :ssl_params. :ssl should determine whether a secure connection is used or not, rather than :scheme, and if :ssl is true, then :ssl_params should be used to build the ssl context. My implementation might be useful.

I don't think there's any other way to connect to TLS protected instances than sending :ssl and :ssl_params to the SentinelClient initializer and pass them along to the endpoint initializer.

@ioquatix
Copy link
Member

ioquatix commented Aug 17, 2024

Understood, so in any case, the sentinels should all have the same credentials.

Regarding Async::Redis::Endpoint, it does accept ssl_context which is the most general ssl configuration mechanism (ssl_params is more specific, but we could also add support for it).

I added the documentation here: 6a75f89

In this way, the sentinels could be using TLS but it may also need to be identical, or perhaps it's simply not supported?

@jlledom
Copy link
Contributor Author

jlledom commented Aug 18, 2024

Regarding Async::Redis::Endpoint, it does accept ssl_context which is the most general ssl configuration mechanism

I added the documentation here: 6a75f89

I understand.

In this way, the sentinels could be using TLS but it may also need to be identical, or perhaps it's simply not supported?

Sentinels can be TLS protected. According to the docs, a sentinel will listen in the TLS port only when tls-replication is enabled. Which means TLS must be enabled for all sentinels or none. However, I don't know whether all sentinels must share the same SSL context. I'd say it's not really needed, any sentinel can have its own TLS cert and key as long as all of them accept connections from each other and from a client.

About how to connect to master from SentinelClient, the initializer can accept a new option :client_ssl_context to be used when acting as a client, i.e. when connecting to instances.

@ioquatix
Copy link
Member

I think we should provide some more generic mechanism, like client_endpoint_options or something. It's used to construct the client endpoint from the host/port etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants