求助:基于Hyper实现支持mTLS连接的HTTP代理
基于Hyper实现支持mTLS的HTTP代理问题
问题现象
我基于Hyper的HTTP代理示例,添加tokio-rustls实现支持mTLS的代理,处理HTTP隧道的代码如下。测试时用Kotlin代码通过该代理访问要求mTLS认证的本地Nginx服务器,抛出Unsupported or unrecognized SSL message错误,Nginx日志显示收到的是加密格式的请求数据。
Rust代理隧道处理代码
async fn tunnel( mut upgraded: Upgraded, destination: (String, u16), certificates: Certificates, ) -> std::io::Result<()> { let root_cert_store = prepare_cert_store(&certificates); let tls_client_config = tls_config(certificates, root_cert_store); let tls_connector = TlsConnector::from(Arc::new(tls_client_config)); let target = TcpStream::connect(target_address(&destination)).await?; let domain = rustls::ServerName::try_from(destination.0.as_str()).expect("Invalid DNSName"); let mut tls_target = tls_connector.connect(domain, target).await?; debug!("TlS Connection ready"); let (wrote, recvd) = tokio::io::copy_bidirectional(&mut upgraded, &mut tls_target).await?; debug!("Client wrote {} and received {} bytes", wrote, recvd); Ok(()) }
Kotlin测试代码
fun main() { val fm = FuelManager.instance fm.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("127.0.0.1", 8100)) repeat(1) { val response = "https://nginx-test".httpGet() .responseString() println("Response: $response") println("Response Data: ${response.second.data.toString(Charset.defaultCharset())}") } }
Nginx配置
server { listen 80; location / { return 301 https://$host$request_uri; } } server { listen 443 ssl; server_name nginx.test; ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_client_certificate /etc/nginx/client_certs/ca.crt; ssl_verify_client optional; ssl_verify_depth 2; location / { if ($ssl_client_verify != SUCCESS) { return 403; } proxy_set_header SSL_Client_Issuer $ssl_client_i_dn; proxy_set_header SSL_Client $ssl_client_s_dn; proxy_set_header SSL_Client_Verify $ssl_client_verify; return 200 "yay"; } }
问题根源
你的代码逻辑混淆了HTTP隧道的工作模式:
- 当客户端通过HTTP代理访问HTTPS站点时,会先发送
CONNECT请求给代理,代理与目标服务器建立TCP连接(而非TLS连接),然后返回200 Connection Established给客户端。 - 之后客户端会直接与目标服务器通过代理的隧道进行TLS握手(包括mTLS所需的客户端证书验证),代理仅做透明的字节转发。
但你现在的代码中,代理主动与目标服务器建立了TLS连接,随后把客户端发送的TLS握手加密数据直接转发到这个已加密的连接中。此时Nginx已经和代理处于TLS会话中,会把收到的客户端TLS数据当成应用层HTTP请求,自然无法识别,从而抛出Unsupported or unrecognized SSL message错误。
解决方案
根据你的需求,有两种正确的实现方式:
方案1:代理作为透明隧道,客户端直接与目标做mTLS
如果你的需求是让客户端自己完成与目标服务器的mTLS认证,代理仅做流量转发,那么只需要去掉代理端的TLS连接逻辑,直接转发TCP流即可:
async fn tunnel( mut upgraded: Upgraded, destination: (String, u16), ) -> std::io::Result<()> { let mut target = TcpStream::connect(target_address(&destination)).await?; debug!("TCP Connection ready"); let (wrote, recvd) = tokio::io::copy_bidirectional(&mut upgraded, &mut target).await?; debug!("Client wrote {} and received {} bytes", wrote, recvd); Ok(()) }
此时需要确保客户端配置了正确的客户端证书,以便和Nginx完成mTLS握手。
方案2:代理作为mTLS客户端,与目标服务器完成认证
如果你的需求是代理代表客户端与目标服务器进行mTLS认证(客户端与代理之间用普通HTTP通信),那么不能使用CONNECT隧道模式,而是需要代理解析客户端的HTTP请求,然后作为mTLS客户端向目标服务器发起请求,再将响应返回给客户端:
- 放弃
CONNECT隧道的处理逻辑,改为直接处理客户端的HTTP请求。 - 用tokio-rustls创建TLS连接器,向目标服务器发起HTTPS请求(携带客户端证书)。
- 将目标服务器的响应转发给客户端。
这种模式下,客户端不需要配置任何证书,所有mTLS逻辑都在代理端完成。
内容的提问来源于stack exchange,提问作者Bruno Ortiz
相关产品推荐
相关产品推荐

