friends-of-reactphp / mysql

Async MySQL database client for ReactPHP.
MIT License
331 stars 66 forks source link

[TLS] Cannot connect to remote db #199

Closed jamesrweb closed 2 months ago

jamesrweb commented 2 months ago

As the title states, I have a Linode MySQL cluster and need to connect to it via TLS, it's a mandatory requirement. I have passed the certificate path and tried verifying peers, using local_cert and more but it still won't connect. I use clue/framework-x and inside my DI container, I instantiate the connection like so:

use React\MySQL\Factory as ConnectionFactory;
use React\Socket\Connector as SocketConnector;

...

ConnectionInterface::class => function (EnvironmentInterface $environment) {
      $user = $environment->getDatabaseUser();
      $password = $environment->getDatabasePassword();
      $host = $environment->getDatabaseHost();
      $name = $environment->getDatabaseName();
      $certificate = $environment->getMySqlTlsCertificatePath();
      $connector = new SocketConnector([
          'tls' => [
              'cafile' => dirname(__DIR__) . "/" . $certificate
          ]
      ]);
      $credentials = "{$user}:{$password}@{$host}/{$name}";
      $factory = new ConnectionFactory(null, $connector);

      return $factory->createLazyConnection($credentials);
}
...

The information is all correct and I can see the $credentials contains all necessary values. This is my current version but as I said, I have tried with verify_peer, verify_peer_name, local_cert, etc but still it cannot connect.

Using my local MySQL GUI, the connection with the exact same connection details and certificate is fine and so it must be something to do with the connector as far as I can understand it and the error thrown is:

[Mon May 13 17:14:56 2024] PHP Fatal error:  Uncaught TypeError: method_exists(): Argument #1 ($object_or_class) must be of type object|string, null given in /Users/<user>/<path/to/project>/vendor/react/dns/src/Query/TimeoutExecutor.php:57

Note: I edited the path to the error for this issue, in case it was confusing for anyone.

Anyway, if someone can point me to the right direction I would appreciate it because I have my .crt file and just need to connect to the database but the documentation for this and TLS isn't very clear and I have tried everything I can think of now, so any help would be great 🤷🏻.

jamesrweb commented 2 months ago

@SimonFrings @clue any ideas?

I have done more debugging and logging and found this alongside the error in the initial issue post:

Connection to mysql://<username>:<password>@<host>/<database> failed: Connection to tcp://<username>:3306 failed during DNS lookup. Last error for IPv6: DNS query for <username> (AAAA) returned ... ":"::1","http_method":"POST","server":"localhost","referrer":null,"uid":"db6b308"}

Original / current error, as a reminder:

[Mon May 13 17:14:56 2024] PHP Fatal error:  Uncaught TypeError: method_exists(): Argument #1 ($object_or_class) must be of type object|string, null given in /Users/<user>/<path/to/project>/vendor/react/dns/src/Query/TimeoutExecutor.php:57

Running nslookup <host> returns:

Server:  UnKnown
Address:  <address>

Nicht autorisierende Antwort:
Name:    <host>
Address:  <another_address>
Aliases:  <host>

In short: it exists, connects, responds as expected with nslookup. Not really sure why it tries to connect to tcp://<username>:3306 but... the environment and connection string information is definitely correct and so this responding as it does is surprising, especially when the nslookupshows the correct information as expected.

Finally, as mentioned above, my local SQL GUI connects with the exact same connection information and local certificate file. So it must be something with the connector or its configuration but the docs are unclear and I cannot see the issue, even after manually debugging and then GPTing for a while 😉.

jamesrweb commented 2 months ago

Oh and the ConnectionInterface container implementation currently looks like this:

...
ConnectionInterface::class => function (EnvironmentInterface $environment) {
        $user = $environment->getDatabaseUser();
        $password = $environment->getDatabasePassword();
        $host = $environment->getDatabaseHost();
        $name = $environment->getDatabaseName();
        $certificate = $environment->getMySqlTlsCertificatePath();
        $connector = new SocketConnector([
            'tls' => [
                'cafile' => dirname(__DIR__).'/'.$certificate,
                'verify_peer' => true,
                'verify_peer_name' => true,
                'allow_self_signed' => false,
            ],
        ]);
        $credentials = "{$user}:{$password}@{$host}/{$name}";

        $factory = new ConnectionFactory(null, $connector);

        return $factory->createLazyConnection($credentials);
    }
...

And yes, I tried with verify_peer, verify_peer_name and allow_self_signed in all possible combinations also. Nada.

SimonFrings commented 2 months ago

Hey @jamesrweb, thanks for bringing this up :+1:

This is not a configuration problem on your end, it's because we don't have built in support for secure TLS connections. There's also an open feature request for this in #49 which also depends on the open ticket in Socket https://github.com/reactphp/socket/issues/89.

I can't really say when this feature will come, as our highest priority is currently to prepare ReactPHP v3 for the upcoming ReactPHP birthday. If you need this more urgently you can also reach out to us.

Let's use #49 for further discussion about this topic, so I'll close this ticket here.

jamesrweb commented 2 months ago

Okay but @SimonFrings, the docs clearly say:

If you need custom connector settings (DNS resolution, TLS parameters, timeouts, proxy servers etc.), you can explicitly pass a custom instance of the ConnectorInterface

And so this must be possible or it's a lie, which I don't believe it to be, but checking the docs here and for the connector, etc, it is unclear how to achieve this.

If it is in the docs, it must be possible and so again, I ask, are the docs correct or not? If not, they should really be updated and corrected because I wasted quite a bit of time on this... I ended up migrating to pure PDO and thus, lost all the benefits of the async implementation. Also, the docs for framework-x itself recommend this project (in the book example) and so I assume other users have hit the same issue, even if they haven't raised it, for this reason and perhaps that has also reduced adoption accordingly or caused others to go this route like I was forced to instead.

clue commented 2 months ago

If you need custom connector settings (DNS resolution, TLS parameters, timeouts, proxy servers etc.), you can explicitly pass a custom instance of the ConnectorInterface

And so this must be possible or it's a lie, which I don't believe it to be, but checking the docs here and for the connector, etc, it is unclear how to achieve this.

@jamesrweb Very much understand your frustration and would love to see built-in TLS support as well, but as @SimonFrings suggested above, this projects currently only offers limited TLS support and the full implementation depends on #49 at the moment. While the documentation may be technically right (the best kind of right), I agree it can be seen as misleading. For what it's worth, we use the same documentation snippet across our components and TLS parameters may indeed be set here as well, but they simply won't have an effect at the moment. PRs to improve this welcome, I guess.

If you need TLS support for a commercial projects, please reach out and I'm happy to work something out to make sure we can ship this in this project as soon as possible. In the meantime, you may use either unencrypted connections (depending on your provider and connection setup), potentially use an SSH tunnel instead (e.g. https://github.com/clue/reactphp-ssh-proxy#database-tunnel), or you may use a TLS proxy (e.g. https://proxysql.com/documentation/SSL-Support/) to work around this limitation.

As @SimonFrings suggested, let's continue this discussion in #49. Thank you for your support!