返回

解决 Cloudflare 525 错误:Apache 反代 WebSocket (wss) 到 Ratchet

php

解决 Cloudflare 525 错误:Apache 反向代理 WebSocket (wss) 到 Ratchet

搞个聊天应用,用了 Ratchet 做 WebSocket 服务器。结果遇到 Cloudflare 报 525 错误,说 SSL 握手失败。头疼!这篇博客就来捋一捋怎么解决这个问题。

一、 问题

简单来说,就是客户端 (JavaScript) 用 wss:// 连接,通过 Cloudflare 和 Apache 反向代理,最终要连到 Ratchet WebSocket 服务器。但是 Cloudflare 报 525 错误,连不上。

客户端 JavaScript 代码大概长这样:

ws = new WebSocket("wss://meetwithchat.com/ws");

Apache 的配置,把 /ws 路径的请求反向代理到本地的 127.0.0.1:2053

<VirtualHost *:443>
    ServerName meetwithchat.com
    ServerAlias www.meetwithchat.com
    DocumentRoot /var/www/meetwithchat

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/meetwithchat.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/meetwithchat.com/privkey.pem

    # 反向代理 WebSocket
    ProxyPreserveHost On
    ProxyPass /ws ws://127.0.0.1:2053/
    ProxyPassReverse /ws ws://127.0.0.1:2053/

    ErrorLog ${APACHE_LOG_DIR}/meetwithchat_error.log
    CustomLog ${APACHE_LOG_DIR}/meetwithchat_access.log combined
</VirtualHost>

Ratchet 服务器跑在 127.0.0.1:2053没有 用 TLS (就是普通的 ws://):

<?php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\Server\IoServer;
use React\Socket\Server as Reactor;

require __DIR__ . '/vendor/autoload.php';

class ChatServer implements MessageComponentInterface {
    // ... (实现细节,比如 onOpen, onMessage 等)
}

// Ratchet 服务器设置 (本地用普通的 ws)
$loop = React\EventLoop\Factory::create();
$socket = new Reactor('127.0.0.1:2053', $loop);
$webSocket = new WsServer(new ChatServer());
$httpServer = new HttpServer($webSocket);
$server = new IoServer($httpServer, $socket, $loop);

error_log("WebSocket server is running on port 2053 (plain ws)...");
$server->run();
?>

二、问题原因分析

Cloudflare 525 错误通常表示 Cloudflare 无法与源服务器建立 SSL 连接。 结合我们的架构, 几个可能的原因:

  1. Cloudflare 和 Apache 之间的 SSL/TLS 配置问题: Cloudflare 尝试用 HTTPS 连接 Apache (因为客户端用的是 wss://),但 Apache 可能没有正确处理这个 WebSocket 的 SSL 连接。
  2. Apache 反向代理配置问题: Apache 的反向代理配置可能不完整,或者有错误,导致它无法正确地将 WebSocket 请求转发到 Ratchet 服务器。
  3. Ratchet 服务器没有启用 TLS: 虽然 Apache 到 Ratchet 之间可以用普通的 ws://,但是 Cloudflare 到 Apache 之间必须是 wss://。问题可能出在第一段。
  4. 防火墙或安全组规则: 防火墙阻止了 Cloudflare 到服务器的连接。

三、 解决方案

针对上面分析的原因,咱们一个个来解决。

1. 确保 Cloudflare 到 Apache 的连接是 HTTPS

  • 检查 Cloudflare 的 SSL/TLS 设置: 在 Cloudflare 的 Dashboard 里,找到 "SSL/TLS" 选项卡。
    • "Encryption mode" 应该设置为 "Full (strict)" 或 "Full"。如果是 "Flexible",改成 "Full (strict)"。("Flexible" 不安全,它允许 Cloudflare 和你的服务器之间使用不加密的 HTTP)。
    • "Full (strict)" 要求源服务器有受信任的 CA 签发的证书,或者 Cloudflare Origin CA 证书。"Full" 则只要求源服务器有证书(可以是自签名的)。一般推荐用"Full (strict)" 。
    • Edge Certificates 标签页下确认证书正常.
  • 在Origin Server 标签页中安装Cloudflare Origin CA 证书 (可选, 建议) Cloudflare 提供免费的 Origin CA 证书, 专门用来加密 Cloudflare 和源服务器之间的连接. 这个证书只会被 Cloudflare 信任.
    • 下载 Cloudflare Origin CA 证书(RSA 或 ECC 版本)。
    • 将证书和私钥安装到你的 Apache 服务器。
    • 在 Apache 配置文件中,更新 SSLCertificateFileSSLCertificateKeyFile 指向新的证书和私钥文件。

2. 完善 Apache 反向代理配置

Apache 的配置看起来基本没问题,但还可以做一些优化:

<VirtualHost *:443>
    ServerName meetwithchat.com
    ServerAlias www.meetwithchat.com
    DocumentRoot /var/www/meetwithchat

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/meetwithchat.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/meetwithchat.com/privkey.pem

    # 反向代理 WebSocket
    ProxyPreserveHost On
    # 明确指定 Upgrade 和 Connection 头
    RequestHeader set Connection "Upgrade"
    RequestHeader set Upgrade "websocket"
    ProxyPass /ws ws://127.0.0.1:2053/
    ProxyPassReverse /ws ws://127.0.0.1:2053/
    # 更安全的 RewriteRule (可选,但推荐)
    # RewriteEngine On
    # RewriteCond %{HTTP:Connection} Upgrade [NC]
    # RewriteCond %{HTTP:Upgrade} websocket [NC]
    # RewriteRule /(.*) ws://127.0.0.1:2053/$1 [P,L]

    ErrorLog ${APACHE_LOG_DIR}/meetwithchat_error.log
    CustomLog ${APACHE_LOG_DIR}/meetwithchat_access.log combined
</VirtualHost>
  • RequestHeader set Connection "Upgrade"RequestHeader set Upgrade "websocket" 明确告诉 Apache,这是一个 WebSocket 连接,需要特殊处理。 这两个指令通常是必需的, 不然连接可能被当做普通 HTTP 请求.
  • RewriteEngine On... (注释掉的部分): 这是一种更灵活的反向代理 WebSocket 的方式,使用 RewriteRule。通常情况下,ProxyPass 足够了,但如果遇到更复杂的情况,可以用 RewriteRule。不过对于这个简单例子, ProxyPass已经可以胜任了.
  • 确认mod_proxy_wstunnel已开启。 a2enmod proxy_wstunnel。 然后重启 Apache: systemctl restart apache2.

3. (进阶) Ratchet 服务器启用 TLS (可选,但更安全)

虽然你的需求是 Apache 来处理 TLS,但在 Ratchet 服务器端也启用 TLS,可以增加一层安全性 (端到端加密)。 这意味着 Apache 和 Ratchet 之间也会用 wss://

要让 Ratchet 支持 wss://,需要用到 ReactPHP 的 SecureServer

<?php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\Server\IoServer;
use React\Socket\Server as Reactor;
use React\Socket\SecureServer; // 引入 SecureServer

require __DIR__ . '/vendor/autoload.php';

class ChatServer implements MessageComponentInterface {
   // ...
}

$loop = React\EventLoop\Factory::create();

// 使用你的证书文件路径
$secureSocket = new SecureServer(
    new Reactor('0.0.0.0:2053', $loop), // 监听所有 IP, 方便测试.  生产环境最好还是 127.0.0.1
    $loop,
    [
        'local_cert' => '/etc/letsencrypt/live/meetwithchat.com/fullchain.pem',
        'local_pk' => '/etc/letsencrypt/live/meetwithchat.com/privkey.pem',
        'verify_peer' => false, // 在服务器端通常不需要验证客户端证书
        'verify_peer_name' => false,
    ]
);

$webSocket = new WsServer(new ChatServer());
$httpServer = new HttpServer($webSocket);
$server = new IoServer($httpServer, $secureSocket, $loop);

error_log("WebSocket server is running on port 2053 (wss)...");
$server->run();

  • SecureServer 用它来代替原来的 Reactor
  • local_certlocal_pk 指向你的 Let's Encrypt 证书和私钥文件。
  • 注意这里用了0.0.0.0:2053, 这会让Ratchet监听所有接口的2053端口. 这样方便你直接用 openssl s_client 测试(不需要通过Cloudflare 和 Apache), 来确认Ratchet 的 wss 是否正常. 确认正常后, 可以改回127.0.0.1:2053, 避免直接暴露到公网.

如果 Ratchet 启用了 TLS,记得修改 Apache 的 ProxyPassProxyPassReverse,把 ws:// 改成 wss://

ProxyPass /ws wss://127.0.0.1:2053/
ProxyPassReverse /ws wss://127.0.0.1:2053/

4. 检查防火墙和安全组

确保你的服务器防火墙 (比如 ufwiptables) 或者云服务商的安全组规则允许 Cloudflare 的 IP 地址访问你的服务器的 443 端口 (或者你自定义的其他端口)。 Cloudflare 的 IP 地址列表可以在这里找到: https://www.cloudflare.com/ips/

如果需要更细粒度的控制, 可以根据Cloudflare提供的IP列表, 只允许这些IP访问.

5. 其他

  • 检查 Apache 和 Ratchet 的日志: 查看错误日志,可能会有更详细的错误信息,帮助你定位问题。
  • 简化测试: 先不要通过 Cloudflare,直接在服务器上用 openssl s_client 测试 Apache 的 SSL/TLS 配置是否正确:
    openssl s_client -connect localhost:443 -servername meetwithchat.com
    
    如果这个命令失败,说明 Apache 的 SSL/TLS 配置有问题。 成功的话, 再尝试连接 websocket.

四、 总结

按照上面的步骤排查和修改,应该能解决 Cloudflare 525 错误。记住,关键在于确保 Cloudflare 到 Apache,Apache 到 Ratchet 的每一段连接都正确配置了 SSL/TLS。 如果在 Ratchet 也启用了 TLS,就实现了端到端加密,更安全。