Apache mod_proxy_wstunnel代理WebSocket报AH01991错误排查
我使用Apache的mod_proxy_wstunnel模块在客户端站点与后端WebSocket服务器之间做数据代理,但Apache会异常重置套接字连接。
当前我的服务端与客户端代码逻辑非常简单:
客户端JS代码
var socket = new WebSocket("wss://local-test.mysite.com/wss/", "basic-protocol"); socket.onopen = function(e) { console.log('open: ', e); }; socket.onerror = function(e) { console.log('error: ', e); }; socket.onmessage = function(e) { console.log('message: ', e); }; socket.onclose = function(e) { console.log('close: ', e); };
服务端核心代码(C++)
为精简内容已省略部分struct细节:
int server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); bind(server, ...); // 绑定到4433端口 listen(server, 1024); int client = accept(server, ...); char buff[4096]; long r = recv(client, buffer, sizeof(buffer), 0);
故障现象
握手请求接收流程可正常运行:JS加载完成并创建WebSocket实例后,服务端可正常接收到如下HTTP请求报文:
GET /wss/ HTTP/1.1 Host: localhost:4433 Pragma: no-cache Cache-Control: no-cache User-Agent: { user agent string } Origin: https://local-test.mysite.com Sec-WebSocket-Version: 13 Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.9 Cookie: { cookie data here } Sec-WebSocket-Key: {base64 key} Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Protocol: basic-protocol X-Forwarded-For: 192.168.11.1 X-Forwarded-Host: local-test.mysite.com X-Forwarded-Server: local-test.mysite.com Connection: Upgrade Upgrade: websocket
我解析报文中的Sec-WebSocket-Key字段,拼接WebSocket标准魔数("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"),计算SHA1哈希值后做base64编码,向客户端返回如下握手响应:
HTTP/1.1 101 Switching Protocols\r\n Connection: Upgrade\r\n Upgrade: websocket\r\n Sec-WebSocket-Accept: {hashed response key}\r\n Sec-WebSocket-Protocol: basic-protocol\r\n\r\n
返回响应后服务端回到recv调用等待后续前端发送的数据(通过页面DOM按钮触发发送),但立刻抛出ECONNRESET错误;同时前端JS WebSocket连接以1006错误码(异常终止)关闭,Apache日志输出如下报错:
[ssl:info] (70014)End of file found: [client 192.168.11.1:54673] AH01991: SSL input filter read failed.
我无法确定问题根因是Apache代理配置错误还是服务端握手响应不符合规范,以下是对应虚拟主机的Apache配置内容:
Listen local-test.mysite.com:80 Listen local-test.mysite.com:443 <VirtualHost local-test.mysite.com:80> RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,QSA] </VirtualHost> <VirtualHost local-test.mysite.com:443> ServerAdmin admin@mysite DocumentRoot "/Code/source/dev/web" ErrorLog /var/log/httpd/web/error.log CustomLog /var/log/httpd/web/access.log common ServerName local-test.mysite.com ServerAlias local-test SSLEngine on SSLOptions +StrictRequire SSLCertificateFile /etc/httpd/ssl/mysite.crt SSLCertificateKeyFile /etc/httpd/ssl/mysite.key <Directory /Code/source/dev/web> RewriteEngine On RewriteBase / RewriteRule ^/index\.php$ - [L,NC] RewriteCond %{REQUEST_URI} !\.(png|jpg|gif|jpeg|bmp|svg|ico)$ RewriteCond %{REQUEST_URI} !\.(css|js|map|ttf|woff|woff2|eot)$ RewriteRule . index.php [L] </Directory> # Websocket config SSLProxyEngine on ProxyPreserveHost On ProxyRequests Off ProxyPass /wss ws://localhost:4433 keepalive=On enablereuse=On connectiontimeout=1800 ProxyPassReverse /wss ws://localhost:4433 RewriteEngine on RewriteCond %{HTTP:Upgrade} =websocket [NC] RewriteRule /(.*) ws://localhost:4433%{REQUEST_URI} [P,L] </VirtualHost>
我已尝试调整ProxyPass与Rewrite规则的多种组合配置,均未解决问题。
当前运行环境
- Apache/2.4.51 (Fedora)
- Chrome 版本 103.0.5060.53
问题由两处错误共同导致,按顺序修改即可解决:
1. 服务端握手响应格式错误
你写的响应字符串里手动加了\r\n换行符,但代码里每一行响应头直接做了物理换行,实际输出时会在每个\r\n前额外多一个LF字符,相当于响应头段提前出现了空行,Apache代理校验HTTP响应格式不通过,会直接主动断开连接。
正确的响应要保证所有头行仅以\r\n作为行结尾,不要在字符串内硬敲物理换行,参考写法:
const char* ws_resp = "HTTP/1.1 101 Switching Protocols\r\n" "Connection: Upgrade\r\n" "Upgrade: websocket\r\n" "Sec-WebSocket-Accept: {计算后的正确accept值}\r\n" "Sec-WebSocket-Protocol: basic-protocol\r\n" "\r\n";
同时校验Sec-WebSocket-Accept的计算逻辑:必须是Sec-WebSocket-Key的值拼接魔数字符串后,取SHA1计算得到的20字节原始二进制结果做base64编码,不要把SHA1输出的十六进制字符串拿去做base64,值错误同样会导致握手失败。
2. Apache配置存在规则冲突
你同时配置了ProxyPass和RewriteRule的WebSocket转发规则,两条规则会重复匹配请求,导致代理链路异常。另外enablereuse=On参数是给普通HTTP长连接连接池复用设计的,和WebSocket升级后的长连接存在兼容性问题,会导致连接被代理提前回收。
删掉多余的Rewrite转发规则,修正ProxyPass的参数和路径匹配即可,修改后的WebSocket配置段如下:
# Websocket config SSLProxyEngine on ProxyPreserveHost On ProxyRequests Off # 统一路径结尾斜杠,去掉不兼容的enablereuse参数 ProxyPass /wss/ ws://localhost:4433/wss/ keepalive=On connectiontimeout=1800 ProxyPassReverse /wss/ ws://localhost:4433/wss/
改完后重启Apache服务,再测试WebSocket连接即可正常建立,不会再出现1006错误码和连接重置问题。
内容的提问来源于stack exchange,提问作者txtechhelp

