Boost.asio+OpenSSL实现HTTPS代理握手失败问题求助
我正在使用Boost/OpenSSL/VS2019学习Windows下的TCP Socket编程,编写了一个回显服务器示例,与自定义客户端测试时运行正常。但将Chrome或IE11的HTTPS代理设置为该服务器的IP和443端口后,握手环节总是失败,错误信息为:
handshake failed "https proxy request (SSL routines, ssl3_get_record) [asio.ssl:336130203]"
我使用OpenSSL生成了自签名证书,自定义客户端通过ssl_context.load_verify_file("cert.pem")验证证书后可正常通信,且已将该证书安装到IE和Chrome中,但浏览器连接代理时仍握手失败。
疑问
- 要让浏览器正常完成握手,自签名证书中的域名、IP等标识信息是否必须与代理服务器的实际信息一致?当前我使用的是与测试机器无关的虚拟证书,但自定义客户端可正常工作。
- 我想实现一个简单的HTTPS中继,目前HTTP代理已正常运行。为集成SSL,首先需要确保代理能接受浏览器的HTTPS代理请求并传输数据,是否浏览器的HTTPS代理请求有特殊要求,或是我的实现方式有误?
尝试过的操作
- 使用OpenSSL生成自签名证书文件(cert.pem、key.pem、dh2048.pem)
- 基于上述证书构建服务器和客户端,服务器监听443端口,同机/跨机客户端与服务器均可正常握手并通信
- 将证书安装到IE和Chrome中
- 浏览器通过HTTPS代理连接服务器,握手失败
预期与实际结果
- 预期结果:浏览器与代理服务器可成功完成SSL握手
- 实际结果:浏览器握手失败,报错信息如上
代码实现
class session { public: session(boost::asio::io_service& io_service, boost::asio::ssl::context& context) : socket_(io_service, context) { } ssl_socket::lowest_layer_type& socket() { return socket_.lowest_layer(); } void start() { socket_.async_handshake(boost::asio::ssl::stream_base::server, boost::bind(&session::handle_handshake, this, boost::asio::placeholders::error)); } void handle_handshake(const boost::system::error_code& error) { if (!error) { std::cout << "handshake good" << std::endl; socket_.async_read_some(boost::asio::buffer(data_, max_length), boost::bind(&session::handle_read, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } else { std::cout << "handshake failed " + error.what() << std::endl; delete this; } } void handle_read(const boost::system::error_code& error, size_t bytes_transferred) { if (!error) { boost::asio::async_write(socket_, boost::asio::buffer(data_, bytes_transferred), boost::bind(&session::handle_write, this, boost::asio::placeholders::error)); } else { delete this; } } void handle_write(const boost::system::error_code& error) { if (!error) { socket_.async_read_some(boost::asio::buffer(data_, max_length), boost::bind(&session::handle_read, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } else { delete this; } } private: ssl_socket socket_; enum { max_length = 1024 }; char data_[max_length]; }; class server { public: server(boost::asio::io_service& io_service, unsigned short port) : io_service_(io_service), acceptor_(io_service, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)), context_(boost::asio::ssl::context::sslv23) { context_.set_options( boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use); context_.set_password_callback(boost::bind(&server::get_password, this)); context_.use_certificate_chain_file("cert.pem"); context_.use_private_key_file("key.pem", boost::asio::ssl::context::pem); context_.use_tmp_dh_file("dh2048.pem"); start_accept(); } std::string get_password() const { return "test"; } void start_accept() { session* new_session = new session(io_service_, context_); acceptor_.async_accept(new_session->socket(), boost::bind(&server::handle_accept, this, new_session, boost::asio::placeholders::error)); } void handle_accept(session* new_session, const boost::system::error_code& error) { if (!error) { std::cout << "accept good" << std::endl; new_session->start(); } else { delete new_session; } start_accept(); } private: boost::asio::io_service& io_service_; boost::asio::ip::tcp::acceptor acceptor_; boost::asio::ssl::context context_; };
针对疑问1:证书标识信息的要求
浏览器对证书的校验比自定义客户端严格得多,自签名证书的域名/IP必须与代理服务器的实际访问地址完全一致。自定义客户端仅做了证书存在性验证,没有校验主题标识(SAN)或通用名称(CN)与访问地址的匹配性,所以虚拟证书能正常工作。但浏览器会强制校验这一点,哪怕证书已安装到信任列表,只要标识不匹配,握手就会触发错误(虽当前报错是记录读取阶段,但标识不匹配是常见隐性阻碍)。
生成证书时,需在OpenSSL配置中添加与服务器IP/域名匹配的SAN字段,或把CN设置为服务器的IP/域名。比如:
- 生成证书时指定CN:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -subj "/CN=192.168.1.100" - 或在openssl.cnf中配置SAN:
subjectAltName = IP:192.168.1.100,DNS:yourdomain.com
针对疑问2:HTTPS代理的请求流程错误
你的核心逻辑完全错误——浏览器使用HTTPS代理时,不会直接与代理建立SSL连接,而是先建立普通TCP连接,发送CONNECT命令告知代理目标服务器的地址和端口,代理返回200 Connection Established后,浏览器才会通过代理与目标服务器建立SSL连接。
当前代码一接受连接就启动SSL握手,但此时浏览器正在发送明文的CONNECT请求,服务器把明文当成SSL记录解析,自然会报ssl3_get_record错误(SSL记录有固定格式,明文不符合要求)。
简单HTTPS中继的正确流程
- 接受客户端的普通TCP连接,不启动SSL握手
- 读取客户端发送的
CONNECT请求(格式示例:CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n) - 代理主动连接目标服务器(如example.com:443)
- 向客户端返回
HTTP/1.1 200 Connection Established\r\n\r\n - 启动双向数据转发:将客户端的所有数据中继到目标服务器,同时将目标服务器的数据中继回客户端——此阶段代理无需处理SSL握手,仅做透明TCP转发即可
代码修改示例
将session类改为先处理明文CONNECT请求,再做中继:
class session : public std::enable_shared_from_this<session> { public: session(boost::asio::io_service& io_service) : client_socket_(io_service), target_socket_(io_service) { } boost::asio::ip::tcp::socket& socket() { return client_socket_; } void start() { auto self(shared_from_this()); boost::asio::async_read_until(client_socket_, request_buf_, "\r\n\r\n", [this, self](const boost::system::error_code& error, std::size_t bytes_transferred) { if (!error) { // 解析CONNECT请求 std::string request(boost::asio::buffers_begin(request_buf_.data()), boost::asio::buffers_begin(request_buf_.data()) + bytes_transferred); size_t connect_pos = request.find("CONNECT "); size_t port_pos = request.find(':'); size_t http_pos = request.find(" HTTP/"); std::string target_host = request.substr(connect_pos + 8, port_pos - (connect_pos + 8)); std::string target_port_str = request.substr(port_pos + 1, http_pos - (port_pos + 1)); unsigned short target_port = std::stoi(target_port_str); // 连接目标服务器 boost::asio::ip::tcp::resolver resolver(client_socket_.get_io_service()); boost::asio::ip::tcp::resolver::query query(target_host, std::to_string(target_port)); resolver.async_resolve(query, [this, self](const boost::system::error_code& error, boost::asio::ip::tcp::resolver::iterator endpoint_iterator) { if (!error) { boost::asio::async_connect(target_socket_, endpoint_iterator, [this, self](const boost::system::error_code& error, boost::asio::ip::tcp::resolver::iterator) { if (!error) { // 返回连接成功响应 std::string response = "HTTP/1.1 200 Connection Established\r\n\r\n"; boost::asio::async_write(client_socket_, boost::asio::buffer(response), [this, self](const boost::system::error_code& error, std::size_t) { if (!error) { // 启动双向数据转发 start_client_to_target(); start_target_to_client(); } }); } }); } }); } }); } void start_client_to_target() { auto self(shared_from_this()); client_socket_.async_read_some(boost::asio::buffer(data_, max_length), [this, self](const boost::system::error_code& error, std::size_t bytes_transferred) { if (!error) { boost::asio::async_write(target_socket_, boost::asio::buffer(data_, bytes_transferred), [this, self](const boost::system::error_code& error, std::size_t) { if (!error) { start_client_to_target(); } }); } }); } void start_target_to_client() { auto self(shared_from_this()); target_socket_.async_read_some(boost::asio::buffer(data_, max_length), [this, self](const boost::system::error_code& error, std::size_t bytes_transferred) { if (!error) { boost::asio::async_write(client_socket_, boost::asio::buffer(data_, bytes_transferred), [this, self](const boost::system::error_code& error, std::size_t) { if (!error) { start_target_to_client(); } }); } }); } private: boost::asio::ip::tcp::socket client_socket_; boost::asio::ip::tcp::socket target_socket_; enum { max_length = 1024 }; char data_[max_length]; boost::asio::streambuf request_buf_; };
同时,server类需改为使用普通TCP Socket,移除SSL上下文相关逻辑。
内容的提问来源于stack exchange,提问作者Gordon Mitch

